问题:

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

解决方案:

    数据库图表:

wKiom1Zep5WwXsZ2AACXKbpCau4797.png    

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));
        }

运行结果如下:

wKiom1ZfsOzCehd9AAAJTjlnPeU298.png

    上面的代码与原文也不一样,原文:

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命令。最后执行代码程序就可以了。