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方式:

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

IEntityTypeConfiguration<T>方式:

先定義IEntityTypeConfiguration<T>的實現:

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中添加調用:

//加載單個Configuration
modelBuilder.ApplyConfiguration(new BookConfigration());

//加載程序集中所有Configuration
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);

主鍵、備用鍵

主鍵與數據庫概念相一致,表示作為數據行的唯一標識;備用鍵是與主鍵相對應的一個概念,備用鍵字段的值可以唯一標識一條數據,它對應數據庫的唯一約束。

數據標識方式只能配置主鍵,使用Key特性,備用鍵只能通過FluentAPI進行配置。

FluentAPI方式配置的代碼如下:

modelBuilder.Entity<Car>()
    .HasKey(c=>c.Id)    //主鍵
    .HasAlternateKey(c => c.LicensePlate);  //備用鍵

備用鍵可以是組合鍵,通過FluentAPI配置如下:

modelBuilder.Entity<Car>()
    .HasAlternateKey(c => new { c.State, c.LicensePlate });     //組合備用鍵

必填和選填

映射到數據庫的必填和可空,在約定情況下,CLR中可為null的屬性將被映射為數據庫可空字段,不能為null的屬性映射為數據庫的必填字段。注意:如果CLR中屬性不能為null,則無論如何配置都將為必填。

也就是說,如果能為null,則默認都是可空字段,因此在配置時,只需要配置是否為必填即可。

數據標注方式使用Required特性進行標注。

FluentAPI方式代碼如下:

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

最大長度

最大長度設置了數據庫字段的長度,針對string類型、byte[]類型有效,默認情況下,EF將控制權交給數據庫提供程序來決定。

數據標注方式使用MaxLength(length)特性進行標注

FluentAPI方式代碼如下:

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

排除/包含屬性或類型

默認情況下,如果你的類型中包含一個字段,那么EF Core都會將它映射到數據庫中,導航屬性亦是如此。如果不想映射到數據庫,需要進行配置。

數據標注方式,使用NotMapped特性進行標注;

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

//忽略類型
modelBuilder.Ignore<BlogMetadata>();

//忽略屬性
modelBuilder.Entity<Blog>()
    .Ignore(b => b.LoadedFromDatabase);

如果一個屬性或類型不在實體中,但是又想包含在數據庫映射中時,我們只能通過Fluent API進行配置:

//包含類型
modelBuilder.Entity<AuditEntry>();      

//包含屬性,又叫做陰影屬性,它會被映射到數據庫中
modelBuilder.Entity<Blog>()
    .Property<DateTime>("LastUpdated");

陰影屬性

陰影屬性指的是在實體中未定義的屬性,而在EF Core中模型中為該實體類型定義的屬性,這些類型只能通過變更跟蹤器進行維護。

陰影屬性的定義:

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

為陰影屬性賦值:

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

查詢時使用陰影屬性:

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

索引

索引是用來提高查詢效率的,在EF Core中,索引的定義僅支持FluentAPI方式。

FluentAPI方式代碼:

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

可以配合唯一約束創建索引:

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

EF支持復合索引:

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

並發控制

EF Core支持樂觀的並發控制,何謂樂觀的並發控制呢?原理大致是數據庫中每行數據包含一個並發令牌字段,對改行數據的更新都會出發令牌的改變,在發生並行更新時,系統會判斷令牌是否匹配,如果不匹配則認為數據已發生變更,此時會拋出異常,造成更新失敗。使用樂觀的並發控制可提高數據庫性能。

按照約定,EF Core不會設置任何並發控制的令牌字段,但是我們可以通過Fluent API或數據標注進行配置。

數據標注使用ConcurrencyCheck特性標注。除此之外,將數據庫字段標記為Timestamp,則會被認為是RowVersion,也能起到並發控制的功能。

public class Blog
{
    public int BlogId { get; set; }

    [ConcurrencyCheck]
    public string Url { get; set; }
    
    [Timestamp]
    public byte[] Timestamp { get; set; }
}

FluentAPI 方式代碼如下:

//並發控制令牌
modelBuilder.Entity<Person>()
    .Property(p => p.LastName)
    .IsConcurrencyToken();

//行版本號
modelBuilder.Entity<Blog>()
    .Property(p => p.Timestamp)
    .IsRowVersion();

實體之間的關系

實體之間的關系,可以參照數據庫設計的關系來理解。EF是實體框架,它的實體會映射到關系型數據庫中。所以通過關系型數據庫的表之間的關系更容易理解實體的關系。

在數據庫中,數據表之間的關系可以分為一對一、一對多、多對多三種,在實體之間同樣有這三種關系,但是EF Core僅支持一對一、一對多關系,如果要實現多對多關系,則需要通過關系實體進行關聯。

一對一的關系

以下面的實體關系為例:

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對象,對應的數據庫配置如下:

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

一對多的關系

以下面的實體對象為例:

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,對應的數據庫配置如下:

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

多對多的關系

多對多的關系需要我們定義一個關系表來完成。例如下面的實體對象:

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表。對應的關系配置如下:

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

生成的值

這個功能我沒有試驗成功。按照官方文檔,定義如下實體:

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值生成器:

public class DateTimeGenerator : ValueGenerator<DateTime>
{
    public override bool GeneratesTemporaryValues => true;

    public override DateTime Next(EntityEntry entry) => DateTime.Now;
}

最后在FluentAPI中進行配置:

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的陰影屬性,我們可以設置該字段的屬性:

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

EF Core允許我們通過FluentAPI的方式自定義鑒別器的列名和每個類對應的值:

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

查詢類型

查詢類型很有用,EF Core不會對它進行跟蹤,也不允許新增、修改和刪除操作,但是在映射到視圖、查詢對象、Sql語句查詢、只讀庫的表等情況下用到。

例如創建視圖:

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

對應的查詢視圖:

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

使用FluentAPI配置查詢視圖:

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

值轉換

值轉換允許在寫入或讀取數據時,將數據進行轉換(既可以是同類型轉換,例如字符串加密解密,也可以是不同類型轉換,例如枚舉轉換為int或string等)。

這里介紹兩個概念

  • ModelClrType:模型實體的類型
  • ProviderClrType:數據庫提供程序支持的類型

舉個例子,string類型,對應數據庫提供程序也是string類型,而枚舉類型,對數據庫提供程序來說沒有與它對應的類型,則需要進行轉換,至於如何轉換、轉換成什么類型,則有值轉換器(Value Converter)進行處理。

值轉換器包含兩個Func表達式,用以提供ModelClrType和ProviderClrType的互相轉換,例如:

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表達式,我們還可以構造值轉換器實例,例如:

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已經內置了常用的值轉換器,例如字符串和枚舉的轉換器,我們可以直接使用:

var converter = new EnumToStringConverter<EquineBeast>();

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

所有內置的值轉換器都是無狀態(stateless)的,所以只需要實例化一次,並在多個模型中進行使用。

值轉換器還有另外一個用法,即無需實例化轉換器,只需要告訴EF Core需要使用的轉換器類型即可,例如:

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

值轉換器的一些限制:

  • null值無法進行轉換
  • 到目前位置還不支持一個字段到多列的轉換
  • 會影響構造查詢參數,如果造成了影響將會生成警告日志

實體構造函數

EF Core支持實體具有有參的構造函數,默認情況下,EF Core使用無參構造函數來實例化實體對象,如果發現實體類型具有有參的構造函數,則優先使用有參的構造函數。

使用有參構造函數需要注意:

  • 參數名應與屬性的名字、類型相匹配
  • 如果參數中不具有所有字段,則在調用構造函數完成后,對未包含字段進行賦值
  • 使用懶加載時,構造函數需要能夠被代理類訪問到,因此需要構造函數為public或protected
  • 暫不支持在構造函數中使用導航屬性

使用構造函數時,比較好玩的是支持依賴注入,我們可以在構造函數中注入DbContextIEntityTypeILazyLoaderAction<object, string> 這幾個類型。

以上便是常用的構建模型的知識點,更多內容在用到時再進行學習。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM