这是一个权限验证系统的一部分。对象包括:帐户(Account)、角色(Role)和资源(Resource)。它们之间的关系是,多个帐户对应一个角色,一个角色可以拥有多个资源,一个资源也和多个角色存在关系。所以有 Account : Role = n : 1 ,Role : Resource = n : m 。本文只介绍 Castle AR 的多对多关系解决方案,所以忽略 Account,只研究 Role 和 Resource。又由于多对多关系中,一方对另一方的操作在另一方的角度而言在脱离数据概念时是等价的,所以本例只使用 Role 对象作测试。

数据库结构设计



本例中,两个对象通过一个关联表实现多对多关系,因此实际上有三个数据表。SQL Server 2000 生成的 SQL 建表脚本如下:

 

CREATE TABLE [WebFolder_Resource] (
    
[ResourceID] [int] IDENTITY (11NOT NULL ,
    
[ResourceName] [varchar] (50) COLLATE Chinese_PRC_CI_AS NOT NULL ,
    
[RequestName] [varchar] (50) COLLATE Chinese_PRC_CI_AS NOT NULL ,
    
[ShowOrder] [varchar] (10) COLLATE Chinese_PRC_CI_AS NOT NULL 
ON [PRIMARY]
GO

CREATE TABLE [WebFolder_Role] (
    
[RoleID] [int] IDENTITY (11NOT NULL ,
    
[RoleName] [varchar] (50) COLLATE Chinese_PRC_CI_AS NOT NULL ,
    
[RoleDescription] [varchar] (50) COLLATE Chinese_PRC_CI_AS NOT NULL 
ON [PRIMARY]
GO

CREATE TABLE [WebFolder_RoleResource] (
    
[RoleID] [int] NOT NULL ,
    
[ResourceID] [int] NOT NULL 
ON [PRIMARY]
GO

ALTER TABLE [WebFolder_Resource] ADD 
    
CONSTRAINT [PK_WebFolder_Resource] PRIMARY KEY  CLUSTERED 
    (
        
[ResourceID]
    )  
ON [PRIMARY] 
GO

ALTER TABLE [WebFolder_Role] ADD 
    
CONSTRAINT [PK_WebFolder_Role] PRIMARY KEY  CLUSTERED 
    (
        
[RoleID]
    )  
ON [PRIMARY] 
GO

依赖的类库

 

You must reference the following set of assemblies to use ActiveRecord:

  • Castle.ActiveRecord.dll
  • Castle.Model.dll
  • Nullables.dll

But ActiveRecord also depends on NHibernate, so you must reference the following as well:

  • NHibernate.dll
  • Castle.DynamicProxy.dll (Curious? Check DynamicProxy)
  • Nullables.NHibernate.dll
  • log4net.dll
  • Iesi.Collections.dll

领域对象

Role.cs (StephenCat.WebFolder.Domain.Role)

 

using System;
using System.Collections;

using Castle.ActiveRecord;

namespace StephenCat.WebFolder.Domain
{
    
/// <summary>
    
/// Role 的摘要说明。
    
/// </summary>
    [ActiveRecord("WebFolder_Role")]
    
public class Role : ActiveRecordBase
    {
        
/// <summary>
        
/// 创建一个角色
        
/// </summary>
        public Role()
        {
        }

        
private int _id;
        
private String _name;
        
private String _description;

        
private IList _accounts;

        
private IList _resources;

        
/// <summary>
        
/// 角色编号
        
/// </summary>
        [PrimaryKey(PrimaryKeyType.Native, "RoleID")]
        
public int Id
        {
            
get { return _id; }
            
set { _id = value; }
        }

        
/// <summary>
        
/// 角色名称
        
/// </summary>
        [Property("RoleName")]
        
public String Name
        {
            
get { return _name; }
            
set { _name = value; }
        }

        
/// <summary>
        
/// 角色说明
        
/// </summary>
        [Property("RoleDescription")]
        
public String Description
        {
            
get { return _description; }
            
set { _description = value; }
        }

        
/// <summary>
        
/// 角色所拥有的帐号集合
        
/// </summary>
        
        [HasMany(
typeof(Resource), Table="Account", ColumnKey="RoleID", Lazy=true)]
        
public IList Accounts
        {
            
get { return _accounts; }
            
set { _accounts = value; }
        }

        
/// <summary>
        
/// 角色所拥有的资源集合
        
/// </summary>
        [HasAndBelongsToMany( typeof(Resource), 
             Table
="WebFolder_RoleResource"
             ColumnRef
="ResourceID", ColumnKey="RoleID" )]
        
public IList Resources
        {
            
get { return _resources; }
            
set { _resources = value; }
        }


        
/// <summary>
        
/// 删除所有角色
        
/// </summary>
        public static void DeleteAll()
        {
            DeleteAll( 
typeof(Role) );
        }

        
/// <summary>
        
/// 获取所有角色
        
/// </summary>
        
/// <returns>角色集合</returns>
        public static Role[] FindAll()
        {
            
return (Role[]) FindAll( typeof(Role) );
        }

        
/// <summary>
        
/// 获取一个角色
        
/// </summary>
        
/// <param name="id">角色编号</param>
        
/// <returns>角色实体</returns>
        public static Role Find(int id)
        {
            
return (Role) FindByPrimaryKey( typeof(Role), id );
        }

    }
}

Resource.cs (StephenCat.WebFolder.Domain.Resource)

 

using System;
using System.Collections;

using Castle.ActiveRecord;

namespace StephenCat.WebFolder.Domain
{
    
/// <summary>
    
/// Resource 的摘要说明。
    
/// </summary>
    [ActiveRecord("WebFolder_Resource")]
    
public class Resource : ActiveRecordBase
    {
        
/// <summary>
        
/// 创建一个资源
        
/// </summary>
        public Resource()
        {
        }

        
private int _id;
        
private String _name;
        
private String _requestname;
        
private String _showorder;

        
private IList _roles;

        
/// <summary>
        
/// 资源编号
        
/// </summary>
        [PrimaryKey(PrimaryKeyType.Native, "ResourceID")]
        
public int Id
        {
            
get { return _id; }
            
set { _id = value; }
        }

        
/// <summary>
        
/// 资源名称
        
/// </summary>
        [Property("ResourceName")]
        
public String Name
        {
            
get { return _name; }
            
set { _name = value; }
        }

        
/// <summary>
        
/// 请求名称
        
/// </summary>
        [Property("RequestName")]
        
public String RequestName
        {
            
get { return _requestname; }
            
set { _requestname = value; }
        }

        
/// <summary>
        
/// 序号
        
/// </summary>
        [Property("ShowOrder")]
        
public String ShowOrder
        {
            
get { return _showorder; }
            
set { _showorder = value; }
        }

        
/// <summary>
        
/// 资源所拥有的角色集合
        
/// </summary>
        [HasAndBelongsToMany( typeof(Role), 
             Table
="WebFolder_RoleResource"
             ColumnRef
="RoleID", ColumnKey="ResourceID" )]
        
public IList Roles
        {
            
get { return _roles; }
            
set { _roles = value; }
        }

        
/// <summary>
        
/// 删除所有资源
        
/// </summary>
        public static void DeleteAll()
        {
            DeleteAll( 
typeof(Resource) );
        }

        
/// <summary>
        
/// 获取所有资源
        
/// </summary>
        
/// <returns>资源集合</returns>
        public static Resource[] FindAll()
        {
            
return (Resource[]) FindAll( typeof(Resource) );
        }

        
/// <summary>
        
/// 获取一个资源
        
/// </summary>
        
/// <param name="id">资源编号</param>
        
/// <returns>资源集合</returns>
        public static Resource Find(int id)
        {
            
return (Resource) FindByPrimaryKey( typeof(Resource), id );
        }


    }
}

单元测试

测试工具

TestDriven.NET 2.0 

硬件环境

CPU: Intel Pentium IV 2.6c (support hyper-threading)
MB: Asus P4P800
RAM: 512 mb DDR
HD: Seagate ATA 120G x 2 (no RAID)

软件环境

OS: Microsoft Windows Server 2003 Enterprise Edition with Service Pack 1
IDE: Microsoft Visual Studio.NET 2003 Enterprise Architecture Edition
DB: Microsoft SQL Server 2000 with Service Pack 4
Virtual Memory: 2048 ~ 3096 MB on Drive C and 2048 ~ 3096 MB on Drive D

测试用例及结果

 

using System;  // 用于控制台输出

using System.Collections;  // 用于 IList 操作

using NUnit.Framework;  // 用于单元测试

using System.Reflection;
using Castle.ActiveRecord.Framework.Config;  // 用于读取内嵌的 Castle AR 配置

using Castle.ActiveRecord;  // 用于启动 AR 特性

using StephenCat.WebFolder.Domain;  // 领域对象

namespace StephenCat.WebFolder._TestUnitCase
{
    
/// <summary>
    
/// RoleTest 的摘要说明。
    
/// </summary>
    [TestFixture]
    
public class RoleTest
    {
        
public RoleTest()
        {
            
//
            
// TODO: 在此处添加构造函数逻辑
            
//
        }

        
/// <summary>
        
/// 测试创建一个角色
        
/// </summary>
        [Test]
        
public void RoleCreateTest()
        {
            Assembly assembly 
= typeof(Account).Assembly;
            XmlConfigurationSource src 
= new XmlConfigurationSource(assembly.GetManifestResourceStream("StephenCat.WebFolder.Domain.ActiveRecord.config"));
            ActiveRecordStarter.Initialize( src, 
typeof(Role),typeof(Resource) );

            
// 注意:由于存在关联,这里要初始化两个实体。
            
//       否则会抛出 unmapped: Resource 的 Exception。

            
//using(new SessionScope())
            
//{
                Role role = new Role();

                role.Name 
= "sysadmin";
                role.Description 
= "administrator1234";

                role.Save();
            
//}
        }

        
/// <summary>
        
/// 测试不清除原来记录的情况下向角色添加三个资源(循环 n 次测试)。
        
/// </summary>
        [Test]
        
public void RoleAddAgainResourceTest()
        {
            Assembly assembly 
= typeof(Account).Assembly;
            XmlConfigurationSource src 
= new XmlConfigurationSource(assembly.GetManifestResourceStream("StephenCat.WebFolder.Domain.ActiveRecord.config"));
            ActiveRecordStarter.Initialize( src, 
typeof(Role),typeof(Resource) );

            
// 注意:由于存在关联,这里要初始化两个实体。
            
//       否则会抛出 unmapped: Resource 的 Exception。

            Role role 
= Role.Find(3);

            role.Resources.Add(Resource.Find(
2));
            role.Resources.Add(Resource.Find(
3));
            role.Resources.Add(Resource.Find(
5));

            role.Update();  
// 循环三次会出现九条记录。AR 不会考虑仅仅保留唯一记录。

            
/*
             * 控制台输出:
            ------ Test started: Assembly: StephenCat.WebFolder.dll ------

            1 passed, 0 failed, 0 skipped, took 2.27 seconds.
             
*/
        }

        
/// <summary>
        
/// 测试删除所有角色所拥有的资源
        
/// </summary>
        [Test]
        
public void RoleRemoveAllResourceTest()
        {
            Assembly assembly 
= typeof(Account).Assembly;
            XmlConfigurationSource src 
= new XmlConfigurationSource(assembly.GetManifestResourceStream("StephenCat.WebFolder.Domain.ActiveRecord.config"));
            ActiveRecordStarter.Initialize( src, 
typeof(Role),typeof(Resource) );

            
// 注意:由于存在关联,这里要初始化两个实体。
            
//       否则会抛出 unmapped: Resource 的 Exception。

            Role role 
= Role.Find(3);

            role.Resources.Clear(); 
// 到这步仍未真正删除所有拥有的资源

            role.Update(); 
// 更新角色信息,真正实施从数据表删除记录。

            Console.WriteLine(role.Resources.Count); 

        }

        
/// <summary>
        
/// 测试给角色从无到有地添加资源的作用
        
/// </summary>
        [Test]
        
public void RoleAddResourceTest()
        {
            Assembly assembly 
= typeof(Account).Assembly;
            XmlConfigurationSource src 
= new XmlConfigurationSource(assembly.GetManifestResourceStream("StephenCat.WebFolder.Domain.ActiveRecord.config"));
            ActiveRecordStarter.Initialize( src, 
typeof(Role),typeof(Resource) );

            
// 注意:由于存在关联,这里要初始化两个实体。
            
//       否则会抛出 unmapped: Resource 的 Exception。

            Role role 
= Role.Find(3);  // 本来就有三个资源
            Console.WriteLine(role.Resources.Count);

            Role role4 
= Role.Find(4); // 本来没有资源
            Console.WriteLine(role4.Resources.Count);

            role4.Resources.Add(Resource.Find(
2)); // 添加一个资源,未真正添加到数据表
            Console.WriteLine(role4.Resources.Count);

            role4.Update(); 
// 真正添加到数据表
            /*
            控制台输出结果:
            
            ------ Test started: Assembly: StephenCat.WebFolder.dll ------
            3
            0
            1
            
            1 passed, 0 failed, 0 skipped, took 2.23 seconds.
            
            
*/
            
        }

        
/// <summary>
        
/// 测试更新角色所拥有的资源
        
/// </summary>
        [Test]
        
public void RoleUpdateResourceTest()
        {
            Assembly assembly 
= typeof(Account).Assembly;
            XmlConfigurationSource src 
= new XmlConfigurationSource(assembly.GetManifestResourceStream("StephenCat.WebFolder.Domain.ActiveRecord.config"));
            ActiveRecordStarter.Initialize( src, 
typeof(Role),typeof(Resource) );

            
// 注意:由于存在关联,这里要初始化两个实体。
            
//       否则会抛出 unmapped: Resource 的 Exception。

            Role role 
= Role.Find(3);  // 本来有三个资源
            Console.WriteLine(role.Resources.Count);

            role.Resources.Clear(); 
// 尝试删除全部所拥有的资源
            role.Resources.Add(Resource.Find(1)); // 添加一个资源
            Console.WriteLine(role.Resources.Count); 

            role.Update(); 
// 更新到数据表
            /*
            控制台输出结果:
            
            ------ Test started: Assembly: StephenCat.WebFolder.dll ------

            3
            1

            1 passed, 0 failed, 0 skipped, took 2.27 seconds.
            
            
*/
        }

        
/// <summary>
        
/// 测试角色是否拥有某个资源,使用 ActiveRecord 本来的方法
        
/// </summary>
        [Test]
        
public void RoleHasResourceARTest()
        {
            Assembly assembly 
= typeof(Account).Assembly;
            XmlConfigurationSource src 
= new XmlConfigurationSource(assembly.GetManifestResourceStream("StephenCat.WebFolder.Domain.ActiveRecord.config"));
            ActiveRecordStarter.Initialize( src, 
typeof(Role),typeof(Resource) );

            
// 注意:由于存在关联,这里要初始化两个实体。
            
//       否则会抛出 unmapped: Resource 的 Exception。

            Role role 
= Role.Find(3);

            
// 遍历角色所拥有的资源
            foreach(Resource res in role.Resources)
            {
                Console.WriteLine(res.Id);
            }
            
/*
            控制台输出:
            
            1
            2
            3
            5
            
            
*/

            Console.WriteLine(role.Resources.Contains(Resource.Find(
2)));  
            
// 由于 GetHasCode() 的值不同,所以返回 False。
            
// 因此不能使用 Contains 方法直接判断对象是否相等。

            
// 通过判断对象的属性是否相等来判断角色是否包含一个资源,这样才会返回 True。
            
            
foreach(Resource res in role.Resources)
            {
                
if(res.Id==Resource.Find(2).Id)
                {
                    Console.WriteLine(
true);
                    
break;
                }
            }
            
// 比较 stupid,最终还是要面向数据库而非面向对象。
            
// 要面向对象的话,只能自己在每个 domain 类里面手写方法了。
        }

        [Test]
        
public void RoleRemoveResourceTest()
        {
            Assembly assembly 
= typeof(Account).Assembly;
            XmlConfigurationSource src 
= new XmlConfigurationSource(assembly.GetManifestResourceStream("StephenCat.WebFolder.Domain.ActiveRecord.config"));
            ActiveRecordStarter.Initialize( src, 
typeof(Role),typeof(Resource) );

            
// 注意:由于存在关联,这里要初始化两个实体。
            
//       否则会抛出 unmapped: Resource 的 Exception。

            Role role 
= Role.Find(3);

            role.Resources.Remove(Resource.Find(
2));
            role.Update();
            
// 这样也是不能真正删除指定的资源的,只能通过循环检查删除了

            
foreach(Resource res in role.Resources)
            {
                
if(res.Id==Resource.Find(2).Id)
                {
                    role.Resources.Remove(res);
                    
break;
                }
            }
            role.Update();  
// 这样才真正删除了。
        }
    }
}

小结

Castle ActiveRecord 只是把数据表的一行数据映射为一个 .NET 类,把多行数据映射为 Array 数组,数组的元素是 .NET 类。尽管 AR 通过 ActiveRecordBase 类也提供了一些 DAO 应该有的方法,但仍然有某些方法需要自己手工编写。

Castle AR 并没有把对象和关系绝对领域化,领域业务方法甚至某些 DAO 方法仍需要自己编写,因此 Castle AR 在封装了 NHibernate 之后也只是完成了对象关系和数据之间的映射工作而已,也仅此而已