问题:
有一个关联到其自身的表,使用Code-First方法将其建模为一个自引用关联的实体。
解决方案:
数据库图表:
Code-First方法建模方法如下:
1、在项目中添加一个命名为EF6RecipesContext的类,该类派生于DbContext类,需要事先添加EF框架,然后再类中添加using System.Data.Entity;语句。如果没有添加EF框架,需要使用NuGet工具添加EF框架,添加完成后,EF框架会自动配置除连接字符串外的其他EF框架相关信息。然后手动添加连接字符串。附连接字符串模板:
<connectionStrings> <add name="EF6CodeFirstRecipesContext" connectionString="data source=数据库服务器地址;initial catalog=数据库名;integrated security=True;MultipleActiveResultSets=True;App=EntityFramework" providerName="System.Data.SqlClient" /> </connectionStrings>
更快捷的方式是添加ADO.NET实体数据模型,命名实体数据模型为EF6CodeFirstRecipesContext;在模型内容页面选择空代码优先模型,完成。它会自动安装EF框架,并生成一个名为EF6CodeFirstRecipesContext的继承DbContext的类;然后会自动配置应用程序的配置文件,并生成一个名为EF6CodeFirstRecipesContext的连接字符串,连接字符串需要根据项目实际情况修改数据源信息。为了同项目其他文件一致,需要修改生成的EF6CodeFirstRecipesContext类的类名为EF6RecipesContext。
2、在项目中添加一个命名为PictureCategory的类型,用于构造POCO实体。代码如下:
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; public class PictureCategory { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int CategoryId { get; private set; } public string Name { get; set; } public int? ParentCategoryId { get; private set; } [ForeignKey("ParentCategoryId")] public PictureCategory ParentCategory { get; set; } public virtual List<PictureCategory> Subcategories { get; set; } public PictureCategory() { Subcategories = new List<PictureCategory>(); } }
跟书上原来的代码不同的地方是:public virtual List<PictureCategory> Subcategories { get; set; }
原来的代码没有virtual关键字。如果没有virtual关键字,将不能输出Subcategories。具体原因参见:https://msdn.microsoft.com/zh-cn/library/dd468057(v=vs.100).aspx。
3、添加一个DbSet<PictureCategory>自动属性到EF6RecipesContext类。
4、在EF6RecipesContext类中,重新OnModelCreating方法配置PictureCategroy和Subcategories之间的双向关联。EF6RecipesContext类的最终代码如下:
using System.Data.Entity; public class EF6RecipesContext:DbContext { public DbSet<PictureCategory> PictureCategories { get; set; } public EF6RecipesContext():base("name=EF6CodeFirstRecipesContext"){ } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<PictureCategory>() .HasMany(cat => cat.Subcategories) .WithOptional(cat => cat.ParentCategory); } }
原理:
数据库关系表示为维度(degree)、多重性(multiplicity)和方向(direction)。维度是关系中参与的实体类型的数量。一元和二元关系是更一般的。三元及n元关系一般只存在于理论上。
多重性是关系各端参与关系的实体数量,多重性一般有0..1(0个或一个),1(一个)和*(多个)。
方向一般是单向和双向的。
EF支持数据库关系的特定类型,叫做关联(association)类型。关联类型是一个双向的关联,有一个一元或二元的维度,支持 0..1、1、* 3种多重性。
在这个例子中,关联是一个一元的(只有PictureCategory被涉及),有一个0..1和*的多重性,当然也是一个双向关系。
在自引用关联中经常描述一个父-子关系,每一个父类有多个子类,每个子类仅有一个父类。由于这个关系是0..1,而不是1。所以有的类没有父类,这样的类可以理解为层次结构的根节点,它是没有父亲节点的。
下面的代码从根节点递归枚举picture categroies。
static void Main(string[] args) { using (var context = new EF6RecipesContext()) { var louvre = new PictureCategory { Name = "Louvre" }; var child = new PictureCategory { Name = "Egyptian Antiquites" }; louvre.Subcategories.Add(child); child = new PictureCategory { Name = "Sculptures" }; louvre.Subcategories.Add(child); child = new PictureCategory { Name = "Paintings" }; louvre.Subcategories.Add(child); var paris = new PictureCategory { Name = "Paris" }; paris.Subcategories.Add(louvre); var vacation = new PictureCategory { Name = "Summer Vacation" }; //vacation.Subcategories.Add(paris); //context.PictureCategories.Add(vacation); paris.ParentCategory = vacation; context.PictureCategories.Add(paris); context.SaveChanges(); } using (var context = new EF6RecipesContext()) { var roots = context.PictureCategories.Where(c => c.ParentCategory == null); roots.ToList().ForEach(root => Print(root, 0)); } Console.ReadLine(); } static void Print(PictureCategory cat, int level) { StringBuilder sb = new StringBuilder(); Console.WriteLine("{0} {1}", sb.Append(' ', level), cat.Name); cat.Subcategories.ForEach(child => Print(child, level + 1)); }
运行结果如下:
上面的代码与原文也不一样,原文:
vacation.Subcategories.Add(paris); context.PictureCategories.Add(paris);
这样的话vacation的数据将不能插入到表中。上面的代码通过注释,提供了2种添加数据的方式。和上一篇文章出现的情况一样,我们在将对象保存到上下文时一定要找到数据的中心节点,上文中是OrderItem,这篇文章如果是通过vacation添加子节点,则它是中心;如果是paris添加父节点,则paris是中心。
另外还有:roots.ToList().ForEach(root => Print(root, 0));
原文为:roots.ForEach(root => Print(root, 0));
提示:IQueryable<>不支持ForEach方法。所以使用ToList方法将其转换为列表。其实这个时候查询才被执行。ToList方法有时候也被用作迫使查询执行。
最后,我们来看看后台数据库,因为我们在代码执行前并没有添加PictureCategory表,那数据被保存在哪的呢?
打开数据库,发现数据库多了__MigrationHistory和PictureCategories这2张表,默认架构为dbo。这些都是EF框架帮我们实现的。如果为了使用自定义的架构和表名呢?只需要将
modelBuilder.Entity<PictureCategory>() .HasMany(cat => cat.Subcategories) .WithOptional(cat => cat.ParentCategory);
改为:
modelBuilder.Entity<PictureCategory>() .ToTable("Chapter2.PictureCategory") .HasMany(cat => cat.Subcategories) .WithOptional(cat => cat.ParentCategory);
然后删掉__MigrationHistory表或使用Migration命令。最后执行代码程序就可以了。