65.9K
CodeProject 正在变化。 阅读更多。
Home

适用于 .NET 的轻量级 ORM 库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (37投票s)

2007 年 10 月 24 日

CPOL

17分钟阅读

viewsIcon

234531

downloadIcon

3165

本文介绍了 Light 对象关系映射库。

弃用说明

本文提供的代码已弃用,不再维护。建议使用新版本的库。新库与本文提供的代码不兼容。它对整个库进行了重写,应该会更友好、更易于使用。新库可在此处获取。

前言

此版本代码存在许多重大更改。此版本与早期版本不向后兼容。文章文本已更新以反映这些更改,此处提供的代码片段仅适用于 Light 的最新版本。

引言

本文介绍了一个小巧简单的 ORM 库。市面上有许多优秀的 ORM 解决方案,为什么我还要写一个呢?好吧,主要原因很简单:我喜欢确切地知道我的应用程序中运行的代码以及它在做什么。此外,如果我遇到异常,我希望能不打开调试器就能准确找到它可能源于的代码位置。其他显而易见的原因包括我想知道如何编写其中一个,以及不必为每个域对象编写简单的 CRUD ADO.NET 命令。

目的与目标

此库的目的是允许客户端代码(用户)为域对象运行基本数据库命令。假设一个对象代表数据库表中的一条记录。我认为可以安全地说,我们大多数编写处理数据库的面向对象代码的人都以某种形式拥有这些对象。因此,目标是创建一个小型库,允许我重用这些对象,而不会将我限制在任何继承或接口实现中。

此外,我希望保持控制权:我绝对不希望有东西为我生成表或类。同样,我希望避免使用 XML 文件进行映射信息,因为这会增加另一个需要维护代码的地方。我理解这增加了灵活性,但在我的情况下,它是没有必要的。

设计

我希望实现的一个目标是让用户控制数据库连接。连接是用户需要为使此库正常工作而提供的唯一资源。此 ORM 库(Light)允许用户对提供的数据库连接运行简单的 INSERTUPDATEDELETESELECT 语句。它甚至不尝试管理外键或同时处理多个相关对象。相反,Light 提供了所谓的触发器(请参阅下面的触发器部分),这些触发器允许您获得类似的结果。因此,库的范围是:单个表/视图映射到单个对象类型。

Using the Code

Light 使用属性和反射来确定它需要执行哪些语句来完成工作。有两个非常直接的属性用于描述对象映射到的表

  • TableAttribute - 此属性可用于类、接口或结构。它定义了对象类型映射到的表的名称和架构。它还允许您指定为该表提供自动生成编号的数据库序列的名称(当然,目标数据库必须支持序列)。
  • ColumnAttribute - 此属性可用于属性或字段。它定义了列名、数据库数据类型、大小(对于非字符串类型是可选的),以及其他设置,如小数的精度和小数位数。

还有两个属性有助于继承和接口实现

  • TableRefAttribute - 此属性可用于类、接口或结构。如果您需要将表定义委托给另一个类型,它会很有用。
  • MappingAttribute - 此属性可用于类、接口或结构。它扩展了 ColumnAttribute(因此继承了它的所有属性),并添加了一个成员名称的属性。此属性应用于将继承的成员映射到列。稍后将在代码示例中进一步说明。

还有另一个属性可以帮助处理对象验证和相关对象管理等事宜

  • TriggerAttribute - 此属性只能应用于具有特定签名的类。简而言之,它将一个类标记为触发器。这些触发器类在 4 种 CRUD 操作的任何一种之前或之后执行。稍后将在代码示例中进一步说明。

Light 库中最有用的类是 Dao 类。Dao 在这里代表数据访问对象。此类的实例提供对给定对象执行插入、更新、删除和选择的方法,前提是对象已正确使用属性进行修饰。如果给定的对象未正确修饰或为 null,则会引发异常。

关于异常需要说明几句。Light 可能引发几种异常。最重要的是 System.Data.Common.DbException,当在执行数据库语句时发生数据库错误时会引发此异常。如果您的底层数据库是 SQL Server,那么将捕获到的 DbException 异常强制转换为 SqlException 是安全的。其他异常包括:DeclarationException,当类未正确使用属性修饰时引发;TriggerException,当触发器类引发异常时引发;以及 LightException,用于一般错误并将可能发生的任何其他异常包装起来。

请注意,DeclarationExceptionTriggerException 都是 LightException 的子类,因此 catch 语句 catch(LightException e) 将捕获所有三种异常类型。如果您想专门处理 DeclarationExceptionTriggerException,它们的 catch 语句必须出现在捕获 LightExceptioncatch 语句之前。示例如下:

try {
    T t = new T();
    dao.Insert<T>(t);
}
catch(DbException e) {
    SqlException sqle = (SqlException) e;
}
catch(DeclarationException e) {
    ...
}
catch(TriggerException e) {
    ...
}
catch(LightException e) {
    if(e.InnerException != null) //then the following is always true
        bool truth = e.Message.Equals(e.InnerException.Message);
}

您无法直接使用构造函数创建 Dao 类的实例,因为 Dao 是一个 abstract 类。相反,您应该创建针对您的数据库的 Dao 子类的实例。到目前为止,Light 可以与 SQL Server (SqlServerDao) 和 SQLite .NET 提供程序 (SQLiteDao) 数据库配合使用,而无需进行任何修改。如果您需要针对其他数据库引擎,或者希望覆盖 SQL Server 或 SQLite 的默认实现,您所要做的就是创建一个扩展 Dao 类的类并实现其所有 abstract 方法。

除非存在由同一 Dao 实例启动的 **显式** 事务,否则所有操作(Select 除外)都将在 **隐式** 事务中执行。在这种情况下,将使用现有事务。用户必须提交或回滚显式事务。如果在事务处理过程中调用了 Dao 对象的 Dispose 方法,则该事务将被回滚。显式事务是通过用户调用 Dao.Begin 方法启动的事务。隐式事务由 Dao 对象在内部处理,并在命令成功执行后自动提交,或者在命令执行期间发生异常时自动回滚。

请注意,要使所有这些工作正常进行,Dao 对象必须与已打开的数据库连接关联。这可以通过 Dao.Connection 属性来完成。SqlServerDaoSQLiteDao 还提供了接受连接作为参数的构造函数。请记住,管理 Light 使用的数据库连接是您的责任。这意味着您负责打开和关闭所有数据库连接。在调用 Dao 对象的任何方法之前,必须打开连接。Dao 对象 **绝不会** 调用任何连接的 OpenClose 方法,即使发生异常也是如此。以下是一些示例代码,用于演示此概念。假设我们将连接到一个具有以下已定义表的 SQL Server 数据库

create table dbo.person (
    id int not null identity(1,1) primary key,
    name varchar(30),
    dob datetime
)
go

现在,我们来编写一些代码。请注意,此代码未经测试,可能无法编译;请参阅演示项目作为工作示例

using System;
using System.Data;
using System.Data.SqlClient;
using System.Collections;
using System.Collections.Generic;

using Light; // Light library namespace - this is all you need to use it.

//
// Defines a mapping of this interface type to the dbo.person table.
//
[Table("person", "dbo")]
public interface IPerson
{
    [Column("id", DbType.Int32, PrimaryKey=true, AutoIncrement=true)]
    Id { get; set; }

    [Column("name", DbType.AnsiString, 30)]
    Name { get; set; }

    [Column("dob", DbType.DateTime)]
    Dob { get; set; }
}

//
// Says that when operating on type Mother the table definition from
// type IPerson should be used.
//
[TableRef(typeof(IPerson))]
public class Mother : IPerson
{
    private int id;
    private string name;
    private DateTime dob;

    public Mother() {}

    public Mother(int id, string name, DateTime dob)
    {
        this.id = id;
        this.name = name;
        this.dob = dob;
    }

    public int Id
    {
        get { return id; }
        set { id = value; }
    }

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    public DateTime Dob
    {
        get { return dob; }
        set { dob = value; }
    }
}

//
// Notice that this class is identical to Mother but does not
// implement the IPerson interface, so it has to define its
// own mapping.
//
[Table("person", "dbo")]
public class Father
{
    private int id;
    private string name;
    private DateTime dob;

    public Father() {}

    public Father(int id, string name, DateTime dob)
    {
        this.id = id;
        this.name = name;
        this.dob = dob;
    }

    [Column("id", DbType.Int32, PrimaryKey=true, AutoIncrement=true)]
    public int Id
    {
        get { return id; }
        set { id = value; }
    }

    [Column("name", DbType.AnsiString, 30)]
    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    [Column("dob", DbType.DateTime)]
    public DateTime Dob
    {
        get { return dob; }
        set { dob = value; }
    }
}

//
// Same thing but using a struct.
//
[Table("person", "dbo")]
public struct Son
{
    [Column("id", DbType.Int32, PrimaryKey=true, AutoIncrement=true)]
    public int Id;
    [Column("name", DbType.AnsiString, 30)]
    public string Name;
    [Column("dob", DbType.DateTime)]
    public DateTime Dob;
}

//
// Delegating with a struct.
//
[TableRef(typeof(IPerson))]
public struct Daughter : IPerson
{
    private int id;
    private string name;
    private DateTime dob;

    public int Id
    {
        get { return id; }
        set { id = value; }
    }

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    public DateTime Dob
    {
        get { return dob; }
        set { dob = value; }
    }
}

//
// Main.
//
public class Program
{
    public static void Main(string[] args)
    {
        string s = "Server=.;Database=test;Uid=sa;Pwd=";

        // We use a SqlConnection, but any IDbConnection should do the trick
        // as long as you are using the correct Dao implementation to
        // generate SQL statements.
        SqlConnection cn = new SqlConnection(s);

        // Here is the Data Access Object.
        Dao dao = new SqlServerDao(cn);

        // This would also work:
        // Dao dao = new SqlServerDao();
        // dao.Connection = cn;

        try
        {
            // The connection must be opened before using the Dao object.
            cn.Open();

            Mother mother = new Mother(0, "Jane", DateTime.Today);
            int x = dao.Insert(mother);
            Console.WriteLine("Records affected: " + x.ToString());
            Console.WriteLine("Mother ID: " + mother.Id.ToString());

            Father father = new Father(0, "John", DateTime.Today);
            x = dao.Insert(father);
            Console.WriteLine("Father ID: " + father.Id.ToString());

            // We can also force father to be treated as 
            // another type by the Dao.
            // This is not limited to Insert, but the object and type 
            // MUST be compatible.
            dao.Insert<IPerson>(father);

            // This will also work.
            dao.Insert(typeof(IPerson), father);

            // We now have 3 fathers. Let's get rid of the last one.
            // The 'father' variable has the last Father inserted because
            // its Id was set to the last inserted identity.
            x = dao.Delete(father);

            // Now we have 2 fathers. Let's get them from the database.
            IList<Father> fathers = dao.Select<Father>();
            Console.WriteLine(fathers.Count);

            // NOTICE: Dao.Select and Dao.Find methods instantiate objects
            // internally so you cannot use an interface type
            // as the type of objects to return. In other words,
            // the runtime must be able to create instance of given type
            // using reflection (Activator.CreateInstance method).
            // The safest approach you can take is to make
            // sure that every entity type has a default constructor
            // (it could be private).

            Son son;
            son.Name = "Jimmy";
            son.Dob = DateTime.Today;
            dao.Insert(son);

            // Daughter is a struct, so it cannot be null. 
            // If record with given id is not found and the type is a struct,
            // then an empty struct of given type is returned.
            // This, obviously, only works for the generic version
            // of the Find method. The other version returns an object,
            // so null will be returned.
            // The following is usually not a good idea,
            // but they are compatible by table definitions.
            Daughter daughter = dao.Find<Daughter>(son.Id);
            Console.WriteLine(daughter.Name); // should print "Jimmy"

            daughter.Name = "Mary";
            dao.Update(daughter);

            // Refresh the son.
            // Generics not used, so the return type is object, 
            // could be null if not found.
            object obj = dao.Find(typeof(Son), son.Id);
            if(obj != null)
            {
                son = (Son) obj;
                Console.WriteLine(son.Name); // should print "Mary"
            }
        }
        catch(LightException e)
        {
            Console.WriteLine(e.Message);
        }
        catch(Exception e)
        {
            Console.WriteLine(e.Message);
        }
        finally
        {
            dao.Dispose();

            try { cn.Close(); }
            catch {}
        }
    }
}

我认为将表定义委托给另一个类型非常简单。您只需将 TableRefAttribute 应用于一个类型。此功能旨在能够使用类似策略模式的模式。您可以定义一个接口或抽象类,其中包含所有必需的数据元素。您还可以让实现类将表定义委托给此接口或抽象类,但使它们的业务逻辑在方法上有所不同。以下代码展示了 MappingAttribute 的用法,该属性应该有助于继承。假设我们使用相同的连接,并且数据库中存在相同的 dbo.person 表。

using System;
using System.Data;
using Light;

public class AbstractPerson
{
    protected int personId;

    public int PersonId
    {
        get { return personId; }
        set { personId = value; }
    }

    public abstract string Name { get; set; }
    public abstract DateTime Dob { get; set; }

    public abstract void Work();
}

//
// Maps the inherited property "PersonId".
//
[Table("person", "dbo")]
[Mapping("PersonId", "id", DbType.Int32, 
         PrimaryKey=true, AutoIncrement=true)]
public class Father : AbstractPerson
{
    private string name;
    private DateTime dob;

    public Father() {}

    [Column("name", DbType.AnsiString, 30)]
    public override string Name
    {
        get { return name; }
        set { name = value; }
    }

    [Column("dob", DbType.DateTime)]
    public override DateTime Dob
    {
        get { return dob; }
        set { dob = value; }
    }

    public override void Work()
    {
        // whatever he does at work...
    }
}

//
// Maps the inherited protected field "personId".
//
[Table("person", "dbo")]
[Mapping("personId", "id", DbType.Int32, 
         PrimaryKey=true, AutoIncrement=true)]
public class Mother : AbstractPerson
{
    [Column("name", DbType.AnsiString, 30)]
    private string name;
    [Column("dob", DbType.DateTime)]
    private DateTime dob;

    public Mother() {}

    public override string Name
    {
        get { return name; }
        set { name = value; }
    }

    public override DateTime Dob
    {
        get { return dob; }
        set { dob = value; }
    }

    public override void Work()
    {
        // whatever she does at work...
    }
}

MappingAttribute 允许您将继承的成员映射到列。它不必是继承的成员;您还可以使用同一类型中定义的变量和属性。但是,我喜欢在实际信息旁边看到元信息,也就是说,应用于类成员的属性。这使得在更改类成员(例如数据类型)时更容易更改属性。

请注意,Father 使用继承的 **属性**,而 Mother 使用继承的 **字段**。另请注意 MappingAttribute 中成员名称参数的大小写。Father 以大写字母开头 string PersonId,这提示 Light 首先搜索属性。如果找不到名称匹配的属性,则搜索字段。如果找不到名称匹配的字段,则会引发异常。类似地,Mother 有一个以小写字母开头的 personId,因此首先搜索字段。我认为成员搜索的顺序并没有提供多少信息,也没有带来巨大的性能提升,但我一直想实现一些能够“接受提示”并实际使用它的东西。

查询

Light 提供了一种查询数据库的方法。如果您不希望 Light 加载给定类型的全部对象然后自行过滤,这会很有用。我认为您永远不想这样做。Light.Query 对象允许您指定自定义 WHERE 子句,以便操作仅针对记录的子集执行。此对象可与 Dao.SelectDao.Delete 方法一起使用。与 Dao.Delete 方法一起使用时,Light.Query 对象的 WHERE 子句将用于限制将被删除的记录。

概念与在 SQL DELETE 语句中使用 WHERE 子句相同。使用 Light.Query 对象和 Dao.Select 方法允许您指定将作为给定类型对象返回的记录。此外,Dao.Select 方法还会考虑 ORDER BY 子句(Dao.Delete 方法会忽略它),该子句也可以在 Light.Query 对象中指定。同样,概念与在 SQL SELECT 语句中使用 WHEREORDER BY 相同。

Light.Query 对象是一个非常简单的对象,它不解析您提供的 WHEREORDER BY 语句。这意味着两件事。首先,您必须使用数据库中定义的表列的实际名称。您不能使用类属性的名称来查询数据库。其次,您必须为 WHEREORDER BY 子句指定有效的 SQL 语句。如果您使用普通(非参数化)WHERE 子句,那么您也有责任保护自己免受 SQL 注入攻击。我认为在使用参数化语句时这不是问题。

参数化 SQL 语句是查询数据库的推荐方法。它允许数据库缓存执行计划以供以后重用。这意味着数据库不必每次执行 SQL 语句时都解析它们,这肯定有助于提高性能。Light.Query 对象允许您创建参数化 WHERE 子句。要实现这一点,只需使用编写存储过程时使用的参数语法,然后按名称或顺序设置这些参数的值。下面的示例应能清楚地说明这一点(代码未经测试)。

//
// We will use the Son struct defined previously in the article.
// Assume we have a number of records in the dbo.person 
// table to which Son maps.
//

using System;
using System.Data;
using System.Data.SqlClient;
using System.Collections.Generic;
using Light;

IDbConnection cn = new SqlConnection(connectionString);
Dao dao = new SqlServerDao(cn);
cn.Open();

// This will return all Sons born in the last year 
// sorted from youngest to oldest.
// We will use the chaining abilities of the Query and Parameter objects.
IList<Son> bornLastYear = dao.Select<Son>(
    new Query("dob BETWEEN @a AND @b", "dob DESC")
        .Add(
            new Parameter()
                .SetName("@a")
                .SetDbType(DbType.DateTime)
                .SetValue(DateTime.Today.AddYear(-1)),
            new Parameter()
                .SetName("@b")
                .SetDbType(DbType.DateTime)
                .SetValue(DateTime.Today)
        )
    );

// This will return all Sons named John - non-parameterized version.
IList<Son> johnsNoParam = dao.Select<Son>(new Query("name='John'"));

// This will do the same thing, but using parameters.
IList<Son> johnsParam = dao.Select<Son>(
    new Query("name=@name", "dob ASC").Add(
        new Parameter("@name", DbType.AnsiString, 30, "John")
    ));

// This will return all Sons whose name starts with letter J.
Query query = new Query("name like @name").Add(
        new Parameter("@name", DbType.AnsiString, 30, "J%")
    );
IList<Son> startsWithJ = dao.Select<Son>(query);

//
// We can use the same, previously defined, queries to delete records.
//

// This will delete all Sons whose name starts with letter J.
int affectedRecords = dao.Delete<Son>(query);

dao.Dispose();
cn.Close();
cn.Dispose();

创建 QueryParameter 对象(在第一个查询中)可能看起来有些笨拙。QueryParameter 类都遵循 Builder 模式,允许进行此类代码。实现 Builder 模式的类包含在执行所需操作后返回对调用方法的对象的引用的方法。这允许您在同一对象上链接方法调用。QueryParameter 类还具有可以以已知方式设置的常规属性。两种方法都可以。我只是觉得使用这些类以及这些方法会更容易,而且代码会更紧凑。

默认表和列名

您可以在 TableAttribute 中省略表名,在 ColumnAttribute 中省略列名。Light 将根据属性应用的类名和字段名提供表的默认名称和列的默认名称。确定默认名称的规则非常简单。事实上,没有规则。如果属性中未提供类名或字段名,则将原样使用该名称。最好看一个例子

[Table] //same as [Table("Person")]
//[Table(Schema="dbo")] if you need to specify a schema.
public class Person
{
    [Column(DbType.Int32, PrimaryKey=true, AutoIncrement=true)]
    // same as [Column("personId", DbType.Int32, 
    //          PrimaryKey=true, AutoIncrement=true)]
    private int personId;
    
    private string myName;
    
    [Column(DbType.AnsiString, 30)]
    // same as [Column("Name", DbType.AnsiString, 30)]
    public string Name
    {
        get { return myName; }
        set { myName = value; }
    }
}

触发器

触发器的概念来自数据库。数据库触发器是在触发器定义的表上发生特定操作时执行的一段代码。Light 以类似的方式使用触发器。触发器是使用 Light.TriggerAttribute 标记的类,具有 void 返回类型,并接受类型为 Light.TriggerContext 的单个参数。TriggerAttribute 允许您指定 Dao 对象何时调用该方法。同一方法可以标记为被调用一次以上。为此,只需对传递给 TriggerAttributeLight.Action 使用按位 OR 运算符。

触发器方法可以在插入、更新和删除操作之前和/或之后被调用。但是,它只能在 select 操作之后被调用(由 Actions.AfterConstruct 表示),因为在 select 操作之前,根本没有对象可以调用触发器:它们是在 Dao.SelectDao.Find 方法中创建的。

所以,关键在于触发器仅在现有对象上调用。因此,还有另一个注意事项。在调用 Dao.Delete 并为其传递 Query 对象时,不会调用表示要删除的记录的对象上的任何触发器,因为 Light 没有要操作的对象。在内部,Light 不会实例化一个实例来调用其触发器。如果需要这种行为,您应该首先 Dao.Select 要删除的对象,然后将它们传递给 Dao.Delete 方法。

以下是一些演示触发器用法的代码。此代码未经编译或运行测试。假设我们的 SQL Server 数据库中有以下表

create table parent (
    parentid int not null identity(1,1) primary key,
    name varchar(20)
)
go

create table child (
    childid int not null identity(1,1) primary key,
    parentid int not null foreign key references parent (parentid),
    name varchar(20)
)
go

以下是定义此虚父/子关系的 C# 类

using System;
using System.Data;
using System.Collections.Generic;
using Light;

//
// Here is our parent.
//
[Table("parent")]
public class Parent
{
    [Column("parentid", DbType.Int32, PrimaryKey=true, AutoIncrement=true)]
    private int id = 0;
    [Column("name", DbType.AnsiString, 20)]
    private string name;
    
    private IList<Child> children = new List<Child>();
    
    public Parent()
    {}
    
    // No setter as it will be assigned by the database.
    public int ParentId
    {
        get { return id; }
    }
    
    public string ParentName
    {
        get { return name; }
        set { name = value; }
    }
    
    public int ChildCount
    {
        get { return children.Count; }
    }
    
    public Child GetChild(int index)
    {
        if(index > 0 && index < children.Count)
            return children[index];
        return null;
    }
    
    public void AddChild(Child child)
    {
        child.Parent = this;
        children.Add(child);
    }
    
    public void RemoveChild(Child child)
    {
        if(children.Contains(child))
        {
            child.Parent = null;
            children.Remove(child);
        }
    }
    
    //
    // Triggers
    //
    [Trigger(Actions.BeforeInsert | Actions.BeforeUpdate)]
    private void BeforeInsUpd(TriggerContext context)
    {
        // We can do validation here!!!
        // Let's say that the name cannot be empty.
        if(string.IsNullOrEmpty(name))
        {
            // This will cause Dao to throw an exception
            // and will abort the current transaction.
            context.Fail("Parent's name cannot be empty.");
        }
    }
    
    [Trigger(Actions.AfterInsert | Actions.AfterUpdate)]
    private void AfterInsUpd(TriggerContext context)
    {
        // Let's save all the children. The database is ready
        // for it because now this parent's id is in there
        // and referential integrity will not break.
        Dao dao = context.Dao;
        if(context.TriggeringAction == Actions.AfterUpdate)
        {
            // There may have been children already saved
            // so we need to delete them first.
            dao.Delete<Child>(new Query("parentid=@id").Add(
                new Parameter().SetName("@id").SetDbType(DbType.Int32)
                    .SetValue(this.id)
                ));
            
        }
        // And now we can insert the children.
        dao.Insert<Child>(children);
    }
    
    [Trigger(Actions.AfterActivate)]
    private void AfterActivate(TriggerContext context)
    {
        // Let's load all the children.
        Dao dao = context.Dao;
        children = dao.Select<Child>(new Query("parentid=@id").Add(
            new Paremter().SetName("@id").SetDbType(DbType.Int32)
                .SetValue(this.id)
            ));
        
        foreach(Child child in children)
            child.Parent = this;
    }
}

[Table("child")]
public class Child
{
    [Column("childid", DbType.Int32, PrimaryKey=true, AutoIncrement=true)]
    private int id = 0;
    [Column("name", DbType.AnsiString, 20)]
    private string name;
    
    private Parent parent;
    
    public Child()
    {}
    
    public int ChildId
    {
        get { return id; }
    }
    
    public string Name
    {
        get { return name; }
        set { name = value; }
    }
    
    public Parent Parent
    {
        get { return parent; }
        set { parent = value; }
    }
    
    [Column("parentid", DbType.Int32)]
    private int ParentId
    {
        get
        {
            if(parent != null)
                return parent.Id;
            return 0;
        }
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        SqlConnection cn = new SqlConnection("Server=.; Database=test; Uid=sa; Pwd=");
        Dao dao = new SqlServerDao(cn);
        cn.Open();
        
        // Set up parent/child relationships.
        Parent jack = new Parent();
        jack.Name = "Parent Jack";
        
        Child bob = new Child();
        bob.Name = "Child Bob";
        
        Child mary = new Child();
        mary.Name = "Child Mary";
        
        jack.AddChild(bob);
        jack.AddChild(mary);
        
        // When we save the parent, its children will also be saved.
        dao.Insert<Parent>(jack);
        
        // This id was assigned by the database.
        int jacksId = jack.Id;
        
        // Let's now pull jack from the database.
        Parent jack2 = dao.Find<Parent>(jacksId);
        
        // All Jack's children should be loaded by now.
        Console.WriteLine("Jack's children are:");
        for(int i = 0; i < jack2.ChildCount; i++)
            Console.WriteLine(jack2.GetChild(i).Name);
        
        dao.Dispose();
        cn.Close();
    }
}

请小心,不要创建会循环加载对象的触发器。例如,假设我们在 Child 类中添加了一个触发器,该触发器会在 AfterActivate 时加载其父对象。此触发器会加载父对象,父对象会开始加载子对象,子对象又会再次开始加载父对象,依此类推,直到内存耗尽,程序崩溃。

因此,在一对多关系或一个对象完全依赖于另一个对象的情况下,触发器非常有帮助。然而,它们很少能够处理多对多关系,除非您的代码足够严谨,只允许从一侧访问相关对象。当然,触发器并不能解决所有相关对象的问题,但在某些情况下,它们可能会有所帮助。

存储过程

Light 允许您调用存储过程来选择对象。这对于调用基于多个表的搜索过程很有用。或者,您可以创建一个视图来处理这个问题,但在大多数情况下,处理存储过程更容易。然而,一个更好的用途是绕过数据库中定义的多对多关系中的中间表。一个例子应该能清楚地说明这一点。

示例:SQL

create table users (
    userid int identity(1,1) primary key,
    username varchar(30)
)
go

create table roles (
    roleid int identity(1,1) primary key,
    rolename varchar(30)
)
go

-- intermediate table: defines many-to-many relationship
create table userrole (
    userid int foreign key references users(userid),
    roleid int foreign key references roles(roleid),
    constraint pk_userrole primary key(userid, roleid)
)
go

create procedure getroles(@userid int) as
begin
    select roles.*
    from roles join userrole on roles.roleid = userrole.roleid
    where userrole.userid = @userid
end
go

示例:C#

using System;
using System.Data;
using System.Data.SqlClient;
using System.Collection.Generic;
using Light;

[Table("roles")]
public class Role
{
    [Column(DbType.Int32, PrimaryKey=true, AutoIncrement=true)] private int roleid;
    [Column(DbType.AnsiString, 30)] private string rolename;
    
    public int Id {
        get { return roleid; }
        set { roleid = value; }
    }
    
    public string Name {
        get { return rolename; }
        set { rolename = value; }
    }
}

[Table("users")]
public class User
{
    [Column(DbType.Int32, PrimaryKey=true, AutoIncrement=true)] private int userid;
    [Column(DbType.AnsiString, 30)] private string username;
    
    private IList<Role> roles = new List<Role>();
    
    public int Id {
        get { return userid; }
        set { userid = value; }
    }
    
    public string Name {
        get { return username; }
        set { username = value; }
    }
    
    public IList<Role> Roles {
        get { return roles; }
    }
    
    [Trigger(Actions.AfterConstruct)]
    private void T1(TriggerContext ctx)
    {
        // Notice that we are not using the UserRole objects
        // here to pull the list of Role objects.
        
        Dao dao = ctx.Dao;
        roles = dao.Call<Role>(
            new Procedure("getroles").Add(
                new Parameter("@userid", DbType.Int32, this.userid)
            )
        );
    }
}

[Table]
public class UserRole
{
    [Column(DbType.Int32, PrimaryKey=true)] private int userid;
    [Column(DbType.Int32, PrimaryKey=true)] private int roleid;
    
    public int UserId {
        get { return userid; }
        set { userid = value; }
    }
    
    public int RoleId {
        get { return roleid; }
        set { roleid = value; }
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        SqlConnection cn = new SqlConnection("Server=.; Database=test; Uid=sa; Pwd=");
        cn.Open();
        Dao dao = new SqlServerDao(cn);
        
        // add new user
        User user1 = new User();
        user1.Name = "john";
        dao.Insert(user1);
        
        // add some roles
        for(int i = 0; i < 3; i++)
        {
            // create role
            Role role = new Role();
            role.Name = "role " + (i+1).ToString();
            dao.Insert(role);
            
            // associate with user1
            UserRole userrole = new UserRole();
            userrole.UserId = user1.Id;
            userrole.RoleId = role.Id;
            dao.Insert(userrole);
        }
        
        // let's select the only user from the database
        // it should have all roles in its Roles property
        User user2 = dao.Find<User>(user1.Id);
        
        Console.WriteLine("Roles of " + user2.Name + ":");
        foreach(Role role in user2.Roles)
        {
            Console.WriteLine(role.Name);
        }
        
        dao.Dispose();
        cn.Close();
    }
}

性能

Light 是 ADO.NET 的一个包装器,因此按定义比 ADO.NET 慢。此外,Light 使用反射来生成表模型并创建要从 Dao.SelectDao.Find 方法返回的对象。这也比使用 new 运算符创建对象慢。然而,Light 确实试图弥补这些性能下降。

Light 只生成参数化 SQL 语句。运行的每个命令都会在数据库中进行准备(在执行命令之前调用 IDbCommand.Prepare)。这会强制数据库为命令生成执行计划并缓存它。后来对相同类型的命令(INSERTSELECT 等)与相同类型的对象进行的调用应该能够重用数据库中先前创建的执行计划,除非数据库已将其从缓存中移除。

Light 具有生成表模型的缓存机制,因此不必每次都使用反射搜索给定对象的类型。默认情况下,它存储最多 50 个表模型,但这个数字是可配置的(请参阅 Dao.CacheSize)。当缓存已满时,Light 使用最近最少使用算法来选择要从缓存中逐出的表模型。

结论

提供的演示项目实际上并不是一个演示项目。它只是我针对 SQL Server 2005 数据库运行的一系列 NUnit 测试。因此,如果您想运行演示项目,您需要引用(或重新引用)您系统上的 NUnit DLL。此外,您还需要编译源代码并从演示项目中引用它。下载文件中不提供二进制文件,只有源代码。您不需要 Visual Studio 即可使用这些项目;您可以使用免费的 SharpDevelop IDE(用于开发 Light)或经典的命令行。

还包括 Jordan Marr 的一个扩展项目。他的代码增加了对并发的支持,并引入了一个有用的业务框架结构。它跟踪已更改的对象属性,并且仅在对象发生更改时才更新对象。这减少了数据库的负载。业务框架还允许您向对象添加验证规则。

代码已完全注释,因此您可能会在那里找到更多有用的信息。我希望这对某人有所帮助,现在或将来……

致谢

非常感谢 Jordan Marr 的贡献、反馈、想法和扩展项目。

历史

  • 2007-10-18:本文首次提交(代码版本 2.0.0.0)。
  • 2007-11-01:添加了“查询”文章部分;新版本代码(版本 2.1.0.0)。
  • 错误修复和改进

    • 修复了 private 属性的错误;现在,将 private 属性映射到列可以按预期工作。
    • 现在,从数据读取器中提取值使用整数索引而不是列名。
  • 2007-12-16:修改了文章以反映 API 更改(代码版本 2.5.0.0),并添加了“触发器”文章部分。
  • 2008-02-05:代码版本 2.5.3.0。
    • 小的错误修复。
    • 添加了对存储过程的支持。
    • 添加了自动确定表名和列名(如果属性中省略了它们)的功能。
    • 修改了文章以反映 API 更改。
    • 添加了默认名称和存储过程部分。
  • 2010-10-07:更新了指向弃用说明部分中新文章的 URL。
© . All rights reserved.