EF 学习笔记
EF 学习笔记
实践中的问题
数据库架构的修改:
视图主键问题:
视图中的字段只要有是不为空的字段,EF会自动推测主键。
如果你有一个无载荷的多对多关系时,你可以考虑通过增加一标识列将其改变为有载荷的多对多关系。为有载荷做准备。
建模
关系
- 维度是指关系中的实体(表)的数量。一半是二维,就是两个表之间的关系。
- 多重性,是指表示关系的线段两端的实体类型数量。0...1(零或者一),1(一)和*(很多)
- 方向可以是双向,也可以是单向。
- 映射关系 :
- 一个诗人有多首诗,和一首诗只有一个作者 (零个作者)。
- 外键在诗表中并且不为空(可以为空)。
- 外键在后半部分,外键是否可为空,代表是否为零。
- 外键所在的表为1对多中的多的表中。
- 实体的Is-a关系VS对象的Is-a关系。
- 实体间的Is-a关系:两个表公用一个主键,把一个表拆成2个表。
- 对象的Is-a关系:对象继承关系。
- 实体has-a 关系VS对象的has-a。
- 实体键has-a关系:参考一对多(一或者零)。
- 对象的has-a关系:一个对象中包含另外一个对象的引用。(依赖关系)
- 复合类型(DDD的值对象)
- 实体的一个属性是一个复合类型(就是类类型)。
- 这个属性映射到了表中的多个列。
- 类型为复合类型的属性不能为NULL,但是可以是虚值(dummy value)??
- 复合类型不能在上下文对象中被跟踪。??
- 依赖?
- 一个诗人有多首诗,和一首诗只有一个作者 (零个作者)。
一对多
多对多
表自引用 0或者1对多
约束:
.HasMany(cat => cat.SubCategories)
.WithOptional(cat => cat.ParentCategory);
实体到表的映射
多个表组合成一个实体
- //实体对象
- public class Product {
- [Key]
- [DatabaseGenerated(DatabaseGeneratedOption.None)]
- public int SKU { get; set; }
- public string Description { get; set; }
- public decimal Price { get; set; }
- public string ImageURL { get; set; }
- }
上下文中实体映射表的关系
- public class EF6RecipesContext : DbContext {
- public DbSet<Product> Products { get; set; }
- public EF6RecipesContext()
- : base("name=EF6CodeFirstRecipesContext") {
- }
- protected override void OnModelCreating(DbModelBuilder modelBuilder) {
- base.OnModelCreating(modelBuilder);
-
- modelBuilder.Entity<Product>()
- //从表Product 映射 p.SKU, p.Description, p.Price 到实体Product
- .Map(m =>
- m.Properties(p => new { p.SKU, p.Description, p.Price });
- m.ToTable("Product", "Chapter2");
- })
- //从表ProductWebInfot 映射 p.SKU, p.Description, p.Price 到实体Product
- .Map(m => {
- m.Properties(p => new { p.SKU, p.ImageURL });
- m.ToTable("ProductWebInfo", "Chapter2");
- });
- }
多个实体组合成一张表
实体一
- public class Photograph
- {
- [Key]
- [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int PhotoId { get; set; }
- public string Title { get; set; }
- public byte[] ThumbnailBits { get; set; }
- [ForeignKey("PhotoId")]
- public virtual PhotographFullImage PhotographFullImage { get; set; }
- }
实体二
- public class PhotographFullImage
- {
- [Key]
- public int PhotoId { get; set; }
- public byte[] HighResolutionBits { get; set; }
- [ForeignKey("PhotoId")]
- public virtual Photograph Photograph { get; set; }
- }
上下文映射关系
- public class EF6Recipes : DbContext
- {
- public EF6Recipes()
- : base("name=EF6Recipes")
- {
- }
- public DbSet<Photograph> Photographs { get; set; }
- protected override void OnModelCreating(DbModelBuilder modelBuilder)
- {
- base.OnModelCreating(modelBuilder);
- modelBuilder.Entity<Photograph>()
- .HasRequired(p => p.PhotographFullImage)
- .WithRequiredPrincipal(p => p.Photograph);
- modelBuilder.Entity<Photograph>().ToTable("Photograph", "Chapter2");
- modelBuilder.Entity<PhotographFullImage>().ToTable("Photograph", "Chapter2");
- }
- //为您要在模型中包含的每种实体类型都添加 DbSet。有关配置和使用 Code First 模型
- //的详细信息,请参阅 http://go.microsoft.com/fwlink/?LinkId=390109。
-
- // public virtual DbSet<MyEntity> MyEntities { get; set; }
- }
注意:实体框架不直接支持延迟加载某个单一的实体属性。我们在概念层添加一个跟数据库引用约束相似的约束,告诉实体框架一个PhotographFullImage不能离开Photograph而独立存在。
TPT继承映射 一表对应一实体
实体
- [Table("Business", Schema = "Chapter2")]
- public class Business
- {
- [Key]
- [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int BusinessId { get; protected set; }
- public string Name { get; set; }
- public string LicenseNumber { get; set; }
- }
- [Table("eCommerce", Schema = "Chapter2")]
- public class eCommerce : Business
- {
- public string URL { get; set; }
- }
- [Table("Retail", Schema = "Chapter2")]
- public class Retail : Business
- {
- public string Address { get; set; }
- public string City { get; set; }
- public string State { get; set; }
- public string ZIPCode { get; set; }
- }
上下文
- public class EF6Recipes : DbContext
- {
- public EF6Recipes()
- : base("name=EF6Recipes")
- {
- }
-
- public DbSet<Business> Businesses { get; set; }
-
- // public virtual DbSet<MyEntity> MyEntities { get; set; }
- }
TPH继承映射 一表对应多个实体
实体
- /// <summary>
- /// 注意:非共享属性(例如:Salary和Wage)必须为可空类型。
- /// </summary>
- [Table("Employee", Schema = "Chapter2")]
- public abstract class Employee
- {
- [Key]
- [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int EmployeeId { get; protected set; }
-
- public string FirstName { get; set; }
- public string LastName { get; set; }
- }
-
- public class FullTimeEmployee : Employee
- {
- public decimal? Salary { get; set; }
- }
-
- public class HourlyEmployee : Employee
- {
- public decimal? Wage { get; set; }
- }
使用泛型方法OfType<T>()获取full-time employees和hourly employees.
最佳实践:
这里的最佳实践是,如果你的应用中不需要基类实体的实例,那么让它成为抽象类型。
如果你的应用中需要一个基类的实例,可以考虑引进一个新的继承实体来覆盖基类中的映射条件属性。例如,在上例中我们可以创建一个这样的派生类UnclassifiedEmployee。 一旦有这个派生类后,我们就可以放心地把基类设为抽象类型。这就提供了一种简单的方式来规避通过在基类中使用映射条件属性来查询的问题。
TPH注意:
第一点,映射条件属性值必须相互独立。换句话来说,就是你不能将一行,条件映射到两个或是更多的类型上。
第二点,映射条件必须对表中的每一行负责,不能存在某一行不被映射到合适的实体类型上。
第三点,鉴别列(区分类型的列)不能映射到一个实体属性上,除非它先被用作一个is not null的映射条件。
约束
.Map<FullTimeEmployee>(m => m.Requires("EmployeeType").HasValue(1))
.Map<HourlyEmployee>(m => m.Requires("EmployeeType").HasValue(2));
Is-a和Has-a关系建模
删除实体数据模型向导生成的关联1-to-0或者1。
右键Location实体,选择 Add(增加) ➤Inheritance(继承)。选择实体Park作为派生类实体,Location作为基类实体;
从Park实体类型删除属性ParkId;
单击Part实体并查看映射详细信息窗口,如果映射详细信息窗口未显示。选择工具菜单View(视图) ➤Other Windows(其它窗口) ➤Entity Data Model Mapping Details(实体数据模型映射详细信息)。在映射详细信息窗口中将ParkId列映射到LocationId属性;
修改Park实体类型的导航属性名为Office,它代表park的办公位置。
复合类型
Agent实体和他的复合属性name和address
类型为复合类型的属性不能为null
查询
查询的方式
LINQ to Entities
Entity SQL
Native SQL
查询函数
执行sql语句
ExecuteSqlCommand();
SqlQuery();
SqlQuery<T>();
异步查询
异步执行sql
await context.Database.ExecuteSqlCommandAsync("delete from chapter3.AssociateSalary");
异步保存
await context.SaveChangesAsync();
ForEachAsync();
ToListAsync();
FirstOrDefaultAsync();
主从复合结构关系中的拥有从表记录的主表记录
博客(BlogPost)和与之关联的评论(Comment)的模型
- var posts = from post in context.BlogPosts
- where post.Comments.Any()
- select post;
查询中设置默认值
查询返回null值时,给相应属性设置默认值
var employees = from e in context.Employees
select new { Name = e.Name, YearsWorked = e.YearsWorked ?? 0 };
存储过程中返回多结果集
- var cs = @"Data Source=.;Initial Catalog=EFRecipes;Integrated Security=True";
- var conn = new SqlConnection(cs);
- var cmd = conn.CreateCommand();
- cmd.CommandType = System.Data.CommandType.StoredProcedure;
- cmd.CommandText = "Chapter3.GetBidDetails";
- conn.Open();
- var reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
- var jobs = ((IObjectContextAdapter) context).ObjectContext.Translate<Job>(reader, "Jobs",
- MergeOption.AppendOnly).ToList();
- reader.NextResult();
- ((IObjectContextAdapter) context).ObjectContext.Translate<Bid>(reader, "Bids", MergeOption.AppendOnly)
- .ToList();
- foreach (var job in jobs)
- {
- Console.WriteLine("\nJob: {0}", job.JobDetails);
- foreach (var bid in job.Bids)
- {
- Console.WriteLine("\tBid: {0} from {1}",
- bid.Amount.ToString(), bid.Bidder);
- }
- }
Translate()方法绕过了映射层模型。如果你想使用继承映射,或者使用一个包含复合类型属性的实体,Translate()方法会失败。
与列表值比较
- var cats = new List<string> {"Programming", "Databases"};
- var books = from b in context.Books
- where cats.Contains(b.Category.Name)
- select b;
- foreach (var book in books)
- {
- Console.WriteLine("'{0}' is in category: {1}", book.Title,
- book.Category.Name);
- }
过滤关联实体
一个Worker有零个或是多个accidents.每个事故按严重性分类,我们要获取所有的工人,对于与之关联的事故,只对严重事故感兴趣,事故的严重性大于2.
- // 显式禁用延迟加载
- context.Configuration.LazyLoadingEnabled = false;
- var query = from w in context.Workers
- select new
- {
- Worker = w,
- Accidents = w.Accidents.Where(a => a.Severity > 2)
- };
- query.ToList();
- var workers = query.Select(r => r.Worker);
- Console.WriteLine("Workers with serious accidents...");
- foreach (var worker in workers)
- {
- Console.WriteLine("{0} had the following accidents", worker.Name);
- if (worker.Accidents.Count == 0)
- Console.WriteLine("\t--None--");
- foreach (var accident in worker.Accidents)
- {
- Console.WriteLine("\t{0}, severity: {1}",
- accident.Description, accident.Severity.ToString());
- }
- }
左连接
- var products = from p in context.Products
- orderby p.TopSelling.Rating descending
- select p;
- Console.WriteLine("All products, including those without ratings");
-
- foreach (var product in products)
- {
- Console.WriteLine("\t{0} [rating: {1}]", product.Name,
- product.TopSelling == null ? "0"
- : product.TopSelling.Rating.ToString());
- }
-
- var products = from p in context.Products
- join t in context.TopSellings on
- //注意,我们如何将结果集投影到另一个名为'g'的序列中,以及应用DefaultIfEmpty方法
- p.ProductID equals t.ProductID into g
- from tps in g.DefaultIfEmpty()
- orderby tps.Rating descending
- select new
- {
- Name = p.Name,
- Rating = tps.Rating == null ? 0 : tps.Rating
- };
-
- Console.WriteLine("\nAll products, including those without ratings");
- foreach (var product in products)
- {
- Console.WriteLine("\t{0} [rating: {1}]", product.Name,
- product.Rating.ToString());
- }
TPH通过派生类排序
1 为Article 2为Video 3为Picture
- var allMedium = from m in context.Media
- let mediumtype = m is Article
- ? 1
- : m is Video ? 2 : 3
- orderby mediumtype
- select m;
- Console.WriteLine("All Medium sorted by type...\n");
- foreach (var medium in allMedium)
- {
- Console.WriteLine("Title: {0} [{1}]", medium.Title, medium.GetType().Name);
- }
分页和过滤
- //
- string match = "Ro";
- int pageIndex = 0;
- int pageSize = 3;
-
- var customers = context.Customers.Where(c => c.Name.StartsWith(match))
- //var customers = context.Customers.Where(c => c.Name.Contains(match))
- .OrderBy(c => c.Name)
- .Skip(pageIndex * pageSize)
- .Take(pageSize);
- Console.WriteLine("Customers Ro*");
- foreach (var customer in customers)
- {
- Console.WriteLine("{0} [email: {1}]", customer.Name, customer.Email);
- }
按日期分组
- var groups = from r in context.Registrations
- // 凭借内置的TruncateTime函数提取Date部分
- group r by DbFunctions.TruncateTime(r.RegistrationDate)
- into g
- select g;
- foreach (var element in groups)
- {
- Console.WriteLine("\nRegistrations for {0}",
- ((DateTime)element.Key).ToShortDateString());
- foreach (var registration in element)
- {
- Console.WriteLine("\t{0}", registration.StudentName);
- }
- }
结果集扁平化
- var allHistory = from a in context.Associates
- from ah in a.AssociateSalaries.DefaultIfEmpty()
- orderby a.Name
- select new
- {
- Name = a.Name,
- Salary = (decimal?)ah.Salary,
- Date = (DateTime?)ah.SalaryDate
- };
- Console.WriteLine("Associate Salary History");
- foreach (var history in allHistory)
- {
- if (history.Salary.HasValue)
- Console.WriteLine("{0} Salary on {1} was {2}", history.Name,
- history.Date.Value.ToShortDateString(),
- history.Salary.Value.ToString("C"));
- else
- Console.WriteLine("{0} --", history.Name);
- }
使用多属性分组
- var results = from e in context.Events
- // 使用匿名类型封闭复合key State 和City
- group e by new { e.State, e.City } into g
- select new
- {
- State = g.Key.State,
- City = g.Key.City,
- Events = g
- };
- Console.WriteLine("Events by State and City...");
- foreach (var item in results)
- {
- Console.WriteLine("{0}, {1}", item.City, item.State);
- foreach (var ev in item.Events)
- {
- Console.WriteLine("\t{0}", ev.Name);
- }
- }
过滤中使用位操作
假设你有一个表示当地画廊的赞助者(patrons)实体,一些赞助者直接捐款(contribute money),一些在画廊里当志愿者(volunteer),一些服务于董事会(board of directors)。
- [Flags]
- public enum SponsorTypes
- {
- None = 0,
- ContributesMoney = 1,
- Volunteers = 2,
- IsABoardMember = 4
- };
jill 1
Ryan keys 5
Karen Rosen 2
Steven king 3
- // 添加新的测试数据
- context.Patrons.Add(new Patron
- {
- Name = "Jill Roberts",
- SponsorType = (int)SponsorTypes.ContributesMoney
- });
- context.Patrons.Add(new Patron
- {
- Name = "Ryan Keyes",
- //注意位操作符中的OR操作符'|'的用法
- SponsorType = (int)(SponsorTypes.ContributesMoney |
- SponsorTypes.IsABoardMember)
- });
- context.Patrons.Add(new Patron
- {
- Name = "Karen Rosen",
- SponsorType = (int)SponsorTypes.Volunteers
- });
- context.Patrons.Add(new Patron
- {
- Name = "Steven King",
- SponsorType = (int)(SponsorTypes.ContributesMoney |
- SponsorTypes.Volunteers)
- });
- var sponsors = from p in context.Patrons
- //注意位操作符中的AND操作符'&'的用法
- where (p.SponsorType &
- (int)SponsorTypes.ContributesMoney) != 0
- select p;
- Console.WriteLine("Patrons who contribute money");
- foreach (var sponsor in sponsors)
- {
- Console.WriteLine("\t{0}", sponsor.Name);
- }
多列连接(Join) 多属性链接查询
- var orders = from o in context.Orders
- join a in context.Accounts on
- // 使用匿名类型来构造一个复合的查询表达式
- new { Id = o.AccountId, City = o.ShipCity, State = o.ShipState }
- equals
- new { Id = a.AccountId, City = a.City, State = a.State }
- select o;
加载实体和导航属性
Lazy Loading(延迟加载)
Eager Loading(预先加载)
立即加载父实体和与之关联的子实体(记住,对象图是基于关联的父实体和子实体,就像数据库中基于外键的父表和子表)。它叫做Eager Loading(预先加载)
加载父类和所有关联的实体,它会可能会加载一个大的对象图,这远远超出了你的需求。
Include()方法
快速查询一个单独的实体
实体框架的默认行为,当你给出一个获取数据的操作时,它会去查询数据库,即使数据已经被加载到上下文中。
Find()方法非常有效率,因为它会先为目标对象查询上下文。如果对象不存在,它会自动去查询底层的数据存储。如果仍然没有找到,Find()方法将返回NULL给调用者。另外,Find()方法将返回已添加到上下文中(状态为"Added"),但还没有保存到数据库中的对象。Find()方法对三种建模方式均有效:Database First,Model First,Code First。
Find()方法只有在上下文中没有找需要的对象时,才去数据库中查询。
SingleOrDefault(),返回一个对象的,如果在底层数据库中不存在要查找的对象,它返回NULL。当发现多个符合给定条件的对象时,SingleOrDefault()方法将抛出一个异常。
NoTracking 选项将禁用指定对象的对象跟踪。没有了对象跟踪,实体框架将不在跟踪Palm Tree Club对象的改变。也不会将对象加载到上下文中。
查询内存对象
context.EntityName.Local 获取local集合的引用
访问Local集合不会产生针对底层数据库的SQL查询,访问上下中的属性集合总是会产生一个被发送到数据库中的SQL是查询。
加载完整的对象图
给Include()方法传递字符串和强类型形式的查询路径参数。查询路径代表我们想使用Include()方法加载的整个对象图路径。Include()方法,扩展查询包含查询路径中指定的实体对象。
加载派生类型上的导航属性
OfType<>()方法,从实体集中获取给定子类型的实例。
在别的LINQ查询操作中使用Include()方法
group by从句组合使用Include()方法,Include()方法必须放在针对父实体的过虑和分组操作之后。
1、Include()方法是一个在IQueryable<T>上的扩展方法;
2、Include()方法只能应用在最终的查询结果集上,当它被在subquery(子查询)、join(连接)或者嵌套从句中,当生成命令树时,它将被忽略掉。在幕后,实体框架会把你的LINQ to Entites查询转换成一棵命令树,然后数据库提供者(database provider)将其处理并构建成一个用于数据库的SQL查询(译注:这一点很重要,我在上面吃过亏,直到现在才弄明白)
3、Include()方法只能应用 在实体类型的结果集上,如果表达式将结果投影到一个非实体类型的类型上,Include()方法将被忽略。
4、在Include()方法和最外面的操作之间,不能改变结果集的类型。例如,一个group by从句,改变结果集的类型。
5、用于Include()方法的查询路径表达式必须从最外层操作返回的类型中的导航属性开始,查询路径不能从任意点开始。
延缓加载(Deferred Loading)相关实体
Load()
Entry() Entry()方法提供了大量的关于实体的信息,包含通过使用方法collection()和Reference()访问关联实体。
Reference()
关联实体过滤和排序
显示加载时,你可以对它进行完全的控制。如果你用 它,你可以控制是否,何时,何地将关联实体加载到上下文中。
一般地,使用Include()方法为一个父实体返回所有的关联实体,但没有机会过滤和操作结果集。这个规则的一个例外就是,应用了显示加载。
我们只能使用这种方式对Include()为一个父实体返回的关联实体集进行过滤。这个特性在延迟加载和预先加载中无效。
在关联实体上执行聚合操作
测试实体引用或实体集合是否加载
实体框架公布了一个IsLoaded属性。只要它100%的确定,指定的实体或实体集合全部已经加载且在上下文中有效时,它的值便为True。
context.Entry(实体).Collection(x => x.Contractors).IsLoaded
IsLoaded被调用Load()方法的查询设置,也被隐式的关系跨度设置。
显示加载关联实体
有两种方式禁用延迟加载
1、设置Context.Configuration对象的LazyLoadingEnabled属性为False。它会禁用上下文中所有实体对象的延迟加载。
2、在每个实体类中移除导航属性的virtual修饰关键字。这种方法会禁用相应实体的延迟加载,这样就能让你显式控制延迟加载。
doctorJoan.Appointments.Clear();
Clear()方法,清空doctorJoan关联实体集合。这个方法会清除掉doctorJoan和appointments间的关系。有趣的是,它并不会把实例从内存中移除;这些实例仍在上下文中--它们只是不在跟Doctor实体连接。
使用Load()方法重新加载时,由于使用了默认的MergeOption.AppendOnly选项,又没有发现新的实例,所有没有实体对象被加载到上下文中(译注:关联实体集合自然也不会添加,但注意这里的Load()方法是生成了SQL查询语句,产生了数据库交互,并从数据库获取了相应的数据的)。
MergeOption在DbContext中不被直接支持。你可能会回想起,我们使用实体框架时,有两个上下文对象可以使用。在实体框架6中,首选是使用DbContext。它提供了直观,易于使用的,遗留ObectContext上下文对象的外观模式。
与AppendOnly一起,MegeOption类型公布了另外三个选项:
1、NoTracking选项会关闭加载实例的对象状态跟踪。使用NoTracking选项,实体框架将不再跟踪对象的变化,同时也不再知道对象是否已经加载到上下文中。如果对象使用NoTracking选项加载,那么它可以被用于对象的导航属性上。NoTracking有一个额外的副作用。如果我们使用NoTracking选项加载一个Doctor实体,那么,使用Load()方法加载appointments时,不管默认行为AppendOnly,仍然会使用NoTracking。
2、OverwriteChanges选项会使用从数据库获取的数据更新当前对象的值,实体框架会继续使用同一个实体对象。这个选项在你想放弃上下文中对实体对象的修改,并使用数据库中的数据来刷新它时特别管用。这个选项非常有用,例如,你的应用正在实现一个撤消操作的功能。
3、PreserveChanges选项,本质上是OverwriteChanges选项的对立选项。当数据库中有改变时,它会更新实体对象的值。但是当内存里的值发生改变时,它不会更新实体对象的值。一个实体对象在内存中被修改,它不会被刷新。更准确地说,在内存中修改实体对象时,它的当前值(cruuent value)不会改变,但是,如果数据库有改变时,它的初始值(original value)会被更新。
使用Load()方法时,这里有一些限制。实体状态为Added,Deleted,或者是Detached时,不能调用Load()方法。
## 过滤预先加载的实体集合
实体框架不支持直接使用Include()时过滤关联实体集合,但我们可以通过创建一个匿名类型来完成同样的事情,匿名类型包含实体和要过滤的关联实体集合
- // 通过ReleaseType和Rating过虑
- // 创建匿名类型集合
- var cats = from c in context.Categories
- where c.ReleaseType == "DVD"
- select new
- {
- category = c,
- movies = c.Movies.Where(m => m.Rating == "PG-13")
- };
修改外键关联
实体框架支持独立关联和外键关联。对于独立关联,由关联间的实体自行跟踪,修改关联的唯一方式是通过修改对象引用。
对于外键关联,你可以通过修改对象引用,或是直接修改外键属性值来修改关联。外键关联不能用作多对多关系。
注意 记住,外键关联简单易用,它是默认方法,也是实体框架开发团队推荐的方法。除非你有具体的业务要求使用独立关联。否则请使用外键关联。
高级应用
获取多对多关联中的链接表
多对多关联代表的是数据库中的一个中间表,这个中间表叫做链接表。链接表不被表示为一个实体,而是被表示成一个对多对多的关联。
为了获取实体键EventId,和OrganizerId,我们可以使用嵌套的from从句,或者 SelectMany()方法。
- var evsorg1 = from ev in context.Events
- from organizer in ev.Organizers
- select new { ev.EventId, organizer.OrganizerId };
- Console.WriteLine("Using nested from clauses...");
- var evsorg2 = context.Events
- .SelectMany(e => e.Organizers,
- (ev, org) => new { ev.EventId, org.OrganizerId });
通常连接表只作为两张表的关系,没有其他作用。
将链接表表示成一个实体
WorkerTask表只包含支持多对多关系的外键,再无别的列了。
1、创建一个POCO实体类WorkerTak。
2、使用类型为ICollection<WorkerTask>的属性WorkerTasks替换POCO实体Worker的属性Tasks;
3、使用类型为ICollection<WorkerTask>的属性WorkerTasks替换POCO实体Task的属性Workers;
4、在上下文对象DbContext的派生类中添加一个类型为DbSet<WorkerTask>的属性;
如何将一个多对多关联表示为一个单独的实体,以方便添加额外的标量属性。
我们的新模型,已经没有一个简单的方式来导航多对多关联。新模型中是两个一对多的关联,这需要增加一级,链接实体。
- var worker = new Worker { Name = "Jim" };
- var task = new Task { Title = "Fold Envelopes" };
- var workertask = new WorkerTask { Task = task, Worker = worker };
- context.WorkerTasks.Add(workertask);
自引用的多对多关系建模
在你的项目中创建一个继承自DbContext的类Recipe3Context;
在你的项目中添加一个POCO实体类 Product;
在上下文对象Recipe3Context中添加一个类型为DbSet<Product>的属性;
在Recipe3Context中重写上下文对象DbContext的方法OnModelCreating,创建自引用的多对多关系映射
重写上下文对象DbContext的方法OnModelCreating,创建自引用的多对多关系映射。
- modelBuilder.Entity<Product>()
- .HasMany(p => p.RelatedProducts)
- .WithMany(p => p.OtherRelatedProducts)
- .Map(m =>
- {
- m.MapLeftKey("ProductId");
- m.MapRightKey("RelatedProductId");
- m.ToTable("RelatedProduct", "Chapter6");
- });
- var product1 = new Product { Name = "Pole", Price = 12.97M };
- var product2 = new Product { Name = "Tent", Price = 199.95M };
- var product3 = new Product { Name = "Ground Cover", Price = 29.95M };
- product2.RelatedProducts.Add(product3);
- product1.RelatedProducts.Add(product2);
- context.Products.Add(product1);
- context.SaveChanges();
- var product2 = context.Products.First(p => p.Name == "Tent");
- Console.WriteLine("Product: {0} ... {1}", product2.Name,
- product2.Price.ToString("C"));
- Console.WriteLine("Related Products");
- foreach (var prod in product2.RelatedProducts)
- {
- Console.WriteLine("\t{0} ... {1}", prod.Name, prod.Price.ToString("C"));
- }
- foreach (var prod in product2.OtherRelatedProducts)
- {
- Console.WriteLine("\t{0} ... {1}", prod.Name, prod.Price.ToString("C"));
- }
我们使用递归方法来处理传递闭包。在遍历导航属性RelatedProducts和OtherrelatedProduxts时,我们要格外小心,不要陷入一个死循环中。如果产品A关联产品B,然后产品B又关联产品A,这样,我们的应用就会陷入无限递归中。为了阻止这种情况的发生,我们使用一个Dictionary<>来帮助我们处理已遍历过的路径。
- ar product1 = context.Products.First(p => p.Name == "Pole");
- Dictionary<int, Product> t = new Dictionary<int, Product>();
- GetRelated(context, product1, t);
- Console.WriteLine("Products related to {0}", product1.Name);
- foreach (var key in t.Keys)
- {
- Console.WriteLine("\t{0}", t[key].Name);
- }
- static void GetRelated(DbContext context, Product p, Dictionary<int, Product> t)
- {
- context.Entry(p).Collection(ep => ep.RelatedProducts).Load();
- foreach (var relatedProduct in p.RelatedProducts)
- {
- //已遍历过的路径
- if (!t.ContainsKey(relatedProduct.ProductId))
- {
- t.Add(relatedProduct.ProductId, relatedProduct);
- GetRelated(context, relatedProduct, t);
- }
- }
- context.Entry(p).Collection(ep => ep.OtherRelatedProducts).Load();
- foreach (var otherRelated in p.OtherRelatedProducts)
- {
- if (!t.ContainsKey(otherRelated.ProductId))
- {
- t.Add(otherRelated.ProductId, otherRelated);
- GetRelated(context, otherRelated, t);
- }
- }
使用TPH建模自引用关系
可能角色有firefighter(f),teacher(t)或者retired(r)。role列用一个字符来指定人的角色。
1、创建一个派生至DbContext的上下文对象Recipe4Context;
2、创建一个抽象的POCO实体Person;
- [Table("Person", Schema = "Chapter6")]
- public abstract class Person
- {
- [Key]
- [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int PersonId { get; protected set; }
- public string Name { get; set; }
-
- public virtual Person Hero { get; set; }
- public virtual ICollection<Person> Fans { get; set; }
- }
3、在上下文对象Recipe4Context中添加一个类型为DbSe<Person>的属性;
4、使用代码清单6-9中的代码,添加具体的POCO实体类,Fierfighter,Teacher和Retired;
- public class Firefighter : Person
- {
- public string FireStation { get; set; }
- }
-
- public class Teacher : Person
- {
- public string School { get; set; }
- }
-
- public class Retired : Person
- {
- public string FullTimeHobby { get; set; }
- }
在上下文对象Recipe4Context中重写OnModelCreating方法,以此配置HeroId外键和类型层次结构。
- base.OnModelCreating(modelBuilder);
-
- modelBuilder.Entity<Person>()
- .HasMany(p => p.Fans)
- .WithOptional(p => p.Hero)
- .Map(m => m.MapKey("HeroId"));
-
- modelBuilder.Entity<Person>()
- .Map<Firefighter>(m => m.Requires("Role").HasValue("f"))
- .Map<Teacher>(m => m.Requires("Role").HasValue("t"))
- .Map<Retired>(m => m.Requires("Role").HasValue("r"));
- var teacher = new Teacher
- {
- Name = "Susan Smith",
- School = "Custer Baker Middle School"
- };
- var firefighter = new Firefighter
- {
- Name = "Joel Clark",
- FireStation = "Midtown"
- };
- var retired = new Retired
- {
- Name = "Joan Collins",
- FullTimeHobby = "Scapbooking"
- };
- context.People.Add(teacher);
- context.People.Add(firefighter);
- context.People.Add(retired);
- context.People.Add(teacher);
- context.People.Add(firefighter);
- context.People.Add(retired);
- context.SaveChanges();
- //我们有一位教师,它是消防员心中的英雄,一位退休的职工,他是这位教师心中的英雄
- firefighter.Hero = teacher;
- teacher.Hero = retired;
- retired.Hero = firefighter;
- context.SaveChanges();
使用TPH建模自引用关系
创建一个POCO实体类Category
- [Table("Category", Schema = "Chapter6")]
- public class Category
- {
- [Key]
- [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int CategoryId { get; set; }
- public string Name { get; set; }
-
- public virtual Category ParentCategory { get; set; }
- public virtual ICollection<Category> SubCategories { get; set; }
- }
重写OnModelCreating方法
- protected override void OnModelCreating(DbModelBuilder modelBuilder)
- {
- base.OnModelCreating(modelBuilder);
-
- modelBuilder.Entity<Category>()
- .HasOptional(c => c.ParentCategory)
- .WithMany(c => c.SubCategories)
- .Map(m => m.MapKey("ParentCategoryId"));
- }
创建一个存储过程
- create proc chapter6.GetSubCategories
- (@categoryid int)
- as
- begin
- with cats as
- (
- select c1.*
- from chapter6.Category c1
- where CategoryId = @categoryid
- union all
- select c2.*
- from cats join chapter6.Category c2 on cats.CategoryId =
- c2.ParentCategoryId
- )
- select * from cats where CategoryId != @categoryid
- end
使用GetSubCategories()方法获取整个层次结构
- using (var context = new Recipe5Context())
- {
- var book = new Category { Name = "Books" };
- var fiction = new Category { Name = "Fiction", ParentCategory = book };
- var nonfiction = new Category { Name = "Non-Fiction", ParentCategory = book };
- var novel = new Category { Name = "Novel", ParentCategory = fiction };
- var history = new Category { Name = "History", ParentCategory = nonfiction };
- context.Categories.Add(novel);
- context.Categories.Add(history);
- context.SaveChanges();
- }
-
- using (var context = new Recipe5Context())
- {
- var root = context.Categories.Where(o => o.Name == "Books").First();
- Console.WriteLine("Parent category is {0}, subcategories are:", root.Name);
- foreach (var sub in context.GetSubCategories(root.CategoryId))
- {
- Console.WriteLine("\t{0}", sub.Name);
- }
- }
映射派生类中的NULL条件
你想使用TPH创建一个模型,列值为null时,表示一个派生类型,不为null时,表示另一个派生类型。
创建POCO实体类Drug、Medicine和Experimental(实验性)
- [Table("Drug", Schema = "Chapter6")]
- public abstract class Drug
- {
- [Key]
- [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
- public int DrugId { get; set; }
- public string Name { get; set; }
- }
-
- public class Experimental : Drug
- {
- public string PrincipalResearcher { get; set; }
-
- public void PromoteToMedicine(DateTime acceptedDate, decimal targetPrice,
- string marketingName)
- {
- var drug = new Medicine { DrugId = this.DrugId };
- //实体框架会产生一个update(更新)语句,而不是insert(插入)语句。
- using (var context = new Recipe6Context())
- {
- context.Drugs.Attach(drug);
- drug.AcceptedDate = acceptedDate;
- drug.TargetPrice = targetPrice;
- drug.Name = marketingName;
- context.SaveChanges();
- }
- }
-
- }
-
- public class Medicine : Drug
- {
- public decimal? TargetPrice { get; set; }
- public DateTime AcceptedDate { get; set; }
- }
在上下文对象Recipe6Context中添加一个类型为DbSe<Drug>的属性;
在上下文对象Recipe6Context中重写OnModelCreating方法,让它为临床用药(Medicine)和试验性药物(Experimental)类型配置TPH映射。
- protected override void OnModelCreating(DbModelBuilder modelBuilder)
- {
- base.OnModelCreating(modelBuilder);
- modelBuilder.Entity<Experimental>()
- .Map(m => m.Requires("AcceptedDate").HasValue((DateTime?)null));
- modelBuilder.Entity<Medicine>()
- .Map(m => m.Requires(d => d.AcceptedDate).HasValue());
- }
- using (var context = new Recipe6Context())
- {
- var exDrug1 = new Experimental { Name = "Nanoxol",
- PrincipalResearcher = "Dr. Susan James" };
- var exDrug2 = new Experimental { Name = "Percosol",
- PrincipalResearcher = "Dr. Bill Minor" };
- context.Drugs.Add(exDrug1);
- context.Drugs.Add(exDrug2);
- context.SaveChanges();
-
- // Nanoxol刚获生产批准
- exDrug1.PromoteToMedicine(DateTime.Now, 19.99M, "Treatall");
- context.Entry(exDrug1).State = EntityState.Detached; //不在使用此实例
- }
-
- using (var context = new Recipe6Context())
- {
- Console.WriteLine("Experimental Drugs");
- foreach (var d in context.Drugs.OfType<Experimental>())
- {
- Console.WriteLine("\t{0} ({1})", d.Name, d.PrincipalResearcher);
- }
-
- Console.WriteLine("Medicines");
- foreach (var d in context.Drugs.OfType<Medicine>())
- {
- Console.WriteLine("\t{0} Retails for {1}", d.Name,
- d.TargetPrice.Value.ToString("C"));
- }
- }
使用一个非主键列建模TPT继承映射
我们一张职工(Staff)表,它包含员工的姓名(Name),两张包含校长(Principal)、教员(Instructor)信息的表。
这里需要引起注意的是,表Principal和Instructor中的主键不是Staff表的外键。
这种类型的关系结构是不能直接使用TPT继承映射的。
TPT,关联表的主键必须是主表(基表)的外键。
- create procedure [chapter6].[InsertInstructor]
- (@Name varchar(50), @Salary decimal)
- as
- begin
- declare @staffid int
- insert into Chapter6.Staff(Name) values (@Name)
- set @staffid = SCOPE_IDENTITY()
- insert into Chapter6.Instructor(Salary,StaffId) values (@Salary,@staffid)
- select @staffid as StaffId,SCOPE_IDENTITY() as InstructorId
- end
- go
- create procedure [chapter6].[UpdateInstructor]
- (@Name varchar(50), @Salary decimal, @StaffId int, @InstructorId int)
- as
- begin
- update Chapter6.Staff set Name = @Name where StaffId = @StaffId
- update Chapter6.Instructor set Salary = @Salary where InstructorId = @InstructorId
- end
- go
- create procedure [chapter6].[DeleteInstructor]
- (@StaffId int)
- as
- begin
- delete Chapter6.Staff where StaffId = @StaffId
- delete Chapter6.Instructor where StaffId = @StaffId
- end
- go
- create procedure [Chapter6].[InsertPrincipal]
- (@Name varchar(50),@Salary decimal,@Bonus decimal)
- as
- begin
- declare @staffid int
- insert into Chapter6.Staff(Name) values (@Name)
- set @staffid = SCOPE_IDENTITY()
- insert into Chapter6.Principal(Salary,Bonus,StaffId) values
- (@Salary,@Bonus,@staffid)
- select @staffid as StaffId, SCOPE_IDENTITY() as PrincipalId
- end
- go
- create procedure [Chapter6].[UpdatePrincipal]
- (@Name varchar(50),@Salary decimal, @Bonus decimal, @StaffId int, @PrincipalId int)
- as
-
- begin
- update Chapter6.Staff set Name = @Name where StaffId = @StaffId
- update Chapter6.Principal set Salary = @Salary, Bonus = @Bonus where
- PrincipalId = @PrincipalId
- end
- go
- create procedure [Chapter6].[DeletePrincipal]
- (@StaffId int)
- as
- begin
- delete Chapter6.Staff where StaffId = @StaffId
- delete Chapter6.Principal where StaffId = @StaffId
- end
在解决方案浏览器中右键.edmx文件,选择Open With(打开方式) ➤XML Editor(XML文本编辑器)。这将关闭设计器窗口并在XML编辑器中打开.edmx文件。
滚动到映射怪中的标签<EntityContainerMappin\g>。将代码清单6-21中的查询视图(QueryView)插入到标签<EntitySetMapping>中。
- <EntitySetMapping Name="Staffs">
- <QueryView>
- select value
- case
- when (i.StaffId is not null) then
- Apress.EF6Recipes.BeyondModelingBasics.Recipe7.Instructor(s.StaffId,s.Name,i.InstructorId,i.Salary)
- when (p.StaffId is not null) then
- Apress.EF6Recipes.BeyondModelingBasics.Recipe7.Principal(s.StaffId,s.Name,p.PrincipalId,p.Salary,p.Bonus)
- END
- from ApressEF6RecipesBeyondModelingBasicsRecipe7StoreContainer.Staff as s
- left join ApressEF6RecipesBeyondModelingBasicsRecipe7StoreContainer.Instructor as i
- on s.StaffId = i.StaffId
- left join ApressEF6RecipesBeyondModelingBasicsRecipe7StoreContainer.Principal as p
- on s.StaffId = p.StaffId
- </QueryView>
- </EntitySetMapping>
使用TPT继承映射,实体框架要求基类表中的外键是派生类中的主键。在我们的示例中,每个派生类表有自己独立的主键。
为了创建TPT继承映射模型,在概念层,实体Principal和Instructor继承自实体Staff。接下来,我们删除导入表时创建的映射。然后我们使用一个QueryView表达式来创建一个新的映射。使用QueryView将Insert、Update和Delete动作放入我们的代码中。为了处理这些动作,我们在数据库中创建了额外的存储过程。
我们使用QueryView将映射派生类中的标量属性到数据库表中。QueryView中的关键部分是case语句。这里有两种情况,我们有一个Principal和一个Instructor。如果Instructor的StaffId非null时,我们就得到一个Instructor实例;如果Principal的StaffId为null时,我们就得到一个Principal实例。表达式剩下的部分是,引入派生类表中的行。
从模型中插入和获取
- using (var context = new Recipe7Context())
- {
- var principal = new Principal
- {
- Name = "Robbie Smith",
- Bonus = 3500M,
- Salary = 48000M
- };
- var instructor = new Instructor
- {
- Name = "Joan Carlson",
- Salary = 39000M
- };
- context.Staffs.Add(principal);
- context.Staffs.Add(instructor);
- context.SaveChanges();
- }
-
- using (var context = new Recipe7Context())
- {
- Console.WriteLine("Principals");
- Console.WriteLine("==========");
- foreach (var p in context.Staffs.OfType<Principal>())
- {
- Console.WriteLine("\t{0}, Salary: {1:C}, Bonus: {2:C}",
- p.Name, p.Salary,
- p.Bonus);
- }
- Console.WriteLine("Instructors");
- Console.WriteLine("===========");
- foreach (var i in context.Staffs.OfType<Instructor>())
- {
- Console.WriteLine("\t{0}, Salary: {1:C}", i.Name, i.Salary);
- }
- }
嵌套的TPH建模
Employee表包含钟点工,雇员,提成员工
创建POCO实体类 Employee、HourlyEmployee、SalariedEmployee和CommissionedEmployee
- public abstract class Employee
- {
- public int EmployeeId { get; set; }
- public string Name { get; set; }
- }
-
- public class SalariedEmployee : Employee
- {
- public decimal? Salary { get; set; }
- }
-
- public class CommissionedEmployee : SalariedEmployee
- {
- public decimal? Commission { get; set; }
- }
-
- public class HourlyEmployee : Employee
- {
- public decimal? Rate { get; set; }
- public decimal? Hours { get; set; }
- }
- }
在上下文对象中添加一个类型为DbSet\Employee>的属性;
在上下文对象中重写OnModelCreating方法,配置TPH中每个派生类的鉴别值
- protected override void OnModelCreating(DbModelBuilder modelBuilder)
- {
- base.OnModelCreating(modelBuilder);
-
- modelBuilder.Entity<Employee>()
- .HasKey(e => e.EmployeeId)
- .Property(e => e.EmployeeId)
- .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
-
- modelBuilder.Entity<Employee>()
- .Map<HourlyEmployee>(m => m.Requires("EmployeeType").HasValue("hourly"))
- .Map<SalariedEmployee>(m => m.Requires("EmployeeType").HasValue("salaried"))
- .Map<CommissionedEmployee>(m => m.Requires("EmployeeType").HasValue("commissioned"))
- .ToTable("Employee", "Chapter6");
- }
插入并获取Employee的派生类型
- using (var context = new Recipe8Context())
- {
- var hourly = new HourlyEmployee
- {
- Name = "Will Smith",
- Hours = (decimal)39,
- Rate = 7.75M
- };
- var salaried = new SalariedEmployee
- {
- Name = "JoAnn Woodland",
- Salary = 65400M
- };
- var commissioned = new CommissionedEmployee
- {
- Name = "Joel Clark",
- Salary = 32500M,
- Commission = 20M
- };
- context.Employees.Add(hourly);
- context.Employees.Add(salaried);
- context.Employees.Add(commissioned);
- context.SaveChanges();
- }
-
- using (var context = new Recipe8Context())
- {
- Console.WriteLine("All Employees");
- Console.WriteLine("=============");
- foreach (var emp in context.Employees)
- {
- if (emp is HourlyEmployee)
- Console.WriteLine("{0} Hours = {1}, Rate = {2}/hour",
- emp.Name,
- ((HourlyEmployee)emp).Hours.Value.ToString(),
- ((HourlyEmployee)emp).Rate.Value.ToString("C"));
- else if (emp is CommissionedEmployee)
- Console.WriteLine("{0} Salary = {1}, Commission = {2}%",
- emp.Name,
- ((CommissionedEmployee)emp).Salary.Value.ToString("C"),
- ((CommissionedEmployee)emp).Commission.ToString());
- else if (emp is SalariedEmployee)
- Console.WriteLine("{0} Salary = {1}", emp.Name,
- ((SalariedEmployee)emp).Salary.Value.ToString("C"));
- }
- }
在TPT继承映射中应用条件
创建一个多条件过滤 QueryView
WebOrder中的实例为,2012年以后的,2010年到2012年之间未删除的,2010年以前的订单金额大于200美元的。这样的复杂过滤条件不能使用映射详细信息窗口中有限制的条件来创建了。 有一种实现方法是使用QueryView。
在数据库中为WebOrder实体定义的Insert,Update和Delete动作。
- create procedure [Chapter6].[InsertOrder]
- (@CustomerName varchar(50),@OrderDate date,@IsDeleted bit,@Amount decimal)
- as
- begin
- insert into chapter6.WebOrder (CustomerName, OrderDate, IsDeleted, Amount)
- values (@CustomerName, @OrderDate, @IsDeleted, @Amount)
- select SCOPE_IDENTITY() as OrderId
- end
- go
- create procedure [Chapter6].[UpdateOrder]
- (@CustomerName varchar(50),@OrderDate date,@IsDeleted bit,
- @Amount decimal, @OrderId int)
- as
- begin
- update chapter6.WebOrder set CustomerName = @CustomerName,
- OrderDate = @OrderDate,IsDeleted = @IsDeleted,Amount = @Amount
- where OrderId = @OrderId
- end
- go
- create procedure [Chapter6].[DeleteOrder]
- (@OrderId int)
- as
- begin
- delete from Chapter6.WebOrder where OrderId = @OrderId
- end
选择WebOrder实体,并查看Mapping Details window(映射详细信息窗口)。单击Map Entity to Function(映射实体到函数)按钮。这个按钮是在映射详细信息窗口左边最下边的一个按钮。映射Insert、Update和Delete动作到存储过程。prperty/parameter(属性/参数)映射会自动填入。然而,存储过程InsertOrder的返回值必须映射到OrderId属性。这是实体框架用于在插入操作后获取标识列OrderId值的途径。
在映射详细信息窗口中选择映射表(最上边的按钮)。删除WebOrder表的映射。我们将在后面使用QueryView来映射;
在解决方案浏览器中右键.edmx文件,选择Open With(打开方式) ➤XML Editor(XML文本编辑器)。
在C-S映射层,将代码清单6-28中的查询视图(QueryView)插入到标签<EntitySetMapping>中,QueryView将映射实体WebOrder。C-S映射层的修改会在下一次从数据库更新模型时丢失。
- <EntitySetMapping Name="WebOrders">
- <QueryView>
- select value
- Apress.EF6Recipes.BeyondModelingBasics.Recipe10.WebOrder(o.OrderId,
- o.CustomerName,o.OrderDate,o.IsDeleted,o.Amount)
- from ApressEF6RecipesBeyondModelingBasicsRecipe10StoreContainer.WebOrder as o
- where (o.OrderDate > datetime'2012-01-01 00:00') ||
- (o.OrderDate between cast('2010-01-01' as Edm.DateTime) and
- cast('2012-01-01' as Edm.DateTime) and !o.IsDeleted) ||
- (o.Amount > 200 and o.OrderDate <
- cast('2010-01-01' as Edm.DateTime))
- </QueryView>
- </EntitySetMapping>
QueryView是一种只读映射,它可以用来代替实体框架为我们提供的默认映射。
当QueryView在映射层标签<EntitySetMapping>中时,它将映射存储层定义的表和在概念模型层中定义的实体。当它在标签<AssociationSetMapping>中时,它将映射存储层中的关系和概念模型中的关联。QueryView的通常用法是,用在标签<AssociationSetMapping>中,用它来实现基于条件的,且不能通过默认的条件映射来实现的继承映射。
当实体使用QueryView来映射时,实体框架对这精确的映射实现浑然不知。这是因为实体框架不知道底层的表和列用来创建实体的实例,它不能产生适当的存储层动作来插入、更新或者删除实体。实体被实例化后,实体框架也不会进行跟踪。它不知道底层如何修改实体。
实现插入、更新和删除动作的责任落到了开发员人的头上,这些动作可以直接在.edmx文件中实现,也可以在数据库使用存储过程来实现。为了管理这些动作的存储过程,你需要创建<ModificationFunctionMapping>节。
下面是QueryView的常见使用场景:
1、定义一个不被支持的过滤条件,比如:大于,小于等等;
2、映射基于除了is null,not null或者equal to条件的继承;
3、映射一列计算后的列,或是从表中返加列的子集,或是为了改变限制或类型为Data的列,例如,让它为可空类型,将一字符串列显现为整型;
4、映射基于不同主键和外键的TPT继承映射;
5、映射存储层相同的列到概念模型中不同的类型;
6、映射多个类型到同一张表;
TPH继承映射中使用复合条件(大于,小于,between)
在我们的模型中,我们想使用TPH为派生类,AdultMember(成人会员)、SeniorMember(老年人会员)和TeenMember(青少年会员)建模。
实体框架支持TPH继承映射的条件有=、is null和is not null。像<、between、和>这样简单的表达式都不能支持。
一个会员的年龄少于20就是一位青少年会员(我们俱乐部会员最小的年龄为13)。一个会员的年龄在20到55之间就是成年会员。正如你预料的那样,如果年龄超过55就是一位老年会员。
- create procedure [Chapter6].[InsertMember]
- (@Name varchar(50), @Phone varchar(50), @Age int)
- as
- begin
- insert into Chapter6.Member (Name, Phone, Age)
- values (@Name,@Phone,@Age)
- select SCOPE_IDENTITY() as MemberId
- end
-
- create procedure [Chapter6].[UpdateMember]
- (@Name varchar(50), @Phone varchar(50), @Age int, @MemberId int)
- as
- begin
- update Chapter6.Member set Name=@Name, Phone=@Phone, Age=@Age
- where MemberId = @MemberId
- end
-
-
- create procedure [Chapter6].[DeleteMember]
- (@MemberId int)
- as
- begin
- delete from Chapter6.Member where MemberId = @MemberId
- end
映射Member表到派生类型Teen、Adult和Senior的QueryView
- <EntitySetMapping Name="Members">
- <QueryView>
- select value
- case
- when m.Age < 20 then
- Apress.EF6Recipes.BeyondModelingBasics.Recipe11.Teen(m.MemberId,m.Name,m.Phone,m.Age)
- when m.Age between 20 and 55 then
- Apress.EF6Recipes.BeyondModelingBasics.Recipe11.Adult(m.MemberId,m.Name,m.Phone,m.Age)
- when m.Age > 55 then
- Apress.EF6Recipes.BeyondModelingBasics.Recipe11.Senior(m.MemberId,m.Name,m.Phone,m.Age)
- end
- from ApressEF6RecipesBeyondModelingBasicsRecipe11StoreContainer.Member as m
- </QueryView>
- </EntitySetMapping>
我们通过QueryView定义了Member表和派生类型:Senior,Adult,和Teen的映射,扩展了实体框架支持的条件。
使用QueryView也会付出代价。因为我得自己定义映射,还有担起了实现派生类型插入、更新和删除动作的责任。
在创建上下对象时注册SavingChanges事件。并在这个事件中执行验证数据。
当调用SaveChanges()方法时,我们的Validate()方法将检查每个新增或修改的对象。对于每一个实体的实例,我们验证它的属性Age是否和它的类型对应。当发现一个验证错误时,我们简单地抛出一个异常。
- public partial class Recipe11Context
- {
- public override int SaveChanges()
- {
- //译注:书使用的是下面注释的这句,因为这是原书的第二版,
- //书中的代码很可能是第一版时的,没被更新而遗留下来的。
- //this.SavingChanges += new EventHandler(Validate)
- Validate();
- return base.SaveChanges();
- }
-
- public void Validate()
- {
- var entities = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager
- .GetObjectStateEntries(EntityState.Added |
- EntityState.Modified)
- .Select(et => et.Entity as Member);
- foreach (var member in entities)
- {
- if (member is Teen && member.Age > 19)
- {
- throw new ApplicationException("Entity validation failed");
- }
- else if (member is Adult && (member.Age < 20 || member.Age >= 55))
- {
- throw new ApplicationException("Entity validation failed");
- }
- else if (member is Senior && member.Age < 55)
- {
- throw new ApplicationException("Entity validation failed");
- }
- }
- }
-
- }
TPC继承映射建模
你有两张或多张架构和数据类似的表。
表Toyota和BMW有相似的架构(Schema),并描述类似的数据。BMW表只多了额外的一列,它用一bit值来指示对应的实例是否具有避免碰撞(collision-avoidance)特性。
用一个基类来持有表Toyta和BMW的公共属性。另外,我们还想表示经销商与汽车库存量之间的一对多关系。
TPC是一个有趣的继承模型,它允许每个派生类实体映射到单独的物理表。从实用的角度来看,表至少有一部分公共的架构。 这个公共的的架构被映射到基类,额外部分的架构映射到派生类实体。为了让TPC继承模型正常工作,实体键在整个相关表(译注:如本示例中的表Toyota和BMW)中必须唯一。
在TPC中,只有派生类型实体被映射到表。
标记Car实体为抽象类型,不对它进行映射。
我们只映射派生类型BMW和Toyota。
我们将公共属性(CarID,Model,Year和Color)移动到了基类实体。
派生类只包含属于自己的独特的属性。
因为实体Toyota和BMW派生至实体Car,所以,它们变成了相同实体集Cars的一部分。这意味着,实体键必须在包含整个派生类型的实体集中唯一。
当在使用TPC继承映射建模关系时,在派生中定义关系比在基类中定义更好。
TPC继承映射跟别的继承映射相比,有一个特别重要的性能优势。当查询一个一派生类型时,产生针对底层数据库表的查询,没有像TPT继承映射中额外的join连接,也没有像TPH中的过滤。对于有几个派生类型的大型数据集或模型,这种性能优势显得至关重要。
TPC继承映射的缺点是,包含对潜在重复数据的开销,和在整个相关表中确保主键唯一的复杂性。
在基类中应用条件
你想从一个已存在的模型中的实体派生一个新的实体,允许基类被实例化。
这个模型只包含一个单独的实体Invoice(发货单)。我们想从Invoice派生一个新的实体,它表示删除掉的发货单。
在Mapping Details window(映射详细信息窗口)查看实体Invoice的信息,在IsDeleted列上添加一个条件,When IsDeleted =0(当IsDeleted列值为0)
现在IsDeleted列被用作条件,我们需要将其从实体的标量属性中移除。在实体Invoice的属性IsDeleted上右键,选择Delete(删除)
右键设计器,并选择Add(新增)➤Entity(实体)。命名新实体为DeletedInvoice,并选择Invoice作为基类;
在Mapping Details window(映射详细信息窗口),查看实体DeletedInvoice的映射信息。将它映射到Invoice表,在IsDeleted列上添加一个条件,When IsDeleted =1(当IsDeleted列值为1)
Model First来创建独立关联和外键关联
对于一个外键关联,外键被当作依赖实体的一个属性。暴露外键允许使用与管理别的属性值相同的代码,来管理关联的很多方面。最主要的帮助就是在离线场景。
于独立关联,外键不会作为依赖实体的属性。这使得模型的概念层有几分清爽,因为这里没有引入关联的实现细节这种噪音。
修改独立关联为外键关联
对象服务
动态构建连接字符串
在GetConnection()方法中,我们从配置文件中获取了特定环境的data source和initial catalog。
- public static class ConnectionStringManager
- {
- public static string EFConnection = GetConnection();
-
- private static string GetConnection()
- {
- var sqlBuilder = new SqlConnectionStringBuilder();
-
- sqlBuilder.DataSource = ConfigurationManager.AppSettings["SqlDataSource"];
-
- // 填充剩下的
- sqlBuilder.InitialCatalog = ConfigurationManager.AppSettings["SqlInitialCatalog"];
-
- sqlBuilder.IntegratedSecurity = true;
- sqlBuilder.MultipleActiveResultSets = true;
-
- var eBuilder = new EntityConnectionStringBuilder();
- eBuilder.Provider = "System.Data.SqlClient";
- eBuilder.Metadata =
- "res://*/Recipe1.csdl|res://*/Recipe1.ssdl|res://*/Recipe1.msl";
- eBuilder.ProviderConnectionString = sqlBuilder.ToString();
- return eBuilder.ToString();
- }
-
- }
-
- public partial class EF6RecipesContainer
- {
- public EF6RecipesContainer(string nameOrConnectionString)
- : base(nameOrConnectionString)
- {
-
- }
- }
-
从数据库中读取模型
从数据表中读取为模型定义的CSDL,MSL和SSDL。
我们的模型只有一个实体:Customer。概念层的CSDL),映射层的MSL和存储层的SSDL,它们的定义通常能在你的项目中的.edmx文件中发现。但我们想从数据库读取它们的定义。为了能从数据库中读取这些定义,请按下面的步骤进行:
1、右键设计器,查看属性。更改代码生成策略为None。我们将为我们的Customer类使用POCO;
2、创建如图7-2所示的表,这张表将保存我们项目模型的定义;
3、右键设计器,查看属性。更改元数据项目处理(Metadate Artifact Processing)为“复制到输出目录”(Copy to Output Directory)。重新编译你的项目。编译过程将在输出目录生成三个文件:Recipe2.ssdl,Recipe2.csdl和Recipe2.msl;
4、将上一步生成文件的内容插入到Definitions表中合适的列,Id列使用值1;
5、使用代码清单7-2,从数据库表Definitions中读取元数据,并创建一个应用要使用的MetadateWorkSpace类;
- public static class Recipe2Program
- {
- public static void Run()
- {
- //我们创建一个ContextFacotry,使用存储的元数据来创建我们的上下文对象,元数据不是存储在.edmx文件,而是数据库中。我们使用CreateContext()方法来实现这个功能。
- using (var context = ContextFactory.CreateContext())
- {
- context.Customers.AddObject(
- new Customer { Name = "Jill Nickels" });
- context.Customers.AddObject(
- new Customer { Name = "Robert Cole" });
- context.SaveChanges();
- }
-
- using (var context = ContextFactory.CreateContext())
- {
- Console.WriteLine("Customers");
- Console.WriteLine("---------");
- foreach (var customer in context.Customers)
- {
- Console.WriteLine("{0}", customer.Name);
- }
- }
- }
-
-
- }
-
-
- public class Customer
- {
- public virtual int CustomerId { get; set; }
- public virtual string Name { get; set; }
- }
-
- public class EFRecipesEntities : ObjectContext
- {
- private ObjectSet<Customer> customers;
- public EFRecipesEntities(EntityConnection cn)
- : base(cn)
- {
- }
-
- public ObjectSet<Customer> Customers
- {
- get
- {
- return customers ?? (customers = CreateObjectSet<Customer>());
- }
- }
- }
-
- //CreateContext()方法创建了一个新的基于两个参数的EntityConnection:我们在CreateWorkSpace()方法中创建的 workspace,和一个SQL连接字符串。真正的工作发生在CreateWorkSpace()方法如何创建workspace的过程中。
- public static class ContextFactory
- {
- static string connString = @"Data Source=localhost;
- initial catalog=EFRecipes;Integrated Security=True;";
- private static MetadataWorkspace workspace = CreateWorkSpace();
-
- public static EFRecipesEntities CreateContext()
- {
- var conn = new EntityConnection(workspace,
- new SqlConnection(connString));
- return new EFRecipesEntities(conn);
- }
- //CreateWorkSpace()方法,打开存储元数据数据库的连接,我们构建一条SQL语句从Definitions表中读取一行数据,Definitions表(如图7-2)保存着,概念层、存储层和映射层的定义。
- //使用Xmlreaders来读取这些定义 。
- //创建MetadataWorkspace的实例对象了。
- //MetadataWorkspace在内存中代表一个模型。
- //这个workspace是实体框架的默认管道从.edmx文件创建的,而现在我们是从数据库表Definitions创建。
- //还有别的方法可以创建这个对象,这些方法包含使用嵌入资源和Code First来实现。
- private static MetadataWorkspace CreateWorkSpace()
- {
- string sql = @"select csdl,msl,ssdl from Chapter7.Definitions";
- XmlReader csdlReader = null;
- XmlReader mslReader = null;
- XmlReader ssdlReader = null;
-
- using (var cn = new SqlConnection(connString))
- {
- using (var cmd = new SqlCommand(sql, cn))
- {
- cn.Open();
- var reader = cmd.ExecuteReader();
- if (reader.Read())
- {
- csdlReader = reader.GetSqlXml(0).CreateReader();
- mslReader = reader.GetSqlXml(1).CreateReader();
- ssdlReader = reader.GetSqlXml(2).CreateReader();
- }
- }
- }
-
- var edmCollection = new EdmItemCollection(new XmlReader[]
- { csdlReader });
- var ssdlCollection = new StoreItemCollection(new XmlReader[]
- { ssdlReader });
- var mappingCollection = new StorageMappingItemCollection(
- edmCollection, ssdlCollection, new XmlReader[] { mslReader });
-
- var localWorkspace = new MetadataWorkspace();
- localWorkspace.RegisterItemCollection(edmCollection);
- localWorkspace.RegisterItemCollection(ssdlCollection);
- localWorkspace.RegisterItemCollection(mappingCollection);
- return localWorkspace;
- }
- }
配置模型
.edmx文件包含全部三层:概念模型层,映射层和存储逻辑层。
.edmx文件只是一个供设计时使用的便捷容器。
模型的配置依赖模型中所有的层,这些层可以被嵌入程序集,存储在文件中,也可以是7-2节中看到的那样,从别的数据源获取并完成MetadataWorkspace实例的创建。
如果元数据项目处理(Metadate Artifact Processing)设置为“嵌入输出程序集中”(Embed in Output Assembly),你将会看到你的配置文件App.config或者web.config中的连接字符串,包含一个metadata标签,它可能是如下的样子:
这些符号表示,嵌入程序集中的模型层对应的搜索路径。
如果更改元数据项目处理(Metadate Artifact Processing)为“复制到输出目录”(Copy to Output Directory),你会看到连接字符串会改变成类似下面的样子:
这些符号表示,每个模型层对应文件的路径。
表7-1展示了,你可能在别的程序集中引用嵌入模型层数据的结构。
部署模型 使用实体框架的单复数服务
从跟踪器中获取实体
创建一个扩展方法获取实体状态为Added、Modifed或者Unchanged的所有实体。
- public static class Recipe5Program
- {
- public static void Run()
- {
- using (var context = new Recipe5Context())
- {
- var tech1 = new Technician { Name = "Julie Kerns" };
- var tech2 = new Technician { Name = "Robert Allison" };
- context.ServiceCalls.Add(new ServiceCall
- {
- ContactName = "Robin Rosen",
- Issue = "Can't get satellite signal.",
- Technician = tech1
- });
- context.ServiceCalls.Add(new ServiceCall
- {
- ContactName = "Phillip Marlowe",
- Issue = "Channel not available",
- Technician = tech2
- });
-
- //获取Added状态的实体
- foreach (var tech in
- context.ChangeTracker.GetEntities<Technician>())
- {
- Console.WriteLine("Technician: {0}", tech.Name);
- foreach (var call in tech.ServiceCalls)
- {
- Console.WriteLine("\tService Call: Contact {0} about {1}",
- call.ContactName, call.Issue);
- }
- }
- }
-
- }
- }
- //我们实现了扩展方法GetEntities<T>(),它获取上下中状态为Added,Modified,和Unchanged的所有实体。
- public static class ChangeTrackerExtensions
- {
- public static IEnumerable<T> GetEntities<T>(this DbChangeTracker tracker)
- {
- var entities = tracker
- .Entries()
- .Where(entry => entry.State != EntityState.Detached && entry.Entity != null)
- .Select(entry => entry.Entity).OfType<T>();
- return entities;
- }
- }
我们使用LINQ-to Entities过滤Entries<T>()方法的整个集合。这个方法返回所有的非Detached状态的条目。我们从返回结果集中过滤掉关系以及为null的条目,从剩下的条目中,我们只选择给定类型的条目。
标识关系中使用依赖实体
实体LineItem的实体键,它的标识,同时也是相对于实体Invoice的外键。
实体LineItem被称作依赖实体(depaendent entity),Invoice被称作主实体(principal entity)。
简单地从主实体的集合中删除依赖实体,在实体框架中的结果是,删除的依赖实体被标记为删除状态。另外,删除主实体,会连同依赖实体一直被标记为删除。
使用数据库上下文插入实体
插入一个员工,创建一个Employee的实例并调用上下文对象中Employees实体集中的方法Add()。
添加员工的一个任务,创建一个Task的实例,并将它添加到employee对象的Tasks集合中。
调用Add()方法将employee和task添加到数据库上下文中。
- var employee1 = new Employee
- {
- EmployeeNumber = 629,
- Name = "Robin Rosen",
- Salary = 106000M
- };
- var employee2 = new Employee
- {
- EmployeeNumber = 147,
- Name = "Bill Moore",
- Salary = 62500M
- };
- var task1 = new Task { Description = "Report 3rd Qtr Accounting" };
- var task2 = new Task { Description = "Forecast 4th Qtr Sales" };
- var task3 = new Task { Description = "Prepare Sales Tax Report" };
-
- //在Employees实体集上使用Add()方法
- context.Employees.Add(employee1);
-
- //添加两个新的task到employee1的Tasks中
- employee1.Tasks.Add(task1);
- employee1.Tasks.Add(task2);
-
- // 添加一个task到employee2的Tasks中,并使用Add()方法添加task到上下文中
- employee2.Tasks.Add(task3);
- context.Tasks.Add(task3);
-
- //持久化到数据库
- context.SaveChanges();
当你添加一个实体到上下文中,实体框架会为这个新加入的实体,创建一个临时实体键。
实体框架使用这个临时的键来唯一标识该实体。
当实体被持久化到数据库后,这个临时的实体键,会被一个真正的键值给替换。
如果添加到数据库中的两个实体被分配相同的实体键,实体框架会抛出一个异常。
你还可以使用Attach()方法来添加一个实体到上下文中。
首先是调用Attach()方法。它将添加实体到上下文中,但是变化跟踪器一开始将实体标记为Unchanged状态。
如果这时调用SaveChanges()方法,它不会将实体保存到数据库。
实体递给上下文的Entry()方法,获得一个DBEntityEntry实例,并设置它的属性State为新的状态:EntityState.Added。这时调用SaveChanges()方法会将新实体保存到数据库。
异步查询和保存
实体Transaction显然是实体Account的依赖实体。
通过创建一个EntityTypeConfiguration子类来为每个实体配置关系。
我们创建DbContext的子类并重写OnModelCreating方法,在这个方法中,我们添加实体配置到模型构建器的配置集合中。
异步查询和保存,我们将分别使用LINQ to Entities方法ForEachAsync(),和上下文DbContext的方法SaveChangesAsync()。
- var account1 = new Account
- {
- AccountHolder = "Robert Dewey",
- Transactions = new HashSet<Transaction>()
- {
- new Transaction
- {
- TransactionDate = Convert.ToDateTime("07/05/2013"),
- Amount = 104.00M
- },
- new Transaction
- {
- TransactionDate = Convert.ToDateTime("07/12/2013"),
- Amount = 104.00M
- },
- new Transaction
- {
- TransactionDate = Convert.ToDateTime("07/19/2013"),
- Amount = 104.00M
- }
- }
- };
- var account2 = new Account
- {
- AccountHolder = "James Cheatham",
- Transactions = new List<Transaction>
- {
- new Transaction
- {
- TransactionDate = Convert.ToDateTime("08/01/2013"),
- Amount = 900.00M
- },
- new Transaction
- {
- TransactionDate = Convert.ToDateTime("08/02/2013"),
- Amount = -42.00M
- }
- }
- };
- var account3 = new Account
- {
- AccountHolder = "Thurston Howe",
- Transactions = new List<Transaction>
- {
- new Transaction
- {
- TransactionDate = Convert.ToDateTime("08/05/2013"),
- Amount = 100.00M
- }
- }
- };
- context.Accounts.Add(account1);
- context.Accounts.Add(account2);
- context.Accounts.Add(account3);
- context.SaveChanges();
- //为每个account添加每月的服务费 使用异步
- foreach (var account in context.Accounts)
- {
- var transactions = new List<Transaction>
- {
- new Transaction
- {
- TransactionDate = Convert.ToDateTime("08/09/2013"),
- Amount = -5.00M
- },
- new Transaction
- {
- TransactionDate = Convert.ToDateTime("08/09/2013"),
- Amount = -2.00M
- }
- };
-
- Task saveTask = SaveAccountTransactionsAsync(account.AccountNumber, transactions);
-
- Console.WriteLine("Account Transactions for the account belonging to {0} (acct# {1})", account.AccountHolder, account.AccountNumber);
- await saveTask;
- await ShowAccountTransactionsAsync(account.AccountNumber);
- }
-
异步方法
- private static async Task SaveAccountTransactionsAsync(int accountNumber, ICollection<Transaction> transactions)
- {
- using (var context = new Recipe9Context())
- {
- var account = new Account { AccountNumber = accountNumber };
- context.Accounts.Attach(account);
- context.Entry(account).Collection(a => a.Transactions).Load();
-
- foreach (var transaction in transactions.OrderBy(t => t.TransactionDate))
- {
- account.Transactions.Add(transaction);
- }
-
- await context.SaveChangesAsync();
-
- }
-
- }
- private static async Task ShowAccountTransactionsAsync(int accountNumber)
- {
- Console.WriteLine("TxNumber\tDate\tAmount");
- using (var context = new Recipe9Context())
- {
- var transactions = context.Transactions.Where(t => t.AccountNumber == accountNumber);
- await transactions.ForEachAsync(t => Console.WriteLine("{0}\t{1}\t{2}", t.TransactionNumber, t.TransactionDate, t.Amount));
- }
- }
-
POCO
实体框架还支持使用你自己创建的类来作为模型中的实体。术语叫做“普通公共运行时对象”(Plain Old CLR Object),通常被简单地叫做POCO,这并不意味着你的类普通而老掉牙。它仅仅是说,它不包含特定框架的引用,不需要来至第三方的代码,不实现任何第三方的专用接口,并且,它不需要别的任何程序集或者命名空间。你可以实现自己的领域对象,你会看到通过自定义的ObjectContext上下对象使它们适合模型。也就是说,凭借实体框架强大的能力,你可以选择任何架构模式。
使用POCO加载关联实体
实体框架中一共有三种不同的方法来加载或查询关联实体: Eager Loading(预先加载), Lazy Loading(延迟加载)和Explicit Loading(显式加载)。
使用Include()方法演示预先加载关联实体。
- using (var context = new EFRecipesEntities())
- {
- foreach (var venue in context.Venues.Include("Events").Include("Events.Competitors"))
- {
- Console.WriteLine("Venue: {0}", venue.Name);
- foreach (var evt in venue.Events)
- {
- Console.WriteLine("\tEvent: {0}", evt.Name);
- Console.WriteLine("\t--- Competitors ---");
- foreach (var competitor in evt.Competitors)
- {
- Console.WriteLine("\t{0}", competitor.Name);
- }
- }
- }
- }
- using (var context = new EFRecipesEntities())
- {
- foreach (var venue in context.Venues)
- {
- Console.WriteLine("Venue: {0}", venue.Name);
- context.Entry(venue).Collection(v => v.Events).Load();
- foreach (var evt in venue.Events)
- {
- Console.WriteLine("\tEvent: {0}", evt.Name);
- Console.WriteLine("\t--- Competitors ---");
- context.Entry(evt).Collection(e => e.Competitors).Load();
- foreach (var competitor in evt.Competitors)
- {
- Console.WriteLine("\t{0}", competitor.Name);
- }
- }
- }
- }
使用POCO延迟加载
启用延迟加载 ,你不需要做任何事。它是实体框架的默认行为 。
- using (var context = new EFRecipesEntities())
- {
- foreach (var ticket in context.Tickets)
- {
- Console.WriteLine(" Ticket: {0}, Total Cost: {1}",
- ticket.TicketId.ToString(),
- ticket.Violations.Sum(v => v.Amount).ToString("C"));
- foreach (var violation in ticket.Violations)
- {
- Console.WriteLine("\t{0}", violation.Description);
- }
- }
- }
当生成一个实体数据模型时,延迟加载被默认设置。导航属性默认也被标记为virtual。使用延迟加载,你不需要显式地做任何事。
POCO中使用值对象(Complex Type--也叫复合类型)属性
- using (var context = new EFRecipesEntities())
- {
- context.Employees.Add(new Employee
- {
- Name = new Name
- {
- FirstName = "Annie",
- LastName = "Oakley"
- },
- Email = "aoakley@wildwestshow.com"
- });
- context.Employees.Add(new Employee
- {
- Name = new Name
- {
- FirstName = "Bill",
- LastName = "Jordan"
- },
- Email = "BJordan@wildwestshow.com"
- });
- context.SaveChanges();
- }
- using (var context = new EFRecipesEntities())
- {
- foreach (var employee in
- context.Employees.OrderBy(e => e.Name.LastName))
- {
- Console.WriteLine("{0}, {1} email: {2}",
- employee.Name.LastName,
- employee.Name.FirstName,
- employee.Email);
- }
- }
对象变更通知
我们将所有的属性都标记为virtual,设置每个集合的类型为ICollection<T>。
这样做,主要是允许实体框架为每一个POCO实体创建一个代理,在代理类中实现变化跟踪。当我们创建一个POCO实体的实例时,实体框架会动态地创建一个派生至实体的类,这个类充当实体的代理。
可以将实体设置为 sealed 或者不包含virtual标记的属性,这样就可以避免代理的生成。
- using (var context = new EFRecipesEntities())
- {
- var donation = context.Donations.Create();
- donation.Amount = 5000M;
-
- var donor1 = context.Donors.Create();
- donor1.Name = "Jill Rosenberg";
- var donor2 = context.Donors.Create();
- donor2.Name = "Robert Hewitt";
-
- //把捐款归给jill,并保存
- donor1.Donations.Add(donation);
- context.Donors.Add(donor1);
- context.Donors.Add(donor2);
- context.SaveChanges();
-
- // 现在把捐款归给Rebert
- donation.Donor = donor2;
-
- // 报告
- foreach (var donor in context.Donors)
- {
- Console.WriteLine("{0} has given {1} donation(s)", donor.Name,
- donor.Donations.Count().ToString());
- }
- Console.WriteLine("Original Donor Id: {0}",
- context.Entry(donation).OriginalValues["DonorId"]);
- Console.WriteLine("Current Donor Id: {0}",
- context.Entry(donation).CurrentValues["DonorId"]);
- }
- public partial class Donor
- {
- public Donor()
- {
- this.Donations = new HashSet<Donation>();
- }
-
- public int DonorId { get; set; }
- public string Name { get; set; }
-
- public virtual ICollection<Donation> Donations { get; set; }
- }
- public partial class EFRecipesEntities : DbContext
- {
- public EFRecipesEntities()
- : base("name=EFRecipesEntities")
- {
- }
-
- protected override void OnModelCreating(DbModelBuilder modelBuilder)
- {
- throw new UnintentionalCodeFirstException();
- }
-
- public DbSet<Donation> Donations { get; set; }
- public DbSet<Donor> Donors { get; set; }
- }
-
实体框架使用基于快照的方法来检测POCO实体的变更。如果你在POCO实体中更改一小点代码。实体框架创建的变化跟踪代理都能让上下文保持同步。
变化跟踪给我们带来了两点好处,一个是实体框架得到变更通知,它能保持对象图的状态信息和你的POCO实体同步。
另一点是,当实体框架得到处于关系两边实体中一边的变更通知时,如果有需要,它能反映到关系的另一边。
实体框架为POCO实体类生成变化跟踪的代理需要满足如下条件。
类必须是Public的,不是abstract类,不是sealed类;
需要持久化的属性必须是virtual标记的,且实现了getter和setter;
你必须将基于集合的导航属性的类型设为ICollection<T>,它们不能是一个具体的实现类,也不能是另一个派生至ICollection<T>的接口;
获取原始对象
使用Where从句和FirstDefault()方法从数据库中获取原始对象。
获取最新添加的实体并使用Entry()方法替换它的值。
- static void RunExample()
- {
- int itemId = 0;
- using (var context = new EFRecipesEntities())
- {
- var item = new Item
- {
- Name = "Xcel Camping Tent",
- UnitPrice = 99.95M
- };
- context.Items.Add(item);
- context.SaveChanges();
-
- //为下一步保存itemId
- itemId = item.ItemId;
- Console.WriteLine("Item: {0}, UnitPrice: {1}",
- item.Name, item.UnitPrice.ToString("C"));
- }
-
- using (var context = new EFRecipesEntities())
- {
- //假设这是一个更新,我们获取的item使用一个新的价格
- var item = new Item
- {
- ItemId = itemId,
- Name = "Xcel Camping Tent",
- UnitPrice = 129.95M
- };
- var originalItem = context.Items.Where(x => x.ItemId == itemId).FirstOrDefault<Item>();
- //保存值
- context.Entry(originalItem).CurrentValues.SetValues(item);
- context.SaveChanges();
- }
- using (var context = new EFRecipesEntities())
- {
- var item = context.Items.Single();
- Console.WriteLine("Item: {0}, UnitPrice: {1}", item.Name,
- item.UnitPrice.ToString("C"));
- }
- Console.WriteLine("Enter input as exit to exit.:");
- string line = Console.ReadLine();
- if (line == "exit")
- {
- return;
- };
- }
手工同步对象图和变化跟踪器
实体框架有两种不的方法来跟踪你的对象:基于快照的变化跟踪和变化跟踪代理。
基于快照的变化跟踪
当实体框架第一次遇到一个对象时,它会为每一个属性的值在内存中生成一个快照。当一个对象从查询中返回,或者我们添加一个对象到DbSet中时,快照就被生成了。当实体框架需要知道发生了哪些变化时,它会浏览每一个对象并用他们的当前值和快照比较。这个处理过程是被一个在变化跟踪器中名为DetectChanges的方法触发的。
变化跟踪代理
变化跟踪的另一项技术是变化跟踪代理,它能使实体框架具有接收变更通知的能力。
变化跟踪代理是使用实现延迟加载时动态创建代理的机制来实现的,这个动态创建的代理,不只提供延迟加载的功能,当对象发生改变时,它还能通知上下文对象。为了使用变化跟踪代理,你创建的类必须满足,实体框架能在运行时创建一个派生至你的POCO类的动态类型,在这个类型中,它重载了每个属性。这个动态类型叫做动态代理,在重载属性中包含当属性值发生改变时,通知实体框架的逻辑。
基于快照的变化跟踪,依赖实体框架在变化发生时能执行检测。DbContext API的默认行为是,通过DbContext上的事件来自动执行检测。DetectChanges不仅仅是更新上下文的状态管理信息,这些状态管理信息能让更改持久化到数据库中,当你有一个引用类型导航属性,集合类型导航属性和外键的组合时,它还能执行关系修正。
在绝大多数情况下,DetectChanges方法足够快,不会引起性能问题。但是,如果在内存中有大量的对象或者在DbContext中接二连三地执行操作,自变更检测行为就会成为性能关注点。幸运的是,有选项可以把这个自动变更检测关闭掉,并在需要时手工调用它。但稍有不慎,可能会导致意想不到的后果。实体框架精心地为你提供了关闭自动更变检测的功能。如果在性能差的地方使用它时,你将负担起调用DetectChages方法的责任。一旦这个部分的代码执行完毕,就得通过DbContext.Configuration.AutoDetectChangesEnable标识把它启用。
- context.Configuration.AutoDetectChangesEnabled = false;
- var speaker1 = new Speaker { Name = "Karen Stanfield" };
- var talk1 = new Talk { Title = "Simulated Annealing in C#" };
- speaker1.Talks = new List<Talk> { talk1 };
-
- //关联尚未完成
- Console.WriteLine("talk1.Speaker is null: {0}",
- talk1.Speakers == null);
-
- context.Speakers.Add(speaker1);
-
- //现在关联已经修正
- Console.WriteLine("talk1.Speaker is null: {0}",
- talk1.Speakers == null);
- Console.WriteLine("Number of added entries tracked: {0}",
- context.ChangeTracker.Entries().Where(e => e.State == System.Data.Entity.EntityState.Added).Count());
- context.SaveChanges();
- //修改talk的标题
- talk1.Title = "AI with C# in 3 Easy Steps";
- Console.WriteLine("talk1's state is: {0}",
- context.Entry(talk1).State);
- context.ChangeTracker.DetectChanges();
- Console.WriteLine("talk1's state is: {0}",
- context.Entry(talk1).State);
- context.SaveChanges();
-
首先,关闭自动跟踪,创建speaker和talk实例,然后把talk添加到speaker的导航属性集合Talks中。此时,talk已经是speaker的导航属性集合Talks的一部分,但是speaker还不是talk的导航属性集合Speakers的一部分。关联中的另一边还没有被修正。
接下来,我们使用Add方法,将speaker1添加到上下文中。从输出的第二行可以看出,现在,talk的导航属性集合Speakers已经正确。实体框架已经修正了关联中的另一边。
在这里实体框架做了两件事,第一件事是它通知对象状态管理器,有三个对象被创建,虽然最终输出结果不是三个,这是因为它把多对多关联看作是一个独立关系,而不是一个单独的实体。因此,输出结果为2。这些对象分别是speaker和talk。没有多对多关联对应的对象。这是因为变化跟踪器没有返回独立关系的状态。第二件事是实体框架修正了talk中的导航属性Speakers。
当我们调用SaveChages()方法时,实体框架会使用重载版本的Savechanges。在这个方法中,我们更新属性 CreateDate和RevisedDate。在调用SaveChanges()方法之前,实体框架会调用DetectChanges()来查找发生的变更。我们重写了SaveChages()方法。
- public override int SaveChanges()
- {
- var changeSet = this.ChangeTracker.Entries().Where(e => e.Entity is Talk);
- if (changeSet != null)
- {
- foreach (var entry in changeSet.Where(c => c.State == System.Data.Entity.EntityState.Added).Select(a => a.Entity as Talk))
- {
- entry.CreateDate = DateTime.UtcNow;
- entry.RevisedDate = DateTime.UtcNow;
- }
- foreach (var entry in changeSet.Where(c => c.State == System.Data.Entity.EntityState.Modified).Select(a => a.Entity as Talk))
- {
- entry.RevisedDate = DateTime.UtcNow;
- }
- }
- return base.SaveChanges();
- }
测试领域对象
添加一个Ivalidate接口和ChangeAction枚举。
- public enum ChangeAction
- {
- Insert,
- Update,
- Delete
- }
- interface IValidate
- {
- void Validate(ChangeAction action);
- }
-
添加了类Reservation和Schedule的验证代码(实现接口IValidate)部分类
- public partial class Reservation : IValidate
- {
- public void Validate(ChangeAction action)
- {
- if (action == ChangeAction.Insert)
- {
- if (Schedule.Reservations.Count(r =>
- r.ReservationId != ReservationId &&
- r.Passenger == this.Passenger) > 0)
- throw new InvalidOperationException(
- "Reservation for the passenger already exists");
- }
- }
- }
-
- public partial class Schedule : IValidate
- {
- public void Validate(ChangeAction action)
- {
- if (action == ChangeAction.Insert)
- {
- if (ArrivalDate < DepartureDate)
- {
- throw new InvalidOperationException(
- "Arrival date cannot be before departure date");
- }
-
- if (LeavesFrom == ArrivesAt)
- {
- throw new InvalidOperationException(
- "Can't leave from and arrive at the same location");
- }
- }
- }
- }
-
EF实体标签
[Table("PictureCategory", Schema = "Chapter2")]
实体属性标签
- [key]
- [ForeignKey("PhotoId")]
- DatabaseGenerated(DatabaseGeneratedOption.None) 数据迁移才有作用么?
- 给属性配置数据生成选项DatabaseGenerated。
- 它后有三个枚举值:Identity、None和Computed。
- Identity:自增长。None:不处理。Computed:表示这一列是计算列。
EF方法总结:
Context -----ef的数据库上下文
EntityName---对应实体名称
context.Configuration.LazyLoadingEnabled = true;
context.SaveChange()---提交到数据库持久化。
context.Entry(photo).Reference(p => p.PhotographFullImage).Load();
context.EntityName.OfType<FullTimeEmployee>() 继承中查找子类
context.EntityName.Include() 预先加载
context.EntityName.Find() 会先为目标对象查询上下文。如果对象不存在,它会自动去查询底层的数据存储,Find()方法会返回一个最近添加到上下文中的实例,即使它还没有被保存到数据库中。
context.EntityName.SingleOrDefault() 在底层数据库中不存在要查找的对象,它返回NULL,保证只返回一个单独的结果
context.EntityName.AsNoTracking() 禁用指定对象的对象跟踪。没有了对象跟踪,实体框架将不在跟踪Palm Tree Club对象的改变。也不会将对象加载到上下文中。
Include()方法在单个查询中返回父对象和所有的子对象
EFCodeFirst中建立约束方法
DbModelBuilder modelBuilder
modelBuilder.Entity<T>().ToTable() 实体T映射到表
modelBuilder.Entity<T>().map<T2>() 实体T映射到T2实体,参数为映射规则 lambada表达式
.HasRequired
.HasMany
.WithOptional
映射规则
.Requires()
.HasValue()
DbFunctions内置函数
提供格式化、聚合、字符串操作、日期和数字服务。
DbFunctions.TruncateTime
linq方法
.StartsWith 开始
.Contains 包含
.OrderBy 正序排序
.Skip 跳过
.Take 显示条数
.DefaultIfEmpty() 确保左表的值