zoukankan      html  css  js  c++  java
  • 01-EF Core笔记之创建模型

    01-EF Core笔记之创建模型

    使用EF Core的第一步是创建数据模型,模型建的好,下班走的早。EF Core本身已经设置了一系列约定来帮我们快速的创建模型,例如表名、主键字段等,毕竟约定大于配置嘛。如果你想改变默认值,很简单,EF Core提供了Fluent API或Data Annotations两种方式允许我们定制数据模型。

    Fluent API 与 Data Annotations#

    FluentAPI方式和Data Annotations方式,FluentAPI是通过代码语句配置的,Data Annotations是通过特性标注配置的,FluentAPI的方式更加灵活,实现的功能也更多。优先级为:FluentAPI>Data Annotations>Conventions。

    数据标注方式比较简单,在类或字段上添加特性标注即可,对实体类型有一定的入侵。

    FluentAPI方式通过在OnModelCreating方法中添加代码逻辑来完成,也可以通过实现IEntityTypeConfiguration<T>类来完成,方式灵活,更能更加强大。

    OnModelCreating方式:

    Copy
    modelBuilder.Entity<Role>()
        .Property(m => m.RoleName)
        .IsRequired();
    

    IEntityTypeConfiguration<T>方式:

    先定义IEntityTypeConfiguration<T>的实现:

    Copy
    public class BookConfigration : IEntityTypeConfiguration<Book>
    {
        public void Configure(EntityTypeBuilder<Book> builder)
        {
            builder.HasKey(c => c.Id);
    
            builder.Property(c => c.Name)
                .HasMaxLength(100)
                .IsRequired();
        }
    }
    

    然后再OnModelCreating中添加调用:

    Copy
    //加载单个Configuration
    modelBuilder.ApplyConfiguration(new BookConfigration());
    
    //加载程序集中所有Configuration
    modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
    

    主键、备用键#

    主键与数据库概念相一致,表示作为数据行的唯一标识;备用键是与主键相对应的一个概念,备用键字段的值可以唯一标识一条数据,它对应数据库的唯一约束。

    数据标识方式只能配置主键,使用Key特性,备用键只能通过FluentAPI进行配置。

    FluentAPI方式配置的代码如下:

    Copy
    modelBuilder.Entity<Car>()
        .HasKey(c=>c.Id)    //主键
        .HasAlternateKey(c => c.LicensePlate);  //备用键
    

    备用键可以是组合键,通过FluentAPI配置如下:

    Copy
    modelBuilder.Entity<Car>()
        .HasAlternateKey(c => new { c.State, c.LicensePlate });     //组合备用键
    

    必填和选填#

    映射到数据库的必填和可空,在约定情况下,CLR中可为null的属性将被映射为数据库可空字段,不能为null的属性映射为数据库的必填字段。注意:如果CLR中属性不能为null,则无论如何配置都将为必填。

    也就是说,如果能为null,则默认都是可空字段,因此在配置时,只需要配置是否为必填即可。

    数据标注方式使用Required特性进行标注。

    FluentAPI方式代码如下:

    Copy
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
    

    最大长度#

    最大长度设置了数据库字段的长度,针对string类型、byte[]类型有效,默认情况下,EF将控制权交给数据库提供程序来决定。

    数据标注方式使用MaxLength(length)特性进行标注

    FluentAPI方式代码如下:

    Copy
    builder.Property(c => c.Name)
        .HasMaxLength(100)
        .IsRequired();
    

    排除/包含属性或类型#

    默认情况下,如果你的类型中包含一个字段,那么EF Core都会将它映射到数据库中,导航属性亦是如此。如果不想映射到数据库,需要进行配置。

    数据标注方式,使用NotMapped特性进行标注;

    FluentAPI方式使用Ignore方法,代码如下:

    Copy
    //忽略类型
    modelBuilder.Ignore<BlogMetadata>();
    
    //忽略属性
    modelBuilder.Entity<Blog>()
        .Ignore(b => b.LoadedFromDatabase);
    

    如果一个属性或类型不在实体中,但是又想包含在数据库映射中时,我们只能通过Fluent API进行配置:

    Copy
    //包含类型
    modelBuilder.Entity<AuditEntry>();      
    
    //包含属性,又叫做阴影属性,它会被映射到数据库中
    modelBuilder.Entity<Blog>()
        .Property<DateTime>("LastUpdated");
    

    阴影属性#

    阴影属性指的是在实体中未定义的属性,而在EF Core中模型中为该实体类型定义的属性,这些类型只能通过变更跟踪器进行维护。

    阴影属性的定义:

    Copy
    modelBuilder.Entity<Blog>().Property<DateTime>("LastUpdated");
    

    为阴影属性赋值:

    Copy
    context.Entry(myBlog).Property("LastUpdated").CurrentValue = DateTime.Now;
    

    查询时使用阴影属性:

    Copy
    var blogs = context.Blogs
        .OrderBy(b => EF.Property<DateTime>(b, "LastUpdated"));
    

    索引#

    索引是用来提高查询效率的,在EF Core中,索引的定义仅支持FluentAPI方式。

    FluentAPI方式代码:

    Copy
    modelBuilder.Entity<Blog>()
        .HasIndex(b => b.Url);
    

    可以配合唯一约束创建索引:

    Copy
    modelBuilder.Entity<Blog>()
        .HasIndex(b => b.Url)
        .IsUnique();
    

    EF支持复合索引:

    Copy
    modelBuilder.Entity<Person>()
        .HasIndex(p => new { p.FirstName, p.LastName });
    

    并发控制#

    EF Core支持乐观的并发控制,何谓乐观的并发控制呢?原理大致是数据库中每行数据包含一个并发令牌字段,对改行数据的更新都会出发令牌的改变,在发生并行更新时,系统会判断令牌是否匹配,如果不匹配则认为数据已发生变更,此时会抛出异常,造成更新失败。使用乐观的并发控制可提高数据库性能。

    按照约定,EF Core不会设置任何并发控制的令牌字段,但是我们可以通过Fluent API或数据标注进行配置。

    数据标注使用ConcurrencyCheck特性标注。除此之外,将数据库字段标记为Timestamp,则会被认为是RowVersion,也能起到并发控制的功能。

    Copy
    public class Blog
    {
        public int BlogId { get; set; }
    
        [ConcurrencyCheck]
        public string Url { get; set; }
        
        [Timestamp]
        public byte[] Timestamp { get; set; }
    }
    

    FluentAPI 方式代码如下:

    Copy
    //并发控制令牌
    modelBuilder.Entity<Person>()
        .Property(p => p.LastName)
        .IsConcurrencyToken();
    
    //行版本号
    modelBuilder.Entity<Blog>()
        .Property(p => p.Timestamp)
        .IsRowVersion();
    

    实体之间的关系#

    实体之间的关系,可以参照数据库设计的关系来理解。EF是实体框架,它的实体会映射到关系型数据库中。所以通过关系型数据库的表之间的关系更容易理解实体的关系。

    在数据库中,数据表之间的关系可以分为一对一、一对多、多对多三种,在实体之间同样有这三种关系,但是EF Core仅支持一对一、一对多关系,如果要实现多对多关系,则需要通过关系实体进行关联。

    一对一的关系#

    以下面的实体关系为例:

    Copy
    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
    
        public BlogImage BlogImage { get; set; }
    }
    
    public class BlogImage
    {
        public int BlogImageId { get; set; }
        public byte[] Image { get; set; }
        public string Caption { get; set; }
    
        public int BlogForeignKey { get; set; }
        public Blog Blog { get; set; }
    }
    

    每一个Blog对应一个BlogImage,通过Blog可以加载到对应的BlogImage对象,对应的数据库配置如下:

    Copy
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .HasOne(p => p.BlogImage)
            .WithOne(i => i.Blog)
            .HasForeignKey<BlogImage>(b => b.BlogForeignKey);
    }
    

    一对多的关系#

    以下面的实体对象为例:

    Copy
    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
    
        public List<Post> Posts { get; set; }
    }
    
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
        public Blog Blog { get; set; }
    }
    

    每个Blog对应多个Post,而每个Post对应一个Blog,对应的数据库配置如下:

    Copy
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>()
            .HasOne(p => p.Blog)
            .WithMany(b => b.Posts)
            .IsRequired();
    }
    

    多对多的关系#

    多对多的关系需要我们定义一个关系表来完成。例如下面的实体对象:

    Copy
    public class Post
    {
        public int PostId { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
    
        public List<PostTag> PostTags { get; set; }
    }
    
    public class Tag
    {
        public string TagId { get; set; }
    
        public List<PostTag> PostTags { get; set; }
    }
    
    public class PostTag
    {
        public int PostId { get; set; }
        public Post Post { get; set; }
    
        public string TagId { get; set; }
        public Tag Tag { get; set; }
    }
    

    Blog和Tag是多对多的关系,显然无论在Blog或Tag中定义外键都不合适,此时就需要一张关系表来进行关联,这张表就是BlogTag表。对应的关系配置如下:

    Copy
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<PostTag>()
            .HasKey(pt => new { pt.PostId, pt.TagId });
    
        modelBuilder.Entity<PostTag>()
            .HasOne(pt => pt.Post)
            .WithMany(p => p.PostTags)
            .HasForeignKey(pt => pt.PostId);
    
        modelBuilder.Entity<PostTag>()
            .HasOne(pt => pt.Tag)
            .WithMany(t => t.PostTags)
            .HasForeignKey(pt => pt.TagId);
    }
    
    

    生成的值#

    这个功能我没有试验成功。按照官方文档,定义如下实体:

    Copy
    public class Book
    {
        [Key]
        public Guid Id { get; set; }
    
        [MaxLength(100)]
        public string Name { get; set; }
    
        public decimal Price { get; set; }
    
        public DateTime CreateTime { get; set; }
    }
    

    然后定义DateTime值生成器:

    Copy
    public class DateTimeGenerator : ValueGenerator<DateTime>
    {
        public override bool GeneratesTemporaryValues => true;
    
        public override DateTime Next(EntityEntry entry) => DateTime.Now;
    }
    

    最后在FluentAPI中进行配置:

    Copy
    builder.Property(c => c.CreateTime)
        .HasValueGenerator<DateTimeGenerator>()
        .ValueGeneratedOnAddOrUpdate();
    

    按照我的理解应该可以在添加和更新时设置CreateTime的值,并自动保存到数据库,但是值仅在Context中生成,无法保存到数据库中。或许是我理解的不对,后续再进行研究。

    继承#

    关于继承关系如何在数据库中呈现,目前有三种常见的模式:

    • TPH(table-per-hierarchy):一张表存放基类和子类的所有列,使用discriminator列区分类型,目前EF Core仅支持该模式
    • TPT(table-per-type ):基类和子类不在同一个表中,子类对应的表中仅包含基类表的主键和基类扩展的字段,目前EF Core不支持该模式
    • TPC(table-per-concrete-type):基类和子类不在同一个表中,子类中包含基类的所有字段,目前EF Core不支持该模式

    EF Core仅支持TPH模式,基类和子类数据将存储在同一个表中。当发现有继承关系时,EF Core会自动维护一个名为Discriminator的阴影属性,我们可以设置该字段的属性:

    Copy
    modelBuilder.Entity<Blog>()
        .Property("Discriminator")
        .HasMaxLength(200);
    

    EF Core允许我们通过FluentAPI的方式自定义鉴别器的列名和每个类对应的值:

    Copy
    modelBuilder.Entity<Blog>()
        .HasDiscriminator<string>("blog_type")
        .HasValue<Blog>("blog_base")
        .HasValue<RssBlog>("blog_rss");
    

    查询类型#

    查询类型很有用,EF Core不会对它进行跟踪,也不允许新增、修改和删除操作,但是在映射到视图、查询对象、Sql语句查询、只读库的表等情况下用到。

    例如创建视图:

    Copy
    db.Database.ExecuteSqlCommand(
        @"CREATE VIEW View_BlogPostCounts AS 
            SELECT b.Name, Count(p.PostId) as PostCount 
            FROM Blogs b
            JOIN Posts p on p.BlogId = b.BlogId
            GROUP BY b.Name");
    

    对应的查询视图:

    Copy
    public class BlogPostsCount
    {
        public string BlogName { get; set; }
        public int PostCount { get; set; }
    }
    

    使用FluentAPI配置查询视图:

    Copy
    modelBuilder
        .Query<BlogPostsCount>().ToView("View_BlogPostCounts")
        .Property(v => v.BlogName).HasColumnName("Name");
    

    值转换#

    值转换允许在写入或读取数据时,将数据进行转换(既可以是同类型转换,例如字符串加密解密,也可以是不同类型转换,例如枚举转换为int或string等)。

    这里介绍两个概念

    • ModelClrType:模型实体的类型
    • ProviderClrType:数据库提供程序支持的类型

    举个例子,string类型,对应数据库提供程序也是string类型,而枚举类型,对数据库提供程序来说没有与它对应的类型,则需要进行转换,至于如何转换、转换成什么类型,则有值转换器(Value Converter)进行处理。

    值转换器包含两个Func表达式,用以提供ModelClrType和ProviderClrType的互相转换,例如:

    Copy
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .Entity<Rider>()
            .Property(e => e.Mount)
            .HasConversion(
                v => v.ToString(),
                v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
    }
    

    该示例代码将的值转化器提供了枚举类型到字符串的互转。这里只是为了演示,真实场景中,EF Core已经提供了枚举到字符串的转换器,我们只需要直接使用即可。

    除了使用Func表达式,我们还可以构造值转换器实例,例如:

    Copy
    var converter = new ValueConverter<EquineBeast, string>(
        v => v.ToString(),
        v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
    
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(converter);
    

    EF Core已经内置了常用的值转换器,例如字符串和枚举的转换器,我们可以直接使用:

    Copy
    var converter = new EnumToStringConverter<EquineBeast>();
    
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(converter);
    

    所有内置的值转换器都是无状态(stateless)的,所以只需要实例化一次,并在多个模型中进行使用。

    值转换器还有另外一个用法,即无需实例化转换器,只需要告诉EF Core需要使用的转换器类型即可,例如:

    Copy
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion<string>();
    

    值转换器的一些限制:

    • null值无法进行转换
    • 到目前位置还不支持一个字段到多列的转换
    • 会影响构造查询参数,如果造成了影响将会生成警告日志

    实体构造函数#

    EF Core支持实体具有有参的构造函数,默认情况下,EF Core使用无参构造函数来实例化实体对象,如果发现实体类型具有有参的构造函数,则优先使用有参的构造函数。

    使用有参构造函数需要注意:

    • 参数名应与属性的名字、类型相匹配
    • 如果参数中不具有所有字段,则在调用构造函数完成后,对未包含字段进行赋值
    • 使用懒加载时,构造函数需要能够被代理类访问到,因此需要构造函数为public或protected
    • 暂不支持在构造函数中使用导航属性

    使用构造函数时,比较好玩的是支持依赖注入,我们可以在构造函数中注入DbContextIEntityTypeILazyLoaderAction<object, string> 这几个类型。

    以上便是常用的构建模型的知识点,更多内容在用到时再进行学习。

    如果认为此文对您有帮助,别忘了支持一下哦!

    声明:本博客原创文字只代表本人工作中在某一时间内总结的观点或结论,与本人所在单位没有直接利益关系。转载时请在文章页面明显位置给出原文链接。
  • 相关阅读:
    在Ubuntu-20.04上安装Emacs-27.1
    VSCode 配置 C++ 与 python 编译环境
    cf 1557(div2)
    2021牛客暑期多校训练营8
    2021牛客暑期多校训练营7
    2021暑期cf加训3
    2021牛客暑期多校训练营6
    2021牛客暑期多校训练营5
    3ds Max基本操作
    renren-generator快速生成你搬砖需要的CRUD代码的框架
  • 原文地址:https://www.cnblogs.com/zbliao/p/12931437.html
Copyright © 2011-2022 走看看