zoukankan      html  css  js  c++  java
  • EntityFramework Core映射关系详解

    前言

    Hello,开始回归开始每周更新一到两篇博客,本节我们回归下EF Core基础,来讲述EF Core中到底是如何映射的,废话少说,我们开始。

    One-Many Relationship(一对多关系)

    首先我们从最简单的一对多关系说起,我们给出需要映射的两个类,一个是Blog,另外一个则是Post,如下:

        public class Blog
        {
            public int Id { get; set; }
            public int Count { get; set; }
            public string Name { get; set; }
            public string Url { get; set; }
            public IEnumerable<Post> Posts { get; set; }
    
        }
        public class Post
        {
            public virtual int Id { get; set; }
            public virtual string Title { get; set; }
            public virtual string Content { get; set; }
    
            public virtual int BlogId { get; set; }
            public virtual Blog Blog { get; set; }
        }

    此时我们从Blog来看,一个Blog下对应多个Post,而一个Post对应只属于一个Blog,此时配置关系如下:

         public class BlogMap : EntityMappingConfiguration<Blog>
        {
            public override void Map(EntityTypeBuilder<Blog> b)
            {
                b.ToTable("Blog");
                b.HasKey(k => k.Id);
    
                b.Property(p => p.Count);
                b.Property(p => p.Url);
                b.Property(p => p.Name);
                b.HasMany(p => p.Posts)
                    .WithOne(p => p.Blog)
                    .HasForeignKey(p => p.BlogId);
            }
        }

    而Post则为如下:

        public class PostMap : EntityMappingConfiguration<Post>
        {
            public override void Map(EntityTypeBuilder<Post> b)
            {
                b.ToTable("Post");
                b.HasKey(k => k.Id);
                b.Property(p => p.Title);
                b.Property(p => p.Content);
            }
        }

    此时我们利用SqlProfiler监控生成的SQL语句。如下:

    CREATE TABLE [Blog] (
        [Id] int NOT NULL IDENTITY,
        [Count] int NOT NULL,
        [Name] nvarchar(max),
        [Url] nvarchar(max),
        CONSTRAINT [PK_Blog] PRIMARY KEY ([Id])
    );
    CREATE TABLE [Post] (
        [Id] int NOT NULL IDENTITY,
        [BlogId] int NOT NULL,
        [Content] nvarchar(max),
        [Title] nvarchar(max),
        CONSTRAINT [PK_Post] PRIMARY KEY ([Id]),
        CONSTRAINT [FK_Post_Blog_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blog] ([Id]) ON DELETE CASCADE
    );

    此时我们能够很明确的看到对于Post表上的BlogId建立外键BlogId,也就是对应的Blog表上的主键即Id,同时后面给出了DELETE CASADE即进行级联删除的标识,也就是说当删除了Blog上的数据,那么此时Post表上对应的数据也会进行相应的删除。同时在生成SQL语句时,还对Post上的BlogId创建了索引,如下:

    CREATE INDEX [IX_Post_BlogId] ON [Post] ([BlogId]);

    由上知,对于一对多关系中的外键,EF Core会默认创建其索引,当然这里的索引肯定是非唯一非聚集索引,聚集索引为其主键。我们通过数据库上就可以看到,如下:

    此时即使我们不配置指定外键为BlogId同样也没毛病,如下:

    b.HasMany(m => m.Posts).WithOne(o => o.Blog);

    因为上述我们已经明确写出了BlogId,但是EF Core依然可以为其指定BlogId为外键,现在我们反过来想,要是我们将Post中的BlogId删除,同样进行上述映射是否好使呢,经过实际验证确实是可以的,如下:

    别着急下结论,我们再来看一种情况,现在我们进行如下配置并除去Post中的BlogId还是否依然好使呢?

    b.HasMany(m => m.Posts);

    经过临床认证,也是好使的,能够正确表达我们想要的效果并自动添加了外键BlogId列,所以到这里我们可以为一对多关系下个结论:

    一对多关系结论

    在一对多关系中,我们可以通过映射明确指定外键列,也可以不指定,因为EF Core内部会查找是否已经指定其外键列有则直接用指定的,没有则自动生成一个外键列,列名为外键列所在的类名+Id。同时对于一对多关系我们可以直接只使用HasMany方法来配置映射而不需要再配置HasOne或者WithOne,上述皆是从正向角度去配置映射,因为易于理解,当然反之亦然。

    One-One RelationShip (一对一关系)

    对于一对一关系和多对多关系稍微复杂一点,我们来各个击破,我们通过举例比如一个产品只属于一个分类,而一个分类下只有一个产品,如下:

        public class Product
        {
            public int Id { get; set; }
            public string Name { get; set; }
    
            public Category Category { get; set; }
        }
        public class Category
        {
            public int Id { get; set; }
            public string Name { get; set; }
    public int ProductId { get; set; }
    public Product Product { get; set; } }

    此时我们来进行一下一对一关系映射从产品角度出发:

        public class ProductMap : EntityMappingConfiguration<Product>
        {
            public override void Map(EntityTypeBuilder<Product> b)
            {
                b.ToTable("Product");
                b.HasKey(k => k.Id);
    
                b.HasOne(o => o.Category).WithOne(o => o.Product);
            }
    }

    此时我们通过 dotnet ef migrations add Initial 初始化就已经出现如下错误:

    大概意思为未明确Product和Category谁是依赖项,未明确指定导致出现上述错误。而上述对于一对多关系则不会出现如此错误,仔细分析不难发现一对多已经明确谁是主体,而对于一对一关系二者为一一对应关系,所以EF Core无法判断其主体,所以必须我们手动去指定。此时我们若进行如下指定你会发现没有lambda表达式提示:

     b.HasOne(o => o.Category)
                    .WithOne(o => o.Product)
                    .HasForeignKey(k=>k.)

    还是因为主体关系的原因,我们还是必须指定泛型参数才可以。如下所示:

     b.HasOne(o => o.Category)
                    .WithOne(o => o.Product)
                    .HasForeignKey<Category>(k => k.ProductId);

    此时在Category上创建ProductId外键,同时会对ProductId创建如下的唯一非聚集索引:

    CREATE UNIQUE INDEX [IX_Category_ProductId] ON [Category] ([ProductId]);

    Many-Many RelationShip (多对多关系)

    多对多关系在EF Core之前版本有直接使用的方法如HasMany-WithMany,但是在EF Core中则不再提供对应的方法,想想多对多关系还是可以通过一对多可以得到,比如一个产品属于多个分类,而一个分类对应多个产品,典型的多对多关系,但是通过我们的描述则完全可以通过一对多关系而映射得到,下面我们一起来看看:

        public class Product
        {
            public int Id { get; set; }
            public string Name { get; set; }
    
            public IEnumerable<ProductCategory> ProductCategorys { get; set; }
        }
        public class Category
        {
            public int Id { get; set; }
            public string Name { get; set; }
    
            public int ProductId { get; set; }
            public IEnumerable<ProductCategory> ProductCategorys { get; set; }
        }
        public class ProductCategory
        {
            public int ProductId { get; set; }
            public Product Product { get; set; }
    
            public int CategoryId { get; set; }
            public Category Category { get; set; }
        }

    上述我们将给出第三个关联类即ProductCategory,将Product(产品类)和Category(分类类)关联到ProductCategory类,最终我们通过ProductCategory来进行映射,如下:

        public class ProductCategoryMap : EntityMappingConfiguration<ProductCategory>
        {
            public override void Map(EntityTypeBuilder<ProductCategory> b)
            {
                b.ToTable("ProductCategory");
    
                b.HasKey(k => k.Id);
    
                b.HasOne(p => p.Product)
                    .WithMany(p => p.ProductCategorys)
                    .HasForeignKey(k => k.ProductId);
    
                b.HasOne(p => p.Category)
                    .WithMany(p => p.ProductCategorys)
                    .HasForeignKey(k => k.CategoryId);
            }
        }

    好了到了这里为止,关于三种映射关系我们介绍完了,是不是就此结束了,远远不是,下面我们再来其他属性映射。

    键映射

    关于键映射中的外键映射上述已经讨论过,下面我们来讲讲其他类型键的映射。

    备用键/可选键映射(HasAlternateKey)

    备用键/可选键可以为一个实体类配置除主键之外的唯一标识,比如在登录中用户名可以作为用户的唯一标识除了主键标识外,这个时候我们可以为UserName配置可选键,打个比方这样一个场景:一个用户只能购买一本书,在Book表中配置一个主键和用户Id(例子虽然不太恰当却能很好描述可选键的使用场景)

        public class Book
        {
            public int Id { get; set; }
            public string UserId { get; set; }
        }

    下面我们通过可选键来配置用户Id的映射

        public class BookMap : EntityMappingConfiguration<Book>
        {
            public override void Map(EntityTypeBuilder<Book> b)
            {
                b.ToTable("Book");
                b.HasKey(k => k.Id);
                b.HasAlternateKey(k => k.UserId);
            }
        }

    最后监控得到如下语句:

    看到没,为用户Id配置了唯一约束:

    CONSTRAINT [AK_Book_UserId] UNIQUE ([UserId])

    所以我们得出结论:通过可选键我们可以创建唯一约束来除主键之外唯一标识行。

    主体键映射(Principal Key) 

    如果我们想要一个外键引用一个属性而不是主键,此时我们可以通过主体键映射来进行配置,此时配置主体键映射背后实际上自动将其设置为一个可选键。这个就不用我们多讲了。

    好了到此为止我们讲完了键映射,接下来我们再来讲述属性映射:

    属性映射

    对于C#中string类型若我们不进行配置,那么在数据库中将默认设置为NVARCHAR并且长度为MAX且是为可空,如下:

    若我们需要设置其长度且为非空,此时需要进行如下配置:

    b.Property(p => p.Name).IsRequired().HasMaxLength(50);

    通过HaxMaxLength方法来指定最大长度,通过IsRequired方法来指定为非空。但是此时问题来了,数据库类型对于string有VARCHAR、CHAR、NCAHR类型,那么我们应当如何映射呢?比如对于VARCHAR类型,在EF Core中对于数据库列类型我们可以通过 HasColumnType 方法来进行映射,那么假设对于数据库类型为VARCHAR长度为50且为非空,我们是否可以进行如下映射呢?

             b.Property(p => p.Name)
                    .IsRequired()
                    .HasColumnType("VARCHAR")
                    .HasMaxLength(50);

    通过上述迁移出错,我们修改成如下才正确:

    b.Property(p => p.Name)
                    .IsRequired()
                    .HasColumnType("VARCHAR(50)");

    解决一个,又来一个,那么对于枚举类型我们又该进行如何映射呢,枚举对应数据库中的类型为TINYINT,我们进行如下设置:

        public class Product
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public Type Type { get; set; }
            public IEnumerable<ProductCategory> ProductCategorys { get; set; }
        }
    
        public enum Type
        {
            [Description("普通")]
            General = 0,
            [Description("保险")]
            Insurance = 1
        }
        public class ProductMap : EntityMappingConfiguration<Product>
        {
            public override void Map(EntityTypeBuilder<Product> b)
            {
                b.ToTable("Product");
                b.HasKey(k => k.Id);
    
                b.Property(p => p.Type)
                    .IsRequired()
                    .HasColumnType("TINYINT");
            }
        }

    此时则对应生成我们想要的类型:

    CREATE TABLE [Product] (
        [Id] int NOT NULL IDENTITY,
        [Name] nvarchar(max),
        [Type] TINYINT NOT NULL,
        CONSTRAINT [PK_Product] PRIMARY KEY ([Id])

    【注意】:此时将其映射成枚举没毛病上述已经演示,但是当我们获取数据时将TINYINT转换成枚举时将出现如下错误:

    说到底TINYINT对应C#中的byte类最后尝试将其转换为int才会导致转换失败,所以在定义枚举时记得将其继承自byte,如下才好使:

        public enum Type : byte
        {
            [Description("普通")]
            General = 0,
            [Description("保险")]
            Insurance = 1
        }

    讲完如上映射后,我们再来讲讲默认值映射。 当我们敲默认映射会发现有两个,一个是HasDefaultValue,一个是HasDefaultValueSql,我们一起来看看到底如何用:

    我们在Product类中添加Count字段:

     public int Count { get; set; }
    b.Property(p => p.Count).HasDefaultValue(0);

    如上是对于int类型如上设置,如果是枚举类型呢,我们来试试:

     b.Property(p => p.Type)
                    .IsRequired()
                    .HasColumnType("TINYINT").HasDefaultValue(0);

    此时迁移将出现如下错误:

    也就是说无法将枚举值设置成int类型,此时我们应该利用HasDefaultValueSql来映射:

      b.Property(p => p.Type)
                    .IsRequired()
                    .HasColumnType("TINYINT").HasDefaultValueSql("0");

    对于默认值映射总结起来就一句话:对于C#中的类型和数据库类型一致的话用HasDefaultValue,否则请用HasDefaluValueSql。

    【注意】:对于字段类型映射有一个奇葩特例,对于日期类型DateTime,在数据库中也存在其对应的类型datetime,但是如果我们不手动指定类型会默认映射成更精确的日期类型即datetime2(7)。

    我们在Product类中添加创建时间列,如下:

            public DateTime CreatedTime { get; set; }

    此时我们不指定其映射类型,此时我们看到在数据库中的类型为datetime2(7)

    当然以上映射也没什么问题,但是对于大部分对于日期类型都是映射成datetime且给定默认时间为当前时间,所以此时需要手动进行配置,如下:

    b.Property(p => p.CreatedTime)
                    .HasColumnType("DATETIME")
                    .HasDefaultValueSql("GETDATE()");

    说完默认值需要注意的问题,我们再来讲讲计算列的映射,在EF Core中对于计算列映射,在之前版本为ForSqlServerHasComputedColumnSql,目前是HasComputedColumnSql。例如如下这是计算列:

     b.Property(p => p.Name)
                    .IsRequired()
                    .HasComputedColumnSql("((N'Cnblogs'+CONVERT([CHAR](8),[CreatedTime],(112)))+RIGHT(REPLICATE(N'0',(6))+CONVERT([NVARCHAR],[Id],(0)),(6)))");

     

    其中还有关于列名自定义的方法(HasColumnName),主键是否自动生成(ValueGeneratedOnAdd)等方法以及行版本(IsRowVersion)和并发Token(IsConcurrencyToken)。还有设置索引的方法HasIndex

    这里有一个疑问对于string默认设置是为NVARCHAR,其就是unicode,不知为何还有一个IsUnicode方法,它不也是设置为NVARCHAR的吗,这是什么情况?求解,当我们同时设置IsUnicode方法和列类型为VARCHAR时,则还是会生成NVARCHAR,可见映射成NVARCHAR优先级比VARCHAR高,如下

     b.Property(p => p.Name)
                    .IsRequired().IsUnicode()
                    .HasColumnType("VARCHAR(21)")
                    .HasComputedColumnSql("((N'Cnblogs'+CONVERT([CHAR](8),[CreatedTime],(112)))+RIGHT(REPLICATE(N'0',(6))+CONVERT([NVARCHAR],[Id],(0)),(6)))");

    总结 

    本文大概就稍微讲解了EF Core中的映射以及一些稍微注意的地方,刚好今天父亲节,在此祝愿天下父母健康长寿,我们下节再会!

  • 相关阅读:
    CF1051F The Shortest Statement 题解
    CF819B Mister B and PR Shifts 题解
    HDU3686 Traffic Real Time Query System 题解
    HDU 5969 最大的位或 题解
    P3295 萌萌哒 题解
    BZOJ1854 连续攻击游戏 题解
    使用Python编写的对拍程序
    CF796C Bank Hacking 题解
    BZOJ2200 道路与航线 题解
    USACO07NOV Cow Relays G 题解
  • 原文地址:https://www.cnblogs.com/CreateMyself/p/6995403.html
Copyright © 2011-2022 走看看