LINQ学习笔记:LINQ to SQL实体类

2009-04-09 14:28:32  来源:网页教学网 

WebjxCom提示:LINQ to SQL的延迟执行与本地查询在语义上有点不同,那就是当一个子级查询出现在一个Select表达式里面的时候.

LINQ to SQL 实体类

只要使用了适当的属性 (attribute),LINQ to SQL允许你使用任何类去展现数据:

   1: [Table]
   2:  
   3: public class Customer
   4:  
   5: {
   6:  
   7:   [Column(IsPrimaryKey=true)]
   8:  
   9:   public int ID;
  10:  
  11:   [Column]
  12:  
  13:   public string Name;
  14:  
  15: }

[Table]属性存在于System.Data.Linq.Mapping命名空间中,它将会告诉LINQ to SQL此类型的一个对象实例代表数据库表中的一行.默认情况下,它假设表名和类名是一致,如果不一致,我们也可以使用下面的做法来完成映射:

[Table (Name=”Customers”)]

一个被标记了[Table]属性的类称为一个LINQ to SQL实体.它非常接近设置完全匹配数据库当中的表结构.

[Column]属性标记了一个映射到数据库列的字段或者属性. 如果列名和字段名或属性名不同, 我们也可以用以下的做法完成映射:

[Column Name=”FullName”]

public  string Name;

IsPrimaryKey属性指示该列是否是表中的主键列.为了维护对象的一致性以及更新和写回数据库, 它是必须的. 我们还可以定义公开属性去关联私有字段, 而不是直接定义共有字段,这样我们可以在属性访问器里面添加验证逻辑. 如果选择了这种方式, 你可以选择让LINQ to SQL在读取数据库数据的时候跳过属性访问器直接将数据写入字段:

   1: string _name;
   2:  
   3: [Column (Storage=“_name”)]
   4:  
   5: public string Name
   6:  
   7: { get { return _name; } set { _name =value; } }

[Column (Storage=”_name”)]指示LINQ to SQL直接将数据写入_name字段(而不是通过Name属性). 由于LINQ to SQL使用反射因此可以允许字段被定义为私有的.

DataContext

当你定义了实体类之后, 你可以通过实例化DataContext然后调用GetTable开始进行查询操作, 如下所示:

   1: var dc = new DataContext (“cx string…”);
   2:  
   3: Table customers = dc.GetTable ();
   4:  
   5: Customer cust = customers.OrderBy (c =>c.Name).First();
   6:  
   7: cust.Name = “the new name”;
   8:  
   9: dc.SubmitChanges();

一个DataContext对象会对它实例化的所有实体类进行追踪, 在其生命周期内, 一个DataContext永远都不会包含2个指向表中同一行的实体对象.

查看以下示例:

   1: var dc = new DataContext (“connectionString”);
   2:  
   3: Table customers = dc.GetTable ();
   4:  
   5: Customer a = customers.OrderBy (c => c.Name).First();
   6:  
   7: Customer b =customers.OrderBy (c => c.ID).First();

假设a和b返回的都是数据库中的同一条记录,这里就会有很多很有趣的结果产生.首先,当第二条查询执行的时候,它访问了数据库并且获取了一条单笔的记录,然后取得主键并且跟DataContext实例当中的实体Cache进行了一下比较, 结果发现有一条匹配的记录已经存在, 因此查询直接反复了那条已经存在的记录,没有改变任何值.这意味着,如果在第一条查询执行之后有人改变了该记录的某个值,那么第二条查询并不会获得这个最新值.这样设计的目的是为了避免一些不可意料的边界影响以及并发管理.如果你更新了某个实体的属性,但是并没有调用SubmitChanges,这些值将不会被改写.

第二个随之而来的结果是你不能显式的使用一个所有列的子集去填充一个实体类.例如,你可以使用以下的任意一种方法去查询数据并只返回客户名:

   1: customers.Select (c => c.Name);
   2:  
   3: customers.Select (c => new { Name = c.Name } );
   4:  
   5: customers.Select (c => new YourDefinedCustomerType { Name = c.Name } );

而使用以下的做法是行不通的:

   1: customers.Select (c => new Customer {Name = c.Name } );

这是因为Customer实体仅仅是部分属性被赋值,因此如果下次你有另外一个查询要返回所有的列,你得到将会是Cache中那个只有Name属性被赋值的实体.

[在一个多层应用程序中,你不能在中间层中使用一个单一的静态DataContext实例去处理所有的请求,因为DataContext并不是线程安全的.相反的,中间层方法必须根据每一次的客户端请求去创建新的DataContext对象]

自动实体生成

你可以根据已知的数据库schema,通过SqlMeta命令行工具或者VS当中的LINQ to SQL设计器来自动生成对应的LINQ to SQL实体类. 这些工具都会生成partial的实体类, 因此你可以在另外一个分离的文件中添加额外的逻辑.

作为额外的奖励, 你还将得到一个强类型的DataContext类, 它是一个DataContext的子类型并通过属性包装了每一个Table对应的实体类. 通过这个强类型DataContext,你就不再需要调用GetTable了:

   1: var dataContext = new MyTypedDataContext (“connectionString”);
   2:  
   3: Table customers = dataContext.Customers;
   4:  
   5: Console.WriteLine (customers.Count());

或者简单的使用:

   1: Console.WriteLine (dataContext.Customers.Count());

LINQ to SQL设计器会在合适的地方使用复数标识符,在这个例子中,它使用的是dataContext.Customers而不是dataContext.Customer – 虽然SQL Table和实体类名都是Customer.

关联

实体类生成工具还做了一项非常有用的工作,对于每一个在数据库中已经定义了的关系,生成工具都会在两边的实体类中自动生成对应的属性.例如,假设我们定义了一个客户和采购表的一对多的关系:

   1: create table Customer
   2:  
   3: (
   4:  
   5:   ID int not null primary key,
   6:  
   7:   Name varchar(30) not null
   8:  
   9: )
  10:  
  11: create table Purchase
  12:  
  13: (
  14:  
  15:   ID int not null primary key,
  16:  
  17:   CustomerID int references Customer (ID),
  18:  
  19:   Description varchar(30) not null,
  20:  
  21:   Price decimal not null
  22:  
  23: )

如果我们使用了自动生成的实体类的话,我们可以类似下面这样编写查询:

   1: var dataContext = new MyTypedDataContext (“connectionString”);
   2:  
   3: Customer cust1 = dataContext.Customers.OrderBy (c => c.Name).First();
   4:  
   5: foreach (Purchase p in cust1.Purchases)
   6:  
   7:          Console.WriteLine (p.Price);
   8:  
   9: Purchase cheapest = dataContext.Purchases.OrderBy (p => p.Price).First();
  10:  
  11: Customer cust2 = cheapest.Customer;

可以看到我们可以通过customer引用到purchase,同样也可以通过purchase引用到对应的customer. 并且如果cust1和cust2刚好指向的是同一条客户记录, cust1==cust2则会返回true.

现在让我们来查看一下Customer实体上自动生成的Purchases属性的标记:

   1: [Association (Storage=“_Purchases”,
   2:  
   3:              OtherKey=“CustomerID”)]
   4:  
   5: public EntitySet Purchases
   6:  
   7: { get {…} set {…}}

一个EntitySet类似是一个预定义的内置Where语句,用于查询关联的实体.[Association]属性提供了LINQ to SQL编写此查询所需的信息.和其他类型的查询一样,这个查询是延迟执行的,这意味着只有当你开始枚举相关结果集的时候查询才会真正执行.

而在Purchase实体类也有对应的Customer属性, 如下所示:

   1: [Association (Storage=“_Customer”,
   2:  
   3:               ThisKey=“CustomerID”,
   4:  
   5:               IsForeignKey=true)]
   6:  
   7: public Customer Customer { get {…} set{…} }

虽然这个属性的类型是Customer,但其后的对应字段_Customer是EntityRef类型(通过EntityRef.Entity得到Customer实体类),EntityRef实现了延迟执行功能,因此只有当你真正需要Customer数据的时候它才会从数据库当中读取对应的行.

LINQ to SQL的延迟执行

LINQ to SQL的延迟执行与本地查询在语义上有点不同,那就是当一个子级查询出现在一个Select表达式里面的时候

1. 如果是本地查询, 你会有两次的延迟查询. 这意味着如果你只是枚举外层的结果序列, 而没有去枚举内部的序列, 那么内部的子查询永远都不会执行

2. 而如果是LINQ to SQL,子查询将会在外部查询被执行的时候就执行了, 这可以避免额外的往还.

   1: var dataContext = new CustomDataContext (“myConnectionString”);
   2:  
   3: var query = from c in dataContext.Customers
   4:  
   5:             select
   6:  
   7:                from p in c.Purchases
   8:  
   9:                select new{ c.Name, p.Price };
  10:  
  11: foreach (var customerPurchaseResults inquery)
  12:  
  13:   foreach (var namePrice in customerPurchaseResults)
  14:  
  15:     Console.WriteLine (namePrice.Name + ” spent “ + namePrice.Price);

任何EntitySets都会显式的在一次往返中加载所有的数据:

   1: var query = from c in dataContext.Customers
   2:  
   3:             select new { c.Name, c.Purchases };
   4:  
   5: foreach (var row in query)
   6:  
   7:   foreach (Purchase p in row.Purchases)
   8:  
   9:     Console.WriteLine (row.Name + ” spent “ + p.Price);

但如果我们先拥有query变量,而是直接使用下面这样的做法:

   1: foreach (Customer c in dataContext.Customers)
   2:  
   3:   foreach (Purchase p in c.Purchases) // 数据库往返
   4:  
   5:     Console.WriteLine (c.Name + ” spent “ + p.Price);

这种情况延迟加载将会被应用,并在每一个循环中都会和数据库交互.虽然可能引起性能问题, 但当你希望有选择性的执行内部循环的时候,这种做法可以带来你所需要的好处,例如:

   1: foreach (Customer c in dataContext.Customers)
   2:  
   3:   if (myWebService.yourMethod (c.ID)) // 商业逻辑判断
   4:  
   5:     foreach (Purchase p in c.Purchases) // 数据库往返
   6:  
   7:       Console.WriteLine (“output something”);

DataLoadOptions

DataLoadOption有两个主要的用途:

1.允许你预先设置一个EntitySet关联的过滤器

2.允许你立即加载对应的EntitySets以便减少

预先过滤

下面的例子演示如何使用DataLoadOptions的AssociateWith方法:

   1: DataLoadOptions options = new DataLoadOptions();
   2:  
   3: options.AssociateWith
   4:  
   5:   (c => c.Purchases.Where (p => p.Price > 456));
   6:  
   7: dataContext.LoadOptions =options;

代码段指示DataContext实例总是使用给定的断言来过滤customer对应的purchase采购订单.AssociateWith并不会改变延迟执行的语义, 它只是简单的对一个特定的关系应用了一个过滤器而已.

热加载(Eager Loading)

DataLoadOptions的另外一个用途是对特定的EntitySets热加载其对应的parents, 假设你想在一次数据库往返中加载所有的customers及其对应的purchase,示例如下:

   1: DataLoadOptions options = new DataLoadOptions();
   2:  
   3: options.LoadWith (c =>c.Purchases);
   4:  
   5: dataContext.LoadOptions = options;
   6:  
   7: foreach (Customer c in dataContext.Customers)
   8:  
   9:   foreach (Purchase p in c.Purchases)
  10:  
  11:     Console.WriteLine (c.Name + ” bought a “ + p.Description);

该代码段表示当读取Customer的时候,其对应的Purchases也应该在同一时间被加载, 你甚至可以进一步记载PurchaseItem:

   1: options.LoadWith  (c => c.Purchases);
   2:  
   3: options.LoadWith (p =>p.PurchaseItems);

我们也可以将AssociateWith和LoadWith组合在一起使用, 如下所示:

   1: options.LoadWith  (c => c.Purchases);
   2:  
   3: options.AssociateWith (c => c.Purchases.Where (p => p.Price > 456));

这表示当加载Customer的时候同时加载那些Price金额大于456的Purchases.

更新

LINQ to SQL会保持你对实体做的变更痕迹并且允许你调用DataContext.SubmitChanges将它们写回数据库.Table<>类上的InsertOnSubmit和DeleteOnSubmit分别用于新增或者删除表中的一行. 以下示例演示如何使用它们:

   1: var dataContext = new CustomDataContext (“connectionString”);
   2:  
   3:     Customer cust = new Customer { ID=1000, Name=“Bloggs” };
   4:  
   5:     dataContext.Customers.InsertOnSubmit(cust);
   6:  
   7:     dataContext.SubmitChanges();

我们也可以在读取行资料之后,更新然后删除它:

   1: var dataContext = new CustomDataContext (“connectionString”);
   2:  
   3: Customer cust = dataContext.Customers.Single(c => c.ID == 1000);
   4:  
   5: cust.Name = “Bloggs2″;
   6:  
   7: dataContext.SubmitChanges();    // 更新customer
   8:  
   9: dataContext.Customers.DeleteOnSubmit (cust);
  10:  
  11: dataContext.SubmitChanges();    // 删除customer

DataContext.SubmitChanges收集所有自从DataContext创建以来(或者上次调用SubmitChanges以来)的变更,然后执行对应的SQL语句将他们写回数据库.所有的语句都将被放在一个Transaction当中来运行.

你也可以通过调用Add将一个新行加入到一个EntitySet当中,LINQ to SQL会自动设置对应的外键:

   1: var p1 = new Purchase { ID=100, Description=“Bike”,Price=500 };
   2:  
   3: var p2 = new Purchase { ID=101, Description=“Tools”,Price=100 };
   4:  
   5: Customer cust = dataContext.Customers.Single(c => c.ID == 1);
   6:  
   7: cust.Purchases.Add (p1);
   8:  
   9: cust.Purchases.Remove (p2);
  10:  
  11: dataContext.SubmitChanges();

在此例子中, LINQ to SQL自动将1写入到各个新的purchase中的CustomerID列, 原因就是因为我们定义的Purchases属性:

   1: [Association (Storage=“_Purchases”,
   2:  
   3:               OtherKey=“CustomerID”)]
   4:  
   5: public EntitySet Purchases
   6:  
   7: { get {…} set {…}

如果实体类是由Visual Studio的设计器或者SqlMeta命令行工具自动生成的话, Customer和Purchase之间的关联会有相应的代码自动同步维护。换句话说, 赋值到Purchase.Customer属性会自动将一个新的Customer加入到Customer.Purchases这个EntitySet中,反之亦然. 因此我们可以将之前的例子改写为:

   1: var dataContext = new MyTypedDataContext (“connectionString”);
   2:  
   3: Customer cust = dataContext.Customers.Single
   4:  
   5:                 (c => c.ID == 1);
   6:  
   7: new Purchase { ID=100, Description=“Bike”, Price=500,
   8:  
   9:                Customer=cust};
  10:  
  11: new Purchase { ID=101, Description=“Tools”, Price=100,
  12:  
  13:                Customer=cust};
  14:  
  15: dataContext.SubmitChanges();  // 新增purchase

当你从一个EntitySet当中移除一条记录的时候,它的外键将会自动被设为null. 以下代码将两条purchase记录和它们对应的customer解除关系:

   1: var dataContext = new MyTypedDataContext (“connectionString”);
   2:  
   3:   Customer cust = dataContext.Customers.Single(c => c.ID == 1);
   4:  
   5:   cust.Purchases.Remove
   6:  
   7:     (cust.Purchases.Single (p => p.ID == 100));
   8:  
   9:   cust.Purchases.Remove
  10:  
  11:     (cust.Purchases.Single (p => p.ID == 101));
  12:  
  13:   dataContext.SubmitChanges( );  // 提交到SQL SERVER

这里调用SubmitChanges会试图将数据库中对应的CustomerID列的值设为null, 因为数据库中此列必须为nullable, 更进一步, 实体类中对应的属性也必须是nullable的, 否则将会得到一个异常. 如果你想把记录从数据库中直接删除, 则应该使用DeleteOnSubmit将他们从Table<>中删除, 如下所示:

   1: Customer cust = dataContext.Customers.Single(c => c.ID == 1);
   2:  
   3:  var dc = dataContext;
   4:  
   5:  dc.Purchases.DeleteOnSubmit
   6:  
   7:    (dc.Purchases.Single (p => p.ID == 100));
   8:  
   9:  dc.Purchases.DeleteOnSubmit
  10:  
  11:    (dc.Purchases.Single (p => p.ID == 101));
  12:  
  13:  dataContext.SubmitChanges();  // 提交到SQL SERVER数据库
更多

推荐文章