EFCore 实体的配置
约定大于配置:如果开发过程中基于框架默认的规则,就可以省去很多配置工作,如果默认的配置满足不了实际开发中的需求,再进行更精细的配置。常用的默认规则有:
1:表名采用DbContext中的对应的Dbset<T>的属性名。如下图,如果没有在Config类中执行表名,EF Core就会默认用这些DbSet<T>属性的名字;
2:数据表列的名字采用实体类属性的名字,列的数据类型采用和实体类属性类型最兼容的类型,没有在Config类中设置长度或者不能为空时,框架会自动给max或者可为空。
3:数据表列的可空性取决于对应实体类属性的可空性。
4:名字为Id的属性为主键,如果主键为short,int 或者long类型,则默认采用自增字段,如果主键为Guid类型则默认采用默认的Guid生成机制生成主键值
两种配置方式
-
Data Annotation
把配置以特性(Annotation)的形式标注在实体类的定义中,这种方式的优点是简单,不需要写太多代码,缺点就是程序耦合性太高,不利于修改;如下所示
-
Fluent Api
通过在对应Config类中以代码的方式,利用EntityTypeBuilder来进行配置:优点就是程序耦合度降低了,可以灵活的修改,缺点是比较复杂
-
推荐用FluentApi 的方式,
因为更具灵活性,比如我需要根据不同的数据库类型,来设置不同的字段长度,我就可以在Config类中增加if else 判断,来设置不同的字段长度。
-
常用FluentApi
- 视图与实体类映射:builder.ToView("blogsView")如果数据库中不是表,而是自定义的 视图,则就需要用到ToView方法,
- 排除属性映射:builder.lgnore(b => b. Name2);如果我们不需要将类的属性映射到数据库表的字段时,我们可以使用Ignore方法将此属性排除,不与数据库发生关系
- 配置列名:builder.Property(b=>b.BlogId).HasColumnName("blog_id");
- 配置列数据类型:builder.Property(e =>e.Title).HasColumnType("varchar(200)")
- 配置主键 默认把名字为Id或者“实体类型+Id“的属性作为主键,也可以用HasKey(来配置其他属性作为主键。builder.HasKey(c =>c.Number);
- 生成列的值 builder.Property(b =>b.Number).ValueGeneratedOnAdd();
- 可以用HasDefaultValue(为属性设定默认值builder.Property(b =>b.Age).HasDefaultValue(6);
- 索引
- builder.HasIndex(b => b.Url);
- 复合索引builder.HasIndex(p => new { p.FirstName,p.LastName });
- 唯一索引:IsUnique();
- 聚集索引:IsClustered();
-
EF Core 主键设置
- EF Core支持多种主键生成策略: 自动增长; Guid; Hi/Lo算法等。
- 自动增长。优点:简单;缺点:数据库迁移以及分布式系统中比较麻烦;并发性能差。long、int等类型主键,默认是自增。因为是数据库生成的值,所以SaveChanges后会自动把主键的值更新到Ia属性。试验一下。场景:插入帖子后,自动重定向帖子地址。
- 自增字段的代码中不能为Id赋值,必须保持默认值0.否则运行的时候就会报错。
- 在SQLserver数据库中,不要把Guid主键作为聚集索引,在Mysql中,插入频繁的表不要用Guid主键
-
通过代码查看EFCore生成的sql语句
方式1.
在继承自DBContext的类中生成一个LoggerFactory,然后在OnConfiguration重载方法中增加 optionsBuilder.UseLoggerFactory(loggerFactory);就可以实现将EFCore底层生成的SQL语句打印在控制台中(根据项目需要,如果需要打印在日志中,则LoggerFactory中添加生成到文件的方法即可)
方式2.
利用DBContextOptionsBuilder.LogTo()方法输出简单日志
方式3.
通过查询出的结果集的ToQueryString()方法查看 , 我们在使用查询Linq语句的where方式时,返回值是IQueryable<T>类型的结果集,它包含了一个ToQueryString()方法,可以打印出执行的SQL语句。(注意:该方法只能针对查询方法能获取SQL语句)
static async Task Main(string[] args)
{
using (MyDBContext dBContext = new MyDBContext())
{
//查询
IQueryable<Book> books = dBContext.Books.Where(b => b.Author == "liyumin");
foreach (var book in books)
{
Console.WriteLine(book.ToString());
}
Console.WriteLine(books.ToQueryString());
}
}
-
同样的C#语句在不同的数据库中被EF Core翻译成不同的SQL语句
比如取前三行数据,SQLserver,Mysql ,oracle三种数据库的语句各不相同
SqlServer : select top(3) * from t;
Mysql: select * from t limit 3;
Oracle : select * from t where rownum<=3;
- EF Core 产生的数据迁移脚本适合数据库类型相关的,不同把与A类型的数据库产生的Migration数据库脚本用到与B类型数据库的对接中使用,如下,生成的Migration脚本都有很强的Mysql标记,对于其他类型数据库是使用不了的。
EFCore 一对多关系配置
实际项目中,我们很少会只对一张表进行查询的操作,一般都是要关联多张业务表进行数据的查询,EFCore 不仅支持单实体类的操作,更支持多实体的关系操作。
-
一对多的配置,
- 例如一个篮球俱乐部队有多个球员,俱乐部和球员就是一对多的关系,建立相关实体类如下:
/// <summary>
/// 篮球队
/// </summary>
public class BasketBallTeam
{
/// <summary>
/// 主键Id
/// </summary>
public int Id { get; set; }
/// <summary>
/// 球队代码
/// </summary>
public string TeamCode { get; set; }
/// <summary>
/// 球队名称
/// </summary>
public string TeamName { get; set; }
/// <summary>
/// 球队总经理
/// </summary>
public string TeamManager { get; set; }
/// <summary>
/// 球队所在城市
/// </summary>
public string TeamCity { get; set; }
/// <summary>
/// 旗下所有球员
/// </summary>
public List<BasketBallPlayer> players { get; set; } = new List<BasketBallPlayer>();
}
// <summary>
/// 篮球运动员
/// </summary>
public class BasketBallPlayer
{
public int Id { get; set; }
/// <summary>
/// 球员姓名
/// </summary>
public string PlayerName { get; set; }
/// <summary>
/// 球员场上位置
/// </summary>
public string PlayerPosition { get; set; }
/// <summary>
/// 所属球队
/// </summary>
public BasketBallTeam team { get; set; }
-
配置一对多的关系
应用HasXXX().WithXXX()方法进行配置,
- 一对多:HasOne(...).WithMany(...);
- 一对一:HasOne(...).WithOne(...);
- 多对多:HasMany(...).WithMany(...);
当前例子中,我们是以一个球队对应多个球员的逻辑,那么就可以HasOne(球队).WithMany(球员),两表的关系配置,既可以配置在一端的Config类,也可以配置在多端的Config类,这里的端指的是继承了IEntityTypeConfiguration<T>的类:
我们这里在球员端配置一对多的关系;
public class BasketBallPlayerConfig : IEntityTypeConfiguration<BasketBallPlayer>
{
public void Configure(EntityTypeBuilder<BasketBallPlayer> builder)
{
builder.ToTable("BasketBall_Player");
builder.Property(e => e.PlayerName).HasMaxLength(200).IsRequired();
//进行一对多的配置
builder.HasOne(c => c.team).WithMany(p => p.players).IsRequired();
}
}
然后我们执行数据库Migration迁移命令,生成球队表和球员表如下:可以看到,EFCore自动在球员表中增加了一个teamId字段,作为球员和球队关联的字段,也就是对应球队Team表中的Id字段的值。
我们通过代码的形式往两个表中插入数据,然后运行代码,看数据是否正常插入
static async Task Main(string[] args)
{
using (MyDBContext dBContext = new MyDBContext())
{
BasketBallTeam team = new BasketBallTeam()
{
TeamCode = "NBA020",
TeamCity = "圣安东尼奥",
TeamName = "马刺队",
TeamManager = "RC-布福德",
};
team.players.Add(new BasketBallPlayer()
{
PlayerName = "文班亚马",
PlayerPosition = "中锋",
});
team.players.Add(new BasketBallPlayer()
{
PlayerName = "克里斯保罗",
PlayerPosition = "控卫",
});
team.players.Add(new BasketBallPlayer()
{
PlayerName = "索汉",
PlayerPosition = "小前锋",
});
dBContext.basketBallTeams.Add(team);
await dBContext.SaveChangesAsync();
}
}
-
一对多关联数据的获取
利用QueryableExtensions扩展类的Include方法,可以将一对多关联的关系数据查询出来:
static async Task Main(string[] args)
{
using (MyDBContext dBContext = new MyDBContext())
{
BasketBallTeam team = dBContext.basketBallTeams.Include(t => t.players).Single(p => p.Id == 1);
Console.WriteLine(team);
Console.WriteLine("--------------------");
foreach (var player in team.players)
{
Console.WriteLine(player);
}
}
}
我们把生成的SQL语句打印在控制台,可以看到使用了Left Join左连接查询的方式:
-
额外的外键字段
上面我们用到的Include方法,用在关联查询时,会将两张关联表的所有字段全部查询出来,但是在实际项目开发中,我们大部分时间只需要查询关联表中的部分字段出来使用,如果还使用include方法,就会造成查询数据的冗余:
1.我们给BasketBallPlayer表单独增加一个teamId属性,用来与team表就行关联
2.然后再Config类中配置HasForeignKey(c=>c.teamId),指定这个属性为外键
3.然后查询的时候,不需要使用Include方法,直接通过球员Id查询,即可带出球队Id,可以看出,查询的语句也没有了left join关键字,避免了数据的冗余;
4.我们还可以利用Linq的Select投影机制,让EF Core只查询我们需要的playName字段:
var ballPlayer = dBContext.BasketBallPlayers
.Select(d => new { PlayerName = d.PlayerName, TeamId = d.TeamId })
.Single(p => p.PlayerName == "文班亚马");
Console.WriteLine("球员:" + ballPlayer.PlayerName + ",球队Id:" + ballPlayer.TeamId);
-
EF Core 单向导航属性
前面我们在处理一对多的关系时,在Team表和Player表中都创建了一个指向关联表的实体类或者实体类集合的一个数据,这种在双方都创建属性的机制叫做双向导航属性。
但是在实际的业务中,我们往往会遇到这种情况,表A的一些字段要频繁的和其他业务表产生数据关联,比如一个用户表User,其他很多业务表中都有类似于操作员的字段,这个操作员是和用户表有关联的,这种情况下,如果在User表实体类中针对每个与它有关系的业务表都新增一个属性与之对应,那显然不太实际。此时我们只需在其他业务表中定义指向User的类作为属性即可,这种就叫做单向导航属性。
- 我们新建两个类,一个User类,一个下单的订单类Order,Order类中包含两个字段指向User表的,一个是下单人OrderCreator,一个是订单审核人OrderApprover
public class User
{
/// <summary>
/// 用户Id
/// </summary>
public int Id { get; set; }
/// <summary>
/// 用户姓名
/// </summary>
public string UserName { get; set; }
}
public class Order
{
/// <summary>
/// 订单号
/// </summary>
public int OrderId { get; set; }
/// <summary>
/// 订单内容
/// </summary>
public string OrderContent { get; set; }
/// <summary>
/// 下单人
/// </summary>
public User OrderCreator { get; set; }
/// <summary>
/// 订单审核人
/// </summary>
public User OrderApprover { get; set; }
public override string ToString()
{
return $"订单编号:{OrderId},订单内容:{OrderContent},下单人:{OrderCreator.UserName},审核人{OrderApprover.UserName}";
}
}
2.对实体类新增Config实体配置类
因为是配置单向导航属性,所以我们只需要在OrderConfig类中配置对应关系,并且WithMany()方法不用设置参数
public class UserConfig : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("T_Users");
}
}
public class OrderConfig : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("T_Orders");
builder.HasOne(o => o.OrderCreator).WithMany().IsRequired();
builder.HasOne(o => o.OrderApprover).WithMany().IsRequired();
}
}
3.执行数据库迁移命令,将表生成到数据库中
4.插入相应的数据
static async Task Main(string[] args)
{
using (MyDBContext dBContext = new MyDBContext())
{
User userKobe = new(){UserName="kobe"};
User userJordon = new(){UserName = "Jordon"};
Order order = new Order()
{
OrderContent = "下单商品",
OrderCreator = userKobe,
OrderApprover = userJordon
};
dBContext.orders.Add(order);
await dBContext.SaveChangesAsync();
}
}
5.通过EFCore查询语句查询对应的数据
Order order = dBContext.orders.Include(o=>o.OrderCreator)
.Include(r=>r.OrderApprover)
.Single(p => p.OrderId == 1);
Console.WriteLine(order);
-
EF Core自引用结构树
在我们实际的项目开发的业务中,会遇到这种实体类,它的上级只有一个,它的下级有多个的场景,比如省市区区域字典这种
1.像这种实体在EFCore中就可以定义为自引用树的实体类,在该种类的定义中,包含一个定义为父类的属性,和一个定义为子节点集合的一个属性,如下代码的定义:
public class AreaDict
{
/// <summary>
/// 区域Id
/// </summary>
public int AreaId {get;set;}
/// <summary>
/// 区域名称
/// </summary>
public string AreaName { get; set; }
/// <summary>
/// 区域级别
/// </summary>
public int AreaLevel { get; set; }
/// <summary>
/// 父级区域信息
/// </summary>
public AreaDict ParentArea { get; set; }
/// <summary>
/// 子节点区域
/// </summary>
public List<AreaDict> ChildAreas { get; set; }
}
2.在实体类对应的Config类中定义对应的引用关系
public class AreaDictConfig : IEntityTypeConfiguration<AreaDict>
{
public void Configure(EntityTypeBuilder<AreaDict> builder)
{
builder.ToTable("T_Area_Dicts");
builder.HasKey(e => e.AreaId);
//设置与父级和子级的关联关系
builder.HasOne(a => a.ParentArea).WithMany(r => r.ChildAreas) ;
}
}
3.执行数据库迁移命令,查看数据库表是否建立
4.使用EFCore 语句插入一些数据:
static async Task Main(string[] args)
{
using (MyDBContext dBContext = new MyDBContext())
{
//省级
AreaDict province = new AreaDict()
{
AreaName = "江西省",
AreaLevel = 2,
ParentArea = null,
ChildAreas = new List<AreaDict>()
};
//地级市
AreaDict cityNanchang = new AreaDict()
{
AreaName = "南昌市",
AreaLevel = 3,
ParentArea = province,
ChildAreas = new List<AreaDict>()
};
AreaDict cityJiujiang = new AreaDict()
{
AreaName = "九江市",
AreaLevel = 3,
ParentArea = province,
ChildAreas = new List<AreaDict>()
};
AreaDict cityYichun = new AreaDict()
{
AreaName = "宜春市",
AreaLevel = 3,
ParentArea = province,
ChildAreas = new List<AreaDict>()
};
province.ChildAreas.Add(cityNanchang);
province.ChildAreas.Add(cityJiujiang);
province.ChildAreas.Add(cityYichun);
//县级市
AreaDict cityNanchangxian = new AreaDict()
{
AreaName = "南昌县",
AreaLevel = 4,
ParentArea = cityNanchang,
};
AreaDict cityLushan = new AreaDict()
{
AreaName = "庐山市",
AreaLevel = 4,
ParentArea = cityJiujiang
};
AreaDict cityZhangshushi = new AreaDict()
{
AreaName = "樟树市",
AreaLevel = 4,
ParentArea = cityYichun
};
cityNanchang.ChildAreas.Add(cityNanchangxian);
cityJiujiang.ChildAreas.Add(cityLushan);
cityYichun.ChildAreas.Add(cityZhangshushi);
await dBContext.areaDicts.AddAsync(province);//把book对象加入Books这个逻辑的表里面
await dBContext.SaveChangesAsync();//update-database
}
}
可以看出,数据按照对应的父子级关系存到了数据库中
- 我们来根据区域级别进行缩进的打印
static async Task Main(string[] args)
{
using (MyDBContext dBContext = new MyDBContext())
{
List<AreaDict> areaDicts = dBContext.areaDicts.ToList();
PrintArea(2, areaDicts,null);
}
}
/// <summary>
/// 递归打印
/// </summary>
/// <param name="areaLevel"></param>
/// <param name="areaDicts"></param>
/// <param name="parent"></param>
static void PrintArea(int areaLevel, List<AreaDict> areaDicts,AreaDict parent)
{
List<AreaDict> childAreas = areaDicts.Where(a => a.ParentArea == parent).ToList();
foreach (AreaDict area in childAreas)
{
Console.WriteLine(new String('-', area.AreaLevel) +area.AreaName);
if (area.ChildAreas is object && area.ChildAreas.Count > 0)
{
PrintArea(area.AreaLevel + 1, area.ChildAreas,area);
}
}
}
打印结果为: