使用 Include()
方法, 该方法在using Microsoft.EntityFrameworkCore;
命名空间下
// 联合查询 老师和学生表
var list = await db.Teacher.Include(s => s.Students).ToListAsync();
主键:Key
自增:DatabaseGenerated( DatabaseGeneratedOption.Identity)
not null
:Required
自定义类型:Column(TypeName ="nvarchar(20)")
最大值:MaxLength(200)
MaxLength特性指定了属性的值所允许的最大值,然后在数据库中就生成相应列的最大值
不映射字段:NotMapped
不需要映射成数据库中的列的时候,可以使用特性。(默认情况下,EF为实体的每个属性映射数据列。 【必须包含get;
和set;
】。NotMapped
特性重写了这个约定。EF不会为没有get;
和set;
的属性,创建列)
设置索引:Index(string name, Properties:[IsClustered = bool],[IsUnique= bool])
用来在特定的列上面创建索引,默认情况下,索引的名称是IX_{属性的名称}
。
name
:索引名称
IsClustered
: 是否是建聚合索引
IsUnique
:是否是唯一索引
外键: [ForeignKey(name string)]
name:主表的列名
public class Student
{
public int StudentID { get; set; }
public string StudentName { get; set; }
[ForeignKey("TeacherId")]
public Teacher teacher{ get; set; }
}
public class Teacher
{
public int TeacherId { get; set; }
public string TeacherName { get; set; }
}
可在派生上下文中替代
OnModelCreating
方法,并使用ModelBuilder API
来配置模型。 此配置方法最为有效,并可在不修改实体类的情况下指定配置。 Fluent API 配置具有最高优先级,并将替代约定和数据注释。
Config
类,用于配合该实体;IEntityTypeConfiguration
接口,并实现接口成员 Configure
方法 EntityTypeBuilder builde
对象配置 T
的实体约束 Fluent API
的详情查看官网 https://docs.microsoft.com/zh-cn/ef/core/modeling/示例:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace EFCore
{
// 实体
public class Student
{
public int Id { get; set; }
///
/// 名称
///
public string StudentName { get; set; }
///
/// 生日
///
public DateTime? Birthday { get; set; }
///
/// 成绩
///
// public List Success { get; set; } = new List();
}
// 实体配置类
public class SuccessConfig : IEntityTypeConfiguration<Success>
{
public void Configure(EntityTypeBuilder<Success> builder)
{
builder.ToTable("Success");//创建数据表
builder.Property(s => s.Score).IsRequired();// 配置字段的约束
//builder.Property(s=>s.StudentId).HasOne()
// 生成外键关系
builder.HasOne(s => s.Student)
.WithMany()
.HasForeignKey(s=>s.StudentId);
}
}
}
注:实际开发中,实体和配置类是分开的,这里只是举例说明
在数据库上下文中实现配置
// 重写 OnModelCreating 方法,配置实体的映射
protected override void OnModelCreating(ModelBuilder modelBuilder) {
// 调用父类方法
base.OnModelCreating(modelBuilder);
// 应用程序集中的配置(实现 FluentAPI 的配置)
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
// user实体
namespace WebApplication9
{
public class User
{
public string Email { get; set; }
public string Name { get; set; }
public string Password { get; set; }
public string Password2 { get; set; }
}
}
// fluent api Validation
namespace WebApplication9
{
public class UserValidation:AbstractValidator<User>
{
public UserValidation() {
RuleFor(u => u.Email)
.NotNull().WithMessage("邮箱不能为空")
.Must(e => e.EndsWith("@qq.com") || e.EndsWith("@163.com")).WithMessage("邮箱格式不正确");
RuleFor(u => u.Name)
.NotNull().WithMessage("名称不能为空")
.Length(1, 4).WithMessage("名称长度要在1-4直接");
RuleFor(u => u.Password).NotNull();
RuleFor(u => u.Password2).Equal(p => p.Password).WithMessage(u => $"{u.Password}于{u.Password2}不一致")
.NotNull();
}
}
}
// 配置 service
services.AddFluentValidation(fx=> {
Assembly assembly = Assembly.Load("WebApplication9");
fx.RegisterValidatorsFromAssembly(assembly); // 加载指定程序集中的 验证配置
// fx.RegisterValidatorsFromAssemblies() 加载多个程序集
});
在数据库中,我们通常将主键设置为自增
Guid 是他自己生成的 id 由小写数字和小写字母组成的 32位字符串,是一个不会重复的主键
注:MySQL 中的
IndexDB
不能使用Guid
,因为该数据库强制要求主键为聚集索引
Guid guid = Guid.NewGuid();
HiLo是High Low
的简写,翻译成中文叫高低位模式。
HiLo是由“Hi”和“Lo”两部分生成主键的一种模式。“Hi”部分来自数据库,“Lo”部分在内存中生成以创建唯一值。请记住,“Lo”是一个范围数字,如0-100。因此,当“Hi”部分用完“Lo”范围时,再次进行数据库调用以获得下一个“Hi数字”。所以HiLo模式的优点在于您预先可以知道主键的值,而不用每次都与数库据发生交互。
总结有以下四点:
“Hi”部分由数据库分配,两个并发请求保证得到唯一的连续值;
一旦获取“Hi”部分,我们还需要知道“incrementSize
”的值(“Lo”条目的数量);
“Lo”取的范围:[0,incrementSize];
标识范围的公式是:(Hi - 1) * incrementSize) + 1 到 (Hi - 1) * incrementSize) + incrementSize)
当所有“Lo”值使用完时,需要重新从数据库中取出一个新的“Hi”值,并将“Lo”部分重置为0。
Fluent API中的配置
// 生成外键关系
builder.HasOne(s => s.Student)
.WithMany()
.HasForeignKey(s=>s.StudentId);
EF Core中的对应关系主要通过以下几个方法进行配置(以下方法也可以没有泛型):
HasOne()
:一个对几个HasMany()
:多个对几个WithOne()
:几个对一个WithMany()
:几个对多个这四个方法实现了一对一,一对多,多对多的复杂关系映射
HasOne()
和WithMany()
示例(文章和评论):一张文章有多条评论
// 实体
///
/// 文章
///
public class Article
{
public int Id { get; set; }
///
/// 标题
///
public string Title { get; set; }
///
/// 内容
///
public string Text { get; set; }
///
/// 评论
///
public List<Comment> Comment { get; set; } = new List<Comment>();
}
///
/// 评论
///
public class Comment
{
public int Id { get; set; }
///
/// 评论内容
///
public string Context { get; set; }
///
/// 文章id(后来加的) 这里有点问题,如果一开始生成数据库的话,会生成两个ArticleId,这个属性是我生成数据库之后添加的,生成数据库之后添加数据库不会再生成
///
//public int ArticleId { get; set; }
///
/// 文章
///
public Article Article { get; set; }
}
// Fluent API配置
//Article 文章配置
public class ArticleConfig : IEntityTypeConfiguration<Article>
{
public void Configure(EntityTypeBuilder<Article> builder)
{
builder.ToTable("Article");
builder.Property(a => a.Title).HasMaxLength(50).IsRequired();
builder.Property(a => a.Text);
}
}
//comment 评论配置
public class CommentConfig : IEntityTypeConfiguration<Comment>
{
public void Configure(EntityTypeBuilder<Comment> builder)
{
builder.ToTable("Comment");
builder.Property(c => c.Context).IsRequired();
//配置关系:Comment的Article 对应 Article的Comment
builder.HasOne<Article>(a => a.Article).WithMany(c=>c.Comment);
}
}
注:这个示例中“一”中包含了多个评论List
,这个是根据实际情况设置的;例如:员工--离职单, 请假单,采购单
…的关系,要是还像示例中写,“一”中会包含很多“多
”,这是不可取的,所以具体情况,具体分析
HasOne()
和WithOne()
示例(订单和快递单):一个订单对应一个快递单号
//实体
///
/// 订单
///
public class Order
{
public int Id { get; set; }
///
/// 商品名称
///
public string GoodsName { get; set; }
///
/// 快递的导航属性
///
public Express Express { get; set; }
}
///
/// 快递
///
public class Express
{
public int Id { get; set; }
///
/// 快递名称
///
public string ExpressName { get; set; }
///
/// 订单Id---用于设置和订单表的外键
///
public int OrderId { get; set; }
///
/// 订单导航属性
///
public Order Order { get; set; }
}
// Fluent API配置
//订单
public class OrderConfig : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("Order");
builder.Property(o => o.GoodsName).HasMaxLength(50).IsRequired();
}
}
//快递
public class ExpressConfig : IEntityTypeConfiguration<Express>
{
public void Configure(EntityTypeBuilder<Express> builder)
{
builder.ToTable("Express");
builder.Property(e => e.ExpressName).HasMaxLength(20).IsRequired();
// 一对一关系配置 注意外键的配置 和 泛型的使用
builder.HasOne<Order>(e => e.Order).WithOne(o => o.Express).HasForeignKey<Express>(e => e.OrderId);
}
HasMany()
和WithMany()
UsingEntity()
方法,具体使用看示例(不使用的话EF会为我们自动创建表)示例(老师和学生):多个老师对应多个学生
// 实体
public class Student
{
public int Id { get; set; }
public string StudentName { get; set; }
// 都需要list确认关系
public List<Teacher> Teachers { get; set; } = new List<Teacher>();
}
public class Teacher
{
public int Id { get; set; }
public string TeacherName { get; set; }
// 都需要list确认关系
public List<Student> Students { get; set; } = new List<Student>();
}
// Fluent API配置
// 学生FluentAPI配置
public class StudentConfig : IEntityTypeConfiguration<Student>
{
public void Configure(EntityTypeBuilder<Student> builder)
{
builder.ToTable("Student");
builder.Property(s => s.StudentName).HasMaxLength(10).IsRequired();
// 配置多对多关系
// UsingEntity(j => j.ToTable("Student_Teacher")) 相当于又创建了一个表,
// 是用于连接 student 和 teacher 的关系的表,使用的是联合主键(student表的Id和teacher表的Id)
builder.HasMany(s => s.Teachers).WithMany(t => t.Students).UsingEntity(j => j.ToTable("Student_Teacher"));
}
}
// 老师FluentAPI配置
public class TeacherConfig : IEntityTypeConfiguration<Teacher>
{
public void Configure(EntityTypeBuilder<Teacher> builder)
{
builder.ToTable("Teacher");
builder.Property(t => t.TeacherName).HasMaxLength(10).IsRequired();
}
}
HasOne()
和WithMany()
Parent
)和List
类型的类型(Children
);就是需要一个自身的父节点和自己的若干个子节点示例(地球上国家地区的分类):地球是根节点,下面有各个洲,各个洲有各个国家
// 实体
///
/// 地区类 地球-->州-->国家
///
// 自连接的关系:一个节点有且只有一个父节点,单有多个子节点;根节点只有子节点
public class Address
{
public int Id { get; set; }
public string Name { get; set; }
///
/// 父节点
///
public Address Parent { get; set; }
///
/// 子节点
///
public List<Address> Children { get; set; } = new List<Address>();
//public bool IsDelete { get; set; }
}
// Fluent API配置
public class AddressConfig : IEntityTypeConfiguration<Address>
{
public void Configure(EntityTypeBuilder<Address> builder)
{
builder.ToTable("Address");
builder.Property(a => a.Name).HasMaxLength(50).IsRequired();
// 注意这里是一对多的关系
builder.HasOne(a => a.Parent).WithMany(a=>a.Children);
// 软删除 全局过滤
// builder.HasQueryFilter(a => a.IsDelete == false);
}
}
具体问题具体分析
IEnumerable
公开枚举器,该枚举器支持在指定类型的集合上进行简单迭代。也就是说:实现了此接口的
object
,就可以直接使用foreach
遍历此object
;
IEnumerable
和IEnumerable
接口在 .NET 中是非常重要的接口,它允许开发人员定义foreach语句功能的实现并支持非泛型方法的简单的迭代,IEnumerable
和IEnumerable
接口是 .NET Framework 中最基本的集合访问器。它定义了一组扩展方法,用来对数据集合中的元素进行遍历、过滤、排序、搜索等操作。
IEnumerable
接口是非常的简单,只包含一个抽象的方法Get IEnumerator(T)
,它返回一个可用于循环访问集合的IEnumerator
对象。
EF Core中的IEnumerable
IEnumerable
类型时,EF不会在数据库中过滤筛选条件,而是直接select * from 表
拿到所有数据放到内存中进行筛选,他的底层操作是ADO.NET中断开式连接,一次性将拿到所有数据。IEnumerable
接口。**当遇见复杂的筛选过滤时,数据库压力过大,可以使用该方法,返回IEnumerable
类型的Linq
表达式中是可以使用方法进行过滤的。具体问题具体分析注意:只有遍历的时候,才会执行IEnumerable的方法
IQueryable
它继承
IEnumerable
接口,而因为.net版本加入Linq
和IQueryable
后,使得IEnumerable
不再那么单调,变得更加强大和丰富。
IQueryable
接口类型时,不会立即查询,只有碰到遍历或者遇见终结方法时才会执行,例如 tolist,toarray,count,max,min......
一个方法的返回值为IQueryable
时,就不是终结方法 (简单理解)IQueryable
接口并不会直接的从数据库中查询数据,所有我们可以通过条件对Linq
进行二次(多次)拼接,已达到符合我们的查询需求。**当满足需求后执行遍历或者终结方法获取我们所需要的数据。**例如:多条件的筛选查询等…ADO.NET
中的连接式访问,在查询过程中会议在连接数据库;这样会节省系统内存,但是如果处理慢会占用数据库连接,使用tolist,toarray
等方法直接将数据加载到内存中IEnumerable
和IQueryable
的区别IEnumerable
的过滤,排序等操作,都是在内存中发生的。也就是说数据已经从数据库中获取到了内存中,只是在内存中进行过滤和排序操作。IQueryable
的过滤,排序等操作,只有在数据真正用到的时候才会到数据库中查询。
FormattableString
对象是用于参数化插值的,一般使用$
符号表示;
$
参数化插值原理:他会将字符串中要插入的参数的位置,放一个占位符,将插入的数据转换成参数,填到指定的占位符;
作用:当我们拼接SQL语句时,使用
$
插值的方式,可以防止SQL注入注意:
$
不是字符串拼接
EF执行SQL语句的三种方式:
//-------1.执行非查询语句操作
db.Database.ExecuteSqlInterpolated($"sql语句");// 执行非查询语句操作
//-------2.执行查询操作
db.Address.FromSqlInterpolated($"sql语句").ToList(); // 执行查询操作 基于dbset执行(非立即执行,需要终结方法)
//-------3.使用ADO.NET 太复杂,不推荐,可以使用 Dapper
DbConnection conn= db.Database.GetDbConnection();// 拿到context对应的底层connection
// 判断连接是否打开
if (conn.State!=System.Data.ConnectionState.Open)
{
conn.Open();// 打开连接
}
// 创建Command对象
using (var comm=conn.CreateCommand())
{
comm.CommandText = "sql语句";
using (var reader=comm.ExecuteReader())// 持续连接式访问
{
while (reader.Read())// 读取数据
{
int id = reader.GetInt32(0);// 拿到第一列的数据转换成int类型
string name = reader.GetString(1);// 拿到第二列的数据转换成string类型
Console.WriteLine($"{id}:{name}");
}
}
}
一个实体对象只要和数据库上下文中有相关的操作,EF都会对其进行跟踪
首次跟踪一个实体的时候,EF Core会创建这个实体的快照,执行
SaveChanges()
等方法时,EF Core将会把存储的快照中的值于实体的当前值进行比较
SaveChanges()
识别为什么样的状态,就对数据库执行什么样的操作
实体的五种状态
Add
:SaveChanges()
识别为 插入Unchanged
:SaveChanges()
识别为 忽略Modified
:SaveChanges()
识别为 修改Deleted
:SaveChanges()
识别为 删除Detached
:SaveChanges()
识别为 忽略EntityEntry e1 = db.Entry(address); // 查询状态
string ss=e1.DebugView.LongView;// 快照信息
//AsNoTracking() 禁用跟踪 可以不让EF Core 跟踪他的改变;在只有显示数据的时候可以使用,降低内存的占用
db.Address.AsNoTracking().ToList();
通过修改实体的状态,可直接SaveChanges()
对数据库进行操作,不需要查询
Address aa = new Address { Id = 36, Name = "日本" };
var ee = db.Entry(aa);
ee.Property("Name").IsModified = true;
db.SaveChanges();
给表设置一个特殊字段 ,通过该字段的状态来进行软删除,
删除表的数据时,只需要将特殊字段的状态改为
false
,对数据查询时筛选一下就行了软删除有可能会影响性能,根据需要可以创建联合索引
HasQueryFilter()
:配置全局过滤,所有生成的查询语句后面都会自动添加该方法的条件IgnoreQueryFilters()
:屏蔽全局过滤,使用时会将全局过滤屏蔽// 软删除 全局过滤 在Fluent API中配置
builder.HasQueryFilter(a => a.IsDelete == false);// 这样每个生成的sql查询的后面都会跟一个IsDelete=false 的条件判断,通过判断该字段来判断是否展示该数据,从而实现软删除
// 忽略全局过滤器
db.Address.IgnoreQueryFilters().ToList();// 当使用时会将全局过滤屏蔽
使用悲观锁,将正在操作的数据锁住,只允许“我”来访问,当“我”访问结束解锁后;其他用户才能访问。
开启悲观锁 :将
with(updlock)
放到表后面 ;例如select * from 表 WITH(UPDLOCK) where id=1
优点:简单
缺点:不适用于高并发场景;操作时间过长时,容易发生阻塞;使用不当,会造成死锁
注:尽量使用最小锁的级别(锁的级别:行锁,表锁,页锁)
示例(租房子):当一个用户抢到后,其他用户就不能强了
static void Main(string[] args)
{
Console.WriteLine("请输入租客名称:");
string name = Console.ReadLine();
using (MyDBContext db=new MyDBContext())
{
using (var tran=db.Database.BeginTransaction())// 开启事务
{
// with(updlock) 开启悲观锁
var room = db.Room.FromSqlInterpolated($"select * from room WITH(UPDLOCK) where id=1").SingleOrDefault();
Thread.Sleep(5000);// 等待5秒
if (room.UserName==null)
{
Console.WriteLine($"恭喜您,抢房成功");
room.UserName = name;
}
else
{
Console.WriteLine($"抱歉,房间已被{room.UserName}抢到");
}
db.SaveChanges(); // 关闭悲观锁
tran.Commit();// 提交事务
}
}
Console.ReadKey();
}
使用乐观控制并发;有两种方式(并发令牌):控制单个字段的 新、旧 值(Owner);控制整行的版本
Owner
控制单个字段的 新、旧 值
:通过判定行中的指定字段的 旧值 是否和“我”认为的 旧值 是否相等,若不相等,说明存在并发操作,且修改报错并返回错误(DbUpdateConcurrencyException ex
)
Owner
字段并发操作就是修改判断操作;并发操作时,就会对并发字段进行筛选,如果没有找的符合筛选条件的数据,说明该字段已被修改(并发操作),那么数据库将不会做任何操作(sql执行返回行数为0
),这时EF 会报DbUpdateConcurrencyException
的错误,通过try-actch
来对报错进行处理就可以了。update 表 set Owner='新' where id=1 and Owner=旧 ---并发修改令牌
Fluent API
配置:需要设置并发字段为 IsConcurrencyToken();
DbUpdateConcurrencyException
对象,可以拿到现在数据库中新的行数据(对象)注:Owner
只是别名
// Fluent API配置
builder.Property(r => r.Owner).IsConcurrencyToken();// 设置为并发令牌,适用于单个字段
static void Main(string[] args)
{
Console.WriteLine("请输入租客名称:");
string name = Console.ReadLine();
using (MyDBContext db=new MyDBContext())
{
using (var tran=db.Database.BeginTransaction())// 开启事务
{
var room = db.Room.FromSqlInterpolated($"select * from room where id=1").SingleOrDefault();
Thread.Sleep(5000);// 等待5秒
if (room.UserName==null)
{
Console.WriteLine($"恭喜您,抢房成功");
room.UserName = name;
}
else
{
Console.WriteLine($"抱歉,房间已被{room.UserName}抢到");
}
//------------乐观并发控制重点-------
try
{
db.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.First();
var dbValues = entry.GetDatabaseValues();
// 可以拿到数据中最新的room对象类型的RoomName属性值(导致“我”失败的原因)
string newValue = dbValues.GetValue<string>(nameof(room.RoomName));
}
//-----------------------
tran.Commit();// 提交事务
}
}
Console.ReadKey();
}
RowVersion
控制整行的版本:通过判断行的“版本号”,来判断该行是否存在修改;若“版本号”不同,说明存在并发操作,并且对该行的操作失败,返回错误(
DbUpdateConcurrencyException ex
)
timestamp
类型:SQL Server 中的数据类型字段,该字段为16进制,每一次对数据进行操作时,该类型的字段就会自动改变,且该字段不会重复,一般会被当做行的版本号
来控制数据并发操作
timestamp
类型的字段,该字段会被当做该行的版本号(并发令牌)timestamp
类型的字段进行改变(自动改变)0
);EF会返回错误DbUpdateConcurrencyException
对象,可以拿到现在数据库中新的行数据(对象)byte[]
类型IsRowVersion()
对版本属性进行设置guid
但是每次都需要手动更新guid
//Model
public class Room
{
public int Id { get; set; }
///
/// 房间名称
///
public string RoomName { get; set; }
///
/// 拥有该房间的用户名称
///
public string UserName { get; set; }
//public string Owner { get; set; } //设置并发字段 通过单个字段判断并发操作
public byte[] RowVersion { get; set; }// 指定的行版本号 行中任何字段改变,该值都会改变
}
// Fluent API配
public class RoomConfig : IEntityTypeConfiguration<Room>
{
public void Configure(EntityTypeBuilder<Room> builder)
{
builder.ToTable("Room");
builder.Property(r => r.RoomName).HasMaxLength(20);
builder.Property(r => r.UserName).HasMaxLength(20);
//builder.Property(r => r.Owner).IsConcurrencyToken();// 设置为并发令牌,适用于单个字段
builder.Property(r => r.RowVersion).IsRowVersion();// 对这个一行做任何改变,他的值就会改变,适合做并发令牌,适用于修改多个字段
// 低版本mysql 精度不够, 可以使用guid 但是每次都需要手动更新guid
}
}
// 实现
class Program
{
static void Main(string[] args)
{
Console.WriteLine("请输入租客名称:");
string name = Console.ReadLine();
using (MyDBContext db=new MyDBContext())
{
using (var tran=db.Database.BeginTransaction())// 开启事务
{
var room = db.Room.FromSqlInterpolated($"select * from room where id=1").SingleOrDefault();
Thread.Sleep(5000);// 等待5秒
if (room.UserName==null)
{
Console.WriteLine($"恭喜您,抢房成功");
room.UserName = name;
}
else
{
Console.WriteLine($"抱歉,房间已被{room.UserName}抢到");
}
try
{
db.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.First();
var dbValues = entry.GetDatabaseValues();
// 可以拿到数据中最新的room对象类型的RoomName属性值(导致“我”失败的原因)
string newValue = dbValues.GetValue<string>(nameof(room.RoomName));
}
tran.Commit();// 提交事务
}
}
Console.ReadKey();
}
}
IsConcurrencyToken()
把这个字段设置为并发令牌即可RowVersion
列,这样就不用开发者手动来在每次更新数据的时候,手动更新并发令牌的值了。充血模型 第一个是属性 第二个是回调(配置属性用的)