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

一个具有ORM功能的小型ADO.NET库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (29投票s)

2010年4月5日

CPOL

6分钟阅读

viewsIcon

79048

downloadIcon

1418

基本的 CRUD 方法以及其他一些有趣的功能

目的

这并非一个成熟的ORM解决方案,也未声称是。其目的是将一个对象映射到一张表/视图,并允许用户执行基本的CRUD(插入更新删除选择)任务。

概述

这个想法很简单。创建一个类来表示数据库表或视图中的一条记录。然后将该类及其成员映射到表中。请注意,类成员必须在类的范围内可见才能被映射。如果父类映射了私有字段,则在处理子类时将不会使用它。

映射

映射可以通过两种方式之一完成:使用属性或实现一个静态方法。这些是源代码中提供的两种方法。通过实现Sqless.Mapping.ITableDefinitionBuilder接口并将该类的一个实例添加到Sqless.Mapping.TableDefinitionFactory.Builders列表中,可以轻松定义其他映射方法。您还可以重新排列此列表中的项,以指定检查类映射的顺序。(注意:访问此列表不是线程安全的。)

要使用属性将类映射到数据库表,该类需要用TableAttribute修饰,用户可以在其中指定以下内容:

  • Name - 映射表的名称
  • Schema - 映射表所属的Schema名称
  • Sequence - 与映射表关联的Sequence对象(这仅对支持Sequence的数据库(如Oracle)有意义)

要将类成员映射到表列,类成员必须用FieldAttribute修饰,用户可以在其中指定以下内容:

  • Name - 数据库列的名称
  • Flags - 字段标志,指定字段的访问方式和时间

FieldFlags enum具有以下值:

  • Read - 允许从数据库列读取字段值并写入.NET对象。
  • Write - 允许从.NET对象读取字段值并保存到数据库列。
  • ReadWrite - 允许字段值在两个方向上传递。等同于FieldFlags.Read | FieldFlags.Write。如果没有指定其他标志,这应该被视为默认值。
  • Key - 指定此字段是表主键的一部分。
  • Auto - 指定此字段的值由数据库自动生成。

指定FieldFlags时,可以使用按位或(bitwise OR)组合标志。

或者,可以在类中实现一个名为DefineTable静态方法。此方法应返回一个Sqless.Mapping.TableDefinition对象,该对象描述包含类的映射。TableDefinition对象实现了Builder模式,因此可以链式调用方法,并且它们提供了一种定义与上述属性相同数据元素的方式。这种方法的优点是避免了发现属性映射所需的System.Reflection调用,因此它应该更快。缺点是需要更多的代码,并且字段映射不会被子类继承。

我们稍后将看到两种映射方法的示例。

API 概述

主要对象是IDatabase对象。它是数据库连接的包装器。它提供了一种处理事务、执行原始SQL语句和访问ITable对象的方式。ITable对象提供了ORM功能,即处理.NET对象和数据库表之间的映射。每个ITable可以创建IQuery对象,这些对象提供了一种针对该表运行带有whereorder by子句的select(和delete)语句的方式。所有与底层数据库的通信都由IStatement对象处理。IStatement是数据库命令对象的包装器。您可以使用IStatement执行原始SQL语句(不是很安全)或参数化SQL语句,参数化由IStatement对象处理。简单的.NET格式化(例如WHERE id = {0})用于指定SQL字符串中的参数占位符。执行使用IStatement的查询会返回IQueryResult对象。IQueryResult提供了方便的方法来迭代结果集。

示例映射

考虑SQL Server中的以下表

create table dbo.People
(
	Id int not null identity,
	Fname varchar(20),
	Lname varchar(20),
	Dob datetime,
	constraint PK_People primary key (Id)
)

创建一个类来表示此表中的记录,并使用属性进行映射

[Table("People", "dbo")]
public class Person
{
	private int? id;
	private string fname;
	private string lname;
	private DateTime? dob;

	[Field("Id", FieldFlags.Read|FieldFlags.Key|FieldFlags.Auto)]
	public int? ID
	{
		get { return id; }
		set { id = value; }
	}

	[Field("Fname")]
	public string FirstName
	{
		get { return fname; }
		set { fname = value; }
	}

	[Field("Lname")]
	public string LastName
	{
		get { return lname; }
		set { lname = value; }
	}

	[Field("Dob")]
	public DateTime? BirthDate
	{
		get { return dob; }
		set { dob = value; }
	}
}

注意ID字段的标志。数据库中的ID字段是标识字段和主键。因此,标志包含FieldFlags.KeyFieldFlags.Auto。标记为Auto的字段将在插入记录对象后填充生成的值。FieldFlags.Read允许此字段只能从数据库读取,但不能写入。这很有用,因为标识字段不应被更新。也可以使任何其他字段(不仅是标识字段)只读或只写。

以下是如何使用静态方法映射同一类

// Now we don't specify any attributes.
public class Person
{
	/* Field declarations */
	/* Property declarations */
	
	public static TableDefinition DefineTable()
	{
		return new TableDefinition("People").Schema("dbo")
			.Field("Id").MapTo("ID").ReadOnly().Key().Auto().Add()
			.Field("Fname").MapTo("FirstName").Add()
			.Field("Lname").MapTo("LastName").Add()
			.Field("Dob").MapTo("BirthDate").Add();
	}
}

对象触发器

触发器是对象方法,在对象参与数据库操作之前或之后被调用。触发器名称定义了它们何时被调用。就像所有其他类成员一样,触发器方法必须在相关类的范围内可见。也就是说,在父类中定义的私有触发器方法在处理子类时不会被调用。触发器方法的签名是EventHandler(object sender, EventArgs args)。第一个参数(sender)是触发触发器的IDatabase对象。第二个是空的EventArgs对象。

以下是所有可能的触发器列表:

// Called before insert operation.
void BeforeInsert(object sender, EventArgs args);

// Called after insert operation.
void AfterInsert(object sender, EventArgs args);

void BeforeUpdate(object sender, EventArgs args);
void AfterUpdate(object sender, EventArgs args);

// Not called if deleting using a query.
void BeforeDelete(object sender, EventArgs args);
void AfterDelete(object sender, EventArgs args);

// No corresponding BeforeSelect.
void AfterSelect(object sender, EventArgs args);

假设我们需要修改Person类,以便如果BirthDate未设置,则默认为DateTime.Today。我们可以向类添加以下方法:

public class Person
{
	/* Declarations are not shown. */
	
	protected void BeforeInsert(object sender, EventArgs args)
	{
		EnsureDob();
	}
	
	protected void BeforeUpdate(object sender, EventArgs args)
	{
		EnsureDob();
	}
	
	private void EnsureDob()
	{
		if (!dob.HasValue)
			dob = DateTime.Today;
	}
}

触发器可以自由加载和保存其他对象。假设我们定义了CustomerPurchase类并正确映射到相应的表。我们还希望每个Customer始终拥有其Purchases列表。我们可以使用AfterSelect触发器来实现这一点。

[Table("Customers")]
public class Customer
{
	private int customerID;
	private IList purchases;
	
	protected void AfterSelect(object sender, EventArgs args)
	{
		purchases = (sender as IDatabase).Table(typeof(Purchase))
			.Query().Eq("CustomerID", this.customerID)
			.OrderBy("PurchaseDate", false)
			.Select();
	}
}

查询对象

IQuery对象允许用户为SelectFindDelete操作指定搜索条件(WHEREORDER BY子句)。方法名称应该很容易理解其功能。一个例子应该使其变得微不足道。IQuery对象实现了构建器模式,因此可以链式调用其方法。

ITable table = db.Table(typeof(Person));

// WHERE Id = 1 and Fname = 'John'
table.Query().Eq("Id", 1).And().Eq("Fname", "John");

// WHERE Fname like 'M%' and Lname = 'Smith'
table.Query().Like("Fname", "M%").And().Eq("Lname", "Smith");

// WHERE (Fname = 'John' and Lname = 'Doe') or (Fname = 'Jane' and Lname = 'Smith')
table.Query().Sub().Eq("Fname", "John").And().Eq("Lname", "Doe").EndSub()
	.Or().Sub().Eq("Fname", "Jane").And().Eq("Lname", "Smith").EndSub();

// WHERE Id IN (1,2,3)
table.Query().In("Id", new int[] { 1,2,3 });

// WHERE Lname = 'Smith' ORDER BY Fname ASC
table.Query().Eq("Lname", "Smith").Order("Fname", true);

// Using a template query object, the following will select all rows
//   WHERE FirstName = 'John' AND LastName = 'Smith'
// All non-null fields are used in the query.
Person p = new Person();
p.FirstName = "John";
p.LastName = "Smith";
IList johnSmiths = database.Table(p.GetType()).Query(p).Select();

执行原始SQL语句

您可以执行任何非查询SQL语句,包括insert、update和delete。当使用IStatement执行SQL查询时,将返回IQueryResult对象。从此,您可以将结果集读入RowSet对象,或提供自己的回调,这些回调将在迭代结果时被调用。

string sql = "select Id, Dob, Fname, Lname from dbo.People";

RowSet rs = database.Prepare(sql).ExecQuery().ToRowSet();
while (rs.Next()) {
	Console.WriteLine("Id = {0}, Dob = {1}, Fname = {2}, Lname = {3}",
		rs.Get(0), rs.Get("Dob"), rs[2], rs["Lname"] );
}

// Get a list of objects
IList people = database.Prepare(sql).ExecQuery()
	.ToList(new ToListCallback(delegate(IRow row) {
		MyObject p = new MyObject();
		p.ID = row[0];
		p.Date = row[1];
		p.Text = row[2] + row[3];
		return p;
	}));

// Output names
database.Prepare(sql).ExecQuery()
	.ForEach(new ForEachCallback(delegate(IRow row) {
		Console.WriteLine( (string)row[2] + " " + (string)row[3] );
	}));

空值和DBNull值

nullDBNull之间的转换是自动处理的。无论何时需要将null值插入数据库字段,都应传递.NET null值或将映射的对象字段设置为null。从数据库中选择的任何DBNull值在分配给对象字段或添加到RowSet对象之前都会转换为.NET null。支持Nullable类型。当nulls传递给EqNe方法时,IQuery对象也能正确处理它们。

跟踪事件

IDatabase对象会触发Trace事件。在对数据库执行任何命令之前,会触发Trace事件。此事件通常用于将生成的SQL语句和参数值写入文件或控制台。更常见的选项是配置应用程序跟踪并写入Trace对象。这对于调试目的很有用。

更多示例

SqlConnection conn = new SqlConnection("my_connection_string");
IDatabase database = new Sqless.SqlServer.SqlDatabase(conn);

// How many Johns do we have
int count = database.Table(typeof(Person))
		.Query().Eq("FirstName", "John")
		.Count();

// Get Person with id = 5
Person p = (Person) database.Table(typeof(Person))
		.Query().Eq("Id", 5).Find();

// Execute stored procedure SelectCustomer
string sql = "exec SelectCustomer {0}";
RowSet rs = database.Prepare(sql).ExecQuery(5).ToRowSet();

// exec InsertCustomer @p0, @p1, @p2
string sql = "exec InsertCustomer {0}, {1}, {2}";
int rowcount = 0;
using (IStatement stmt = database.Prepare(sql))
	for (int i = 0; i < 10; ++i)
		rowcount += stmt.ExecNonQuery(i, "John", "Doe");

历史

  • 2010年4月5日:首次发布
  • 2010年9月19日:更新
  • 2011年11月30日:更新
  • 2011年12月9日:更新
© . All rights reserved.