使用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
- 暫不支持在構造函數中使用導航屬性
使用構造函數時,比較好玩的是支持依賴注入,我們可以在構造函數中注入DbContext
、IEntityType
、ILazyLoader
、Action<object, string>
這幾個類型。
以上便是常用的構建模型的知識點,更多內容在用到時再進行學習。