问题:

    有一个关联到其自身的表,使用Code-First方法将其建模为一个自引用关联的实体。

解决方案:

    数据库图表:

    

Code-First方法建模方法如下:

    1、在项目中添加一个命名为EF6RecipesContext的类,该类派生于DbContext类,需要事先添加EF框架,然后再类中添加using System.Data.Entity;语句。如果没有添加EF框架,需要使用NuGet工具添加EF框架,添加完成后,EF框架会自动配置除连接字符串外的其他EF框架相关信息。然后手动添加连接字符串。附连接字符串模板:

    更快捷的方式是添加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
 Subcategories { get; set; }        public PictureCategory()        {            Subcategories = new List
();        }    }

    跟书上原来的代码不同的地方是: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
 PictureCategories { get; set; }                public EF6RecipesContext():base("name=EF6CodeFirstRecipesContext"){ }        protected override void OnModelCreating(DbModelBuilder modelBuilder)        {            base.OnModelCreating(modelBuilder);            modelBuilder.Entity
()                .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
()                .HasMany(cat => cat.Subcategories)                .WithOptional(cat => cat.ParentCategory);

改为:

                    modelBuilder.Entity
()                                .ToTable("Chapter2.PictureCategory")                .HasMany(cat => cat.Subcategories)                .WithOptional(cat => cat.ParentCategory);

然后删掉__MigrationHistory表或使用Migration命令。最后执行代码程序就可以了。