NHibernate初学者指南(8):增删查改
在开始之前有必要说一下会话(session)和事务(transaction)。
session和transaction是什么
session和transaction是NHibernate提供的最重要的两个对象。通过session对象,可以与数据库进行通信以及执行各种操作。transaction对象为我们提供了一个工具,允许以一个单元管理多个操作。
Session
Nhibernate session可以看成是通往数据库的抽象管道。现在,必须创建一个ADOConnection,打开Connection,传递Connection给Command对象,从Command对象创建DataReader的日子一去不复返了。使用NHibernate,只需向sessionFactory请求一个Session对象,就这么简单。通过session对象,可以往数据库里添加数据,更新以及删除数据库中已有的数据,也可以读取数据库中已有的数据。所有的这些操作都可以使用面向对象的方式执行,不必处理SQL字符串和特定数据库的复杂性。session对象允许我们与数据库中的数据通信,而不用管是SQL Server数据库、MySql数据库或者是Oracle数据库等等。NHibernate完全为我们抽象了这些细节。
Transaction
transaction允许我们以一个工作单元执行很多的任务,结果是要么所有的操作都执行成功,要么即使一个任务执行失败,所有的任务都会返回到原来状态。这种工作方式,我们也称之为“原子操作”。
transaction有四个特点:原子性、一致性、隔离性、持久性。我们也使用首字母ACID描述这些特点。事务操作必须满足ACID。
使用session工厂创建session
NHibernate使用一个工厂对象创建session实例。这个工厂对象称为“对话工厂(session factory)”。一个session工厂可以根据需要创建任意多的session对象。session对象的创建是非常廉价的操作。
另一方面,session工厂的创建非常昂贵。根据系统的复杂性,它可以花费相当长的时间创建一个session工厂实例。这就是为什么我们在一个程序整个生命周期内只创建一个session工厂实例的原因。
一个session工厂是特定于数据库的。如果我们的程序只需要与一个单独的数据库通信,那么只需要一个session工厂。另一方面,如果我们的程序需要几个不同的数据库,那么每个数据库都需要一个session工厂。
session工厂是线程安全的(thread-safe)。运行在不同线程上的代码可以使用同一个session工厂创建session对象。相比之下,session对象只能用在单个线程中。换句话说就是:session对象不是线程安全的。
创建NHibernate session非常简单,一旦有了sessionFactory对象,就可以调用OpenSession方法创建它:
var session = sessionFactory.OpenSession();
通常,不管操作的结果如何,我们都想在using语句中打开session以保证session被关闭和释放。我们还想启动一个事务来对操作的结果做更好的预测。
using (var session = sessionFactory.OpenSession()) { using (var transaction = session.BeginTransaction()) { // create, update, delete, or read data transaction.Commit(); } }
添加数据
创建一个新的实体,可以调用session对象的Save方法持久化到数据库:
var newProductId = (int)session.Save(newProduct);
注意Save方法返回新生成记录的ID。因为有不同的策略生成ID(int,long或GUID),所以返回类型为object类型,我们必须转换结果到预期的类型。我们还可以访问刚刚持久化的实体的ID属性获得新生成ID的值来代替使用Save方法的返回值并转换到正确的类型。
var newProductId = newProduct.Id;
作为Save方法的替代方法,我们也可以使用session对象的Persist方法。Persist方法没有返回值,我们可以直接使用实体的ID属性获取新生成实体ID的值。这种情况下没有类型转换。
查询数据
根据指定的主键值从数据库中读取单条记录,可以使用session对象的Get或者Load方法。下面的代码从数据库中读取ID为1的Product实体:
var product = session.Get<Product>(1);
使用Get或Load,我们必须知道要获取实体的ID。
如果想获取给定类型的实体列表,可以使用session对象的Query方法。这个方法是LINQ to NHibernate驱动程序的一部分。
获取所有的类别列表,可以使用下面的代码:
var allCategories = session.Query<Category>().ToList();
注意语句后边调用ToList()方法。LINQ to NHibernate返回IQuery<Category>类型的列表,,它是延迟加载,如果想NHibernate提前加载所有的记录,就要通过调用ToList()强制完成。
Get和Load
如果延迟加载启动的话,Get和Load有很大的区别。
如果想加载Product实体,可以使用下面的代码:
var product = session.Get<Product>(…?);
Get方法需要加载的实体的主键值作为参数,所以加载ID为1的Product,可以使用下面的代码:
var product = session.Get<Product>(1);
如果请求的实体在数据库中存在,Get方法会物理的从数据库中检索它。如果给定ID的实体在数据库中不存在,那么返回NULL。
如果使用Load方法,NHibernate不会从数据库检索实体,而是创建一个代理对象。这个代理实体唯一填充数据的属性是ID,如下面的代码所示:
var product = session.Load<Product>(1);
通过上面的代码,获得了一个ID为1的代理Product实体。此时,我们的代码会访问除ID以外的任何属性,NHibernate从数据库加载实体。实际上,这就是我们所谓的“延迟加载”。
那么什么情况下使用Load方法呢?需要访问和操作实体时,我们使用session对象的Get方法,那么不需要真的修改和访问实体的详细信息时使用Load方法。我们看个简单的例子。假设我们想更新一个存在Product实体改变它的类别。一件产品属于一个类别,因此,Product实体有一个Category类型的属性。然而,当操作products时,我们不想同时修改类别,只是使用它们。下面的代码能够实现:
var product = session.Get<Product>(productId); product.Category = session.Load<Category>(newCategoryId);
NHibernate会生成一个SELECT语句加载一个product,但是不会加载category。它只是创建一个代理实体,分配一个新的类别给产品。
更新数据
因为NHibernate session对象跟踪对加载的对象任何修改,所以不用显示的调用update或其他方法持久化修改到数据库。此时session对象被刷新,修改会由NHibernate自动持久化。下面的代码可以达到目的:
using (var session = sessionFactory.OpenSession()) using (var tx = session.BeginTransaction()) { var product = session.Get<Product>(1); product.UnitPrice = 10.55m; // new unit price tx.Commit(); }
删除数据
删除数据库中存在的实体,必须首先加载它,然后将它传给session对象的Delete方法,如下面的代码所示:
var productToDelete = session.Load<Product>(productId); session.Delete(productToDelete);
注意为了避免不必要的数据库往返,可以使用Load方法来代替Get方法。当刷新session时,上面代码结果就是一个简单的删除SQL语句送到数据库。但是这并适用于要删除的实体依赖其他实体或子实体的集合。这种情况下,必须加载实体到内存中。
实战时间
在所有理论后,让我们实现一个例子。在这个例子中,我们定义一个简单的模型,并使用自动映射来映射这个模型。然后我们创建一个session工厂,用它来创建session对象。
1. 在SMSS中新建一个空的数据库:NHibernateSessionSample。
2. 打开VS,创建一个Console Application项目:NHibernateSessionSample。
3. 为项目添加NHibernate,NHibernate.ByteCode.Castle和FluentNHibernate程序的引用。
4. 在项目中添加一个Domain文件夹,在该文件夹中添加一个Order类。
public class Order { public virtual int Id { get; set; } public virtual Customer Customer { get; set; } public virtual DateTime OrderDate { get; set; } public virtual IList<LineItem> LineItems { get; set; } }
5. 在Order类中添加一个默认构造函数,初始化LineItems集合,如下面的代码所示:
public Order() { LineItems = new List<LineItem>(); }
6. 在Order类中添加一个虚拟方法用来添加line item,如下面的代码所示:
public virtual void AddLineItem(int quantity, string productCode) { var item = new LineItem { Order = this, Quantity = quantity, ProductCode = productCode }; LineItems.Add(item); }
7. 在Domain文件夹中分别添加LineItem类和Customer类,如下所示:
public class LineItem { public virtual int Id { get; set; } public virtual Order Order { get; set; } public virtual int Quantity { get; set; } public virtual string ProductCode { get; set; } }
public class Customer { public virtual int Id { get; set; } public virtual string CustomerName { get; set; } }
8. 在项目添加一个类文件:OrderingSystemConfiguration。添加下面的代码定义映射哪些类:
namespace NHibernateSessionSample { public class OrderingSystemConfiguration : DefaultAutomappingConfiguration { public override bool ShouldMap(Type type) { return type.Namespace == typeof(Customer).Namespace; } } }
9. 在Program类中为session工厂定义一个静态变量,如下所示:
private static ISessionFactory sessionFactory;
10. 在Program类中添加一个静态方法ConfigureSystem。这个方法配置NHibernate使用SQL Server作为数据库,以及使用自动映射来映射模型和自动创建数据库架构:
private static void ConfigureSystem() { const string connString = "server=.;database=NHibernateSessionSample;" + "integrated security=SSPI;"; var cfg = new OrderingSystemConfiguration(); var model = AutoMap.AssemblyOf<Customer>(cfg); var configuration = Fluently.Configure() .Database(MsSqlConfiguration.MsSql2008 .ConnectionString(connString) .ShowSql ) .Mappings(m => m.AutoMappings.Add(model)) .BuildConfiguration(); var exporter = new SchemaExport(configuration); exporter.Execute(true, true, false); sessionFactory = configuration.BuildSessionFactory(); }
注意上面代码中.ShowSql的调用。它配置NHibernate将发送到数据库的所有SQL语句输出到控制台
11. 在Program类中添加一个静态方法CreateCustomers。这个方法创建两个customer对象,并使用session对象将它们存储到数据库中,如下面的代码所示:
private static void CreateCustomers() { var customerA = new Customer { CustomerName = "Microsoft" }; var customerB = new Customer { CustomerName = "Apple Computer" }; using (var session = sessionFactory.OpenSession()) using (var transaction = session.BeginTransaction()) { session.Save(customerA); session.Save(customerB); transaction.Commit(); } }
12. 在Main方法中,添加下面的代码:
static void Main(string[] args) { ConfigureSystem(); CreateCustomers(); Console.Write("\r\nHit enter to exit:"); Console.ReadLine(); }
13. 运行程序,结果如下图所示:
SQL命令的前几行显示了如果架构已经存在就将它们移除。通过执行上面的命令,在数据库中自动完成了数据库的架构,并在Customer表中插入了两条数据。
下面,我们创建一个订单。
14. 在Program类中添加一个静态方法CreateOrder,代码如下所示:
private static int CreateOrder() { var customer = new Customer { CustomerName = "Intel" }; var order = new Order { Customer = customer, OrderDate = DateTime.Now, }; order.AddLineItem(1, "Apple"); order.AddLineItem(5, "Pear"); order.AddLineItem(3, "Banana"); int orderId; using (var session = sessionFactory.OpenSession()) using (var transaction = session.BeginTransaction()) { session.Save(customer); orderId = (int)session.Save(order); transaction.Commit(); } return orderId; }
15. Main方法中,在CreateCustomers()之后,添加CreateOrder();如下面的代码所示:
static void Main(string[] args) { ConfigureSystem(); CreateCustomers(); //添加在这里 var orderId = CreateOrder(); Console.Write("\r\nHit enter to exit:"); Console.ReadLine(); }
16. 如果这时运行程序的话会出现异常,因为默认情况下,auto-mapper配置HasMany关系为none-inverse和不级联的。为了配置我们想要的映射,在ConfigureSystem方法中添加如下代码(在 var model = AutoMap.AssemblyOf<Customer>(cfg);之后):
model.Override<Order>(map => map.HasMany(x => x.LineItems).Inverse().Cascade.AllDeleteOrphan());
17. 这时再运行程序,一切OK,如下图所示:
注意,3个order的line items自动地插入到了数据库中。
下面加载一个订单,删除它的一个line item,并添加一个新的:
18. 在Program类中添加一个UpdateOrder静态类,代码如下:
private static void UpdateOrder(int orderId) { using (var session = sessionFactory.OpenSession()) using (var transaction = session.BeginTransaction()) { var order = session.Get<Order>(orderId); order.LineItems.RemoveAt(0); order.AddLineItem(2, "Apricot"); transaction.Commit(); } }
19. 在Main方法中调用 UpdateOrder(orderId);
20. 运行程序,如下图所示:
第一个select语句加载order。第二个select语句加载订单的line items。然后,一个insert语句,添加一个新的line item到数据库。最后,一个delete语句,从数据库中移除line item。
最后让我们删除订单。
21. 在Program类中添加一个DeleteOrder静态方法,代码如下:
private static void DeleteOrder(int orderId) { using (var session = sessionFactory.OpenSession()) using (var transaction = session.BeginTransaction()) { var order = session.Load<Order>(orderId); session.Delete(order); transaction.Commit(); } }
22. 在Main方法中调用DeleteOrder(orderId);
23. 运行程序,控制台的输出如下:
第一个select语句加载order,第二个加载order的line item。在订单本身删除之前,它的所有line item先删除。
总结
这篇文章首先介绍了什么是会话和事务,紧接着介绍了使用NHibernate进行增删查改的一些理论知识,最后通过一个控制台的例子,进行了实际的增删查改操作,并通过控制台的输出,可以看到生成的SQL语句。相信,通过这篇文章,你对NHibernate的增删查改有了一定的理解。