0. 写在前面
本篇文章虽说是入门学习,但是也不会循规蹈矩地把EF1.0版本一直到现在即将到来的EF Core 2.0版本相关的所有历史和细节完完整整还原出来。在后文中,笔者会直接进入正题,所以这篇文章仍然还是需要一定的EF ORM基础。
对于纯新手用户,不妨先去看看文末链接中一些优秀博客,笔者当初也是从这些博客起家,也从中得到了巨大的帮助。当然了,官方教程同样至关重要,笔者之前也贡献过部分EF CORE 官方文档资料(基本都是勘误,逃…),本篇文章中很多内容都是撷取自官方的英文文档和示例。
下文示例中将使用Visual Studio自带的Local Sql Server作为演示数据库进行演示,不过可以放心的是,大部分示例都能流畅地在各种关系型数据库中实现运行,前提是更换不同的DATABASE PROVIDERS,如NPGSQL,MYSQL等。
对于未涉及到的知识点(CLI工具,Shadow Property,Logging,从Exsiting Database反向工程生成Context等),只能说笔者最近一直在忙着毕业收尾的事情,有空的时候会把草稿整理下在博文中贴出的,一晃四年,终于也要毕业了。
1. 建立运行环境
新建一个APS.NET CORE WEB模板项目
安装相关Nuget包
//Sql Server Database ProviderInstall-Package Microsoft.EntityFrameworkCore.SqlServer
//提供熟悉的Add-Migration,Update-Database等Powershell命令,不区分关系型数据库类型Install-Package Microsoft.EntityFrameworkCore.Tools
public class MyContext : DbContext{ public MyContext(DbContextOptions<MyContext> options):base(options) {
} protected override void OnModelCreating(ModelBuilder modelBuilder) {
}
}
public void ConfigureServices(IServiceCollection services)
{ // Add framework services.
services.AddMvc();
services.AddDbContext<MyContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
}
// 需要在appsettings.json中新增一个ConnectionStrings节点,用于存放连接字符串。"ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-WebApplication4;Trusted_Connection=True;MultipleActiveResultSets=true"
},
Add-Migration InitializeUpdate-Database
2. 添加实体和映射数据库
使用EF CORE中添加实体,约束属性和关系,最后将其映射到数据库中的方式有两种,一种是Data Annotations,另一种是Fluent Api,这两种方式并没有优劣之分,全凭开发者喜好和需求,不过相对而言,Fluent Api提供的功能更多。
1. 准备工作
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; }
}public class AuditEntry{ public int AuditEntryId { get; set; } public string Username { get; set; } public string Action { get; set; }
}
2. Data Annotations
//在自定义的MyContext中添加以下三行代码public DbSet<Blog> Blogs { get; set; }public DbSet<Post> Posts { get; set; }public DbSet<AuditEntry> AuditEntries { get; set; }//添加Table特性,第一个属性代表数据库表名称[Table("Blogs")]public class Blog{
[Key] public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; }
}
//BLOG TableCREATE TABLE [dbo].[Blogs] (
[BlogId] INT IDENTITY (1, 1) NOT NULL,
[Url] NVARCHAR (MAX) NULL, CONSTRAINT [PK_Blogs] PRIMARY KEY CLUSTERED ([BlogId] ASC)
);CREATE TABLE [dbo].[Posts] (
[PostId] INT IDENTITY (1, 1) NOT NULL,
[BlogId] INT NULL,
[Content] NVARCHAR (MAX) NULL,
[Title] NVARCHAR (MAX) NULL, CONSTRAINT [PK_Posts] PRIMARY KEY CLUSTERED ([PostId] ASC), CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [dbo].[Blogs] ([BlogId])
);
//POST Table
GOCREATE NONCLUSTERED INDEX [IX_Posts_BlogId] ON [dbo].[Posts]([BlogId] ASC);
3. Fluent Api
Fluent Api俗名流式接口,其实就是C#中的扩展接口形式而已,大家日常应该接触过很多了。还记得我们第一步中MyContext定义的OnModelCreating方法吗,Fluent Api就是在那里面使用的
modelBuilder.Entity<Blog>().ToTable("Blogs").HasKey(c=>c.BlogId);
modelBuilder.Entity<Post>().ToTable("Posts").HasKey(c=>c.PostId);
modelBuilder.Entity<AuditEntry>().ToTable("AuditEntries").HasKey(c=>c.AuditEntryId);
无论是使用DbSet< TEntity >的形式抑或是使用modelBuilder.Entity< TEntity >的形式都能将定义的实体映射到数据库中,下文也会继续做出说明。
3. 包含和排除实体类型
将实体在Context中映射到数据库有多种方式:
如以下代码所示。Blog实体包含对Post实体的引用,而独立的AuditEntry则可以在OnModelCreating方法中进行配置。
class MyContext : DbContext{ public DbSet<Blog> Blogs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<AuditEntry>();
}
}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; }
}public class AuditEntry{ public int AuditEntryId { get; set; } public string Username { get; set; } public string Action { get; set; }
}
以上内容指的是“包含实体”的操作,“排除实体”操作也十分地简便。通过以下两种配置方式,在运行了迁移命令后,BlogMetadata实体是不会映射到数据库中的。
1. Data Annotations [NotMapped] 排除实体和属性
public class Blog{ public int BlogId { get; set; } public string Url { get; set; } public BlogMetadata Metadata { get; set; }
}
[NotMapped]public class BlogMetadata{ public DateTime LoadedFromDatabase { get; set; }
}
2. Fluent API [Ignore] 排除实体和属性
class MyContext : DbContext{ public DbSet<Blog> Blogs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Ignore<BlogMetadata>();
}
}public class Blog{ public int BlogId { get; set; } public string Url { get; set; } public BlogMetadata Metadata { get; set; }
}public class BlogMetadata{ public DateTime LoadedFromDatabase { get; set; }
}
NotMapped特性不仅可以用在实体类上,也可以用在指定的属性上。而在Fluent Api中,对应的操作则是
modelBuilder.Entity<Blog>().Ignore(b => b.LoadedFromDatabase);
4. 列名称和类型映射
Property方法对应数据库中的Column。
默认情况下,我们不需要更改任何实体中包含的属性名,EF CORE会自动地根据属性名称映射到数据库中的列名。当开发者需要进行自定义修改名称时( 比如每种关系型数据库的命名规则不一样,虽然笔者一直喜欢使用帕斯卡命名以保持和项目代码结构中的统一),可以使用以下的方式。
少数的几个CLR类型在不做处理的情况下,映射到数据库中时将存在可空选项,如string,int?,这种情况也在下列方式中做了说明。其中Data Annotations对应[Required]特性,Fluent API对应IsRequired方法。
1. Data Annotations
Column特性可用于属性上,它接收多个参数,其中比较重要的是Name和TypeName,前者表示数据库表映射的列名,后者表示数据类型和格式。假如不指定Url属性上的[Column(TypeName="varchar(200)")]
,数据库Blog表的Url列默认数据格式将为varchar(max)
。
public class Blog{
[Column("blog_id")] public int BlogId { get; set; }
[Required]
[Column(TypeName = "varchar(200)")] public string Url { get; set; }
}
2. Fluent API
class MyContext : DbContext{ public DbSet<Blog> Blogs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<Blog>()
.Property(b => b.BlogId)
.HasColumnName("blog_id");
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.HasColumnType("varchar(200)")
.IsRequired();
}
}public class Blog{ public int BlogId { get; set; } public string Url { get; set; }
}
由于各种关系型数据库对于数据类型的名称有所区别,所以自定义数据类型时,一定要参阅目标数据库的数据类型定义。比如PostgreSql支持Json格式,那么就需要添加以下代码——builder.Entity().Property(b => b.SomeStringProperty).HasColumnType("jsonb");
5. 主键
默认情况下,EF CORE会将实体中命名为Id或者[TypeName]Id的属性映射为数据库表中的主键。当然有些开发者不喜欢将主键命名为Id,EF CORE也提供了两种方式进行主键的相关设置。
1. Data Annotations [Key]
Data Annotations方式不支持定义外键名称,主键配置如下代码所示。
class Car{
[Key] public string LicensePlate { get; set; } public string Make { get; set; } public string Model { get; set; }
}
2. Fluent API [HasKey]
Fluent Api方式中的HasKey方法可以将属性映射为主键,对于复合主键(多个属性组合而成的主键标识)也可以很容易地进行表示。
class MyContext : DbContext{ public DbSet<Car> Cars { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<Car>()
.HasKey(c => c.LicensePlate);
//复合主键
//modelBuilder.Entity<Car>()
//.HasKey(c => new { c.State, c.LicensePlate });
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.BlogId)
.HasConstraintName("ForeignKey_Post_Blog");
}
}class Car{ public string State { get; set; } public string LicensePlate { get; set; } public string Make { get; set; } public string Model { get; set; }
}