Entity Framework
结构
实体类
/// <summary>
/// 实体类
/// </summary>
[Table("KProduct")]
public class Product
{
/// <summary>
/// Gets or sets the category.
/// </summary>
public string Category { get; set; }
/// <summary>
/// DatabaseGeneratedOption.Computed 数据由数据库计算得到(比如存储过程,或是 GetDate)
/// </summary>
/// <value>
/// The created.
/// </value>
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime Created { get; set; }
/// <summary>
/// [TypeName] 表示类型
/// [Required] 表示不能为 null
/// [Column] 表示列名
/// [Index("ProductNameIndex", 1, IsUnique = true)] 创建索引,可以不用指定名称,如果有多个索引 ,需要指定先后顺序
/// </summary>
[Required]
[Column("ProductName", TypeName = "varchar")]
[MinLength(2)]
[MaxLength(25, ErrorMessage = "最大长度不能大于 25")]
[Index("ProductNameIndex", 1, IsUnique = true)]
[DefaultValue("xxxxxx")]
public string Name { get; set; }
/// <summary>
/// [key] 属性表示主键
/// 一个键也可以使用 id 或是类名Id 来自动设定
/// 如果主键为 int 或是 guid 则为自增长键
/// </summary>
[Key]
[Column("Id")]
public int PId { get; set; }
/// <summary>
/// Gets or sets the price.
/// </summary>
[Required]
[Column("ProductPrice", TypeName = "money")]
public int Price { get; set; }
/// <summary>
/// [NotMapped] 不需要在数据库中映射
/// </summary>
[NotMapped]
public int SPrice { get; set; }
}
模型类
public class KTStoreMode : DbContext
{
public KTStoreMode(string nameOrConnectionString)
: base(nameOrConnectionString)
{
}
public DbSet<Product> Products { get; set; }
}
在代码中动态设置连接参数
// 方案1. 作为 nameOrConnectionString 参数传入
// 实际使用发现问题, 会报 System.Data.Entity.Core.MetadataException:“无法加载指定的元数据资源。” 异常
private static string BuildDbConnection () {
var stringBuilder = new EntityConnectionStringBuilder {
Provider = @"MySql.Data.MySqlClient",
ProviderConnectionString = YxLoansDataConnectionStringBuilder.GetConnectionString (),
Metadata =
"res://*/Models.TestModel.csdl|res://*/Models.TestModel.ssdl|res://*/Models.TestModel.msl"
};
return stringBuilder.ConnectionString;
}
// 方案2
[DbConfigurationType(typeof(MySql.Data.Entity.MySqlEFConfiguration))]
public class YxDataModel : DbContext {
public YxDataModel() : this(BuildDbConnection(), true) { }
private static DbConnection BuildDbConnection() {
return new MySqlConnection(YxLoansDataConnectionStringBuilder.GetConnectionString());
}
}
Fluent API
Fluent 定义表结构的优先级最高 > 数据注释 > 惯例
public class KTStoreMode : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
var configuration = modelBuilder.Entity<Product>();
configuration.ToTable("tpProduct");
configuration.Property(p => p.PId).HasColumnName("Id").HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
configuration.Property(p => p.Name).HasColumnName("productName").HasColumnType("varchar").IsUnicode(false).HasMaxLength(25).IsRequired();
}
}
数据库结构更新
系统会自动建表,如果更新代码后可以使用工具自动更新表结构
需要关闭自动建表功能,则在构造函数中加入如下代码 Database.SetInitializer
(null); Enable-Migrations, 启用表迁移模式,新版本的 vs 好像已经自动开始该功能
Add-Migration *** 进行表迁移 *** 为迁移的动作名。会生成 ***同名的 cs 升、降级处理文件
Update-Database 对数据进行迁移
Update-Database -TargetMigration:**** 对指定名称的数据进行迁移
特别提示: 如果出现问题,把相关的属性删除, add/update 一下,数据库中会清除相关的相联
注:时间属性设置 [DefaultValue("Getdate()")] 无效 , 一个可能的办法在 DbMigration 中加入如下代码
up 函数中
AddColumn("dbo.tpProduct", "Created", c => c.DateTime(nullable: false, defaultValueSql: "GETDATE()"));
多个 dbContext 时
Enable-Migrations -ContextTypeName ModelOne.Context.ModelOneContext -MigrationsDirectory ModelOneMigrations
// ContextTypeName 指定不同的 DbContext 类
// MigrationsDirectory 指定一个不同的目录
// 目录下生成 Configuration.cs
internal sealed class Configuration : DbMigrationsConfiguration<ModelOne.Context.ModelOneContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
MigrationsDirectory = @"ModelOneMigrations"; // 这里指定配置
}
// 创建与操作时就需要指定 ConfigurationTypeName 了, 应该是配置类的全路径,为了方便可以改为不同名称,就可以仅用类名了
add-migration Initial -ConfigurationTypeName ModelOneDbConfig
update-database -ConfigurationTypeName ModelOneDbConfig
特别说明 (使用 mysql 时)
// 1. 低版本的 mysql 时可能需要添加 DbConfigurationType
[DbConfigurationType(typeof(MySql.Data.Entity.MySqlEFConfiguration))]
public class KTStoreMode : DbContext
// 2. MySql.Data.Entity.EF6 与 MySql.Data 的版本中一致;如果修改版本的话,需要重置编译一次才能使用
SQLITE 数据库操作
官方库
// .net core 6
// package Microsoft.EntityFrameworkCore.Sqlite
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite(this.dataPath);
base.OnConfiguring(optionsBuilder);
}
第三方库
添加 System.Data.SQLite (注意大小写,这里是官方版本,功能可能多一些 )
添加 SQLite.CodeFirst
App.config 中应该有如下信息,不然可能报错
<entityFramework>
<providers>
<provider invariantName="System.Data.SQLite.EF6" type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" />
<provider invariantName="System.Data.SQLite" type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" />
</providers>
</entityFramework>
<system.data>
<DbProviderFactories>
<remove invariant="System.Data.SQLite.EF6" />
<add name="SQLite Data Provider (Entity Framework 6)" invariant="System.Data.SQLite.EF6" description=".NET Framework Data Provider for SQLite (Entity Framework 6)" type="System.Data.SQLite.EF6.SQLiteProviderFactory, System.Data.SQLite.EF6" />
<remove invariant="System.Data.SQLite" /><add name="SQLite Data Provider" invariant="System.Data.SQLite" description=".NET Framework Data Provider for SQLite" type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" /></DbProviderFactories>
</system.data>
static void Main(string[] args)
{
var builder = new SQLiteConnectionStringBuilder { DataSource = dbFile };
var connection = new SQLiteConnection(builder.ConnectionString);
using (var dbStore = new DbStore(connection))
{
}
}
[Table("DbItemTable")]
public class DbItem
{
[Key]
[Required]
[Autoincrement] // 自增列, SQLite.CodeFirst 支持
public long DbId { get; set; }
[Required]
[Column(@"ProductName")]
[MaxLength(240, ErrorMessage = @"Name 长度不能大于 240")]
[Index]
public string Name { get; set; }
[Required]
[SqlDefaultValue(DefaultValue = "current_timestamp")] // SQLite.CodeFirst 支持
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime CreateTime { get; set; }
[NotMapped]
public string Sign { get; set; }
}
public class DbStore : DbContext
{
public DbStore(DbConnection connection)
: base(connection, true) { }
public virtual DbSet<DbItem> DbItems { get; set; }
// 要不要重建数据
public bool RecreateTable { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// 使用 SQLite.CodeFirst https://github.com/msallin/SQLiteCodeFirst 加入对自动创建表的支持, 会创建表,并会记录创建过程
// app.config 中要注册 sqlite, 或是看官方手册, 使用自定义类进行属性注册
/* <provider invariantName="System.Data.SQLite" type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" /> */
// SQLite.CodeFirst 永远重建表, 或是 不存在时新建
var initializer = this.RecreateTable
? new SqliteDropCreateDatabaseAlways<DbStore>(
modelBuilder)
: (IDatabaseInitializer<DbStore>)new SqliteCreateDatabaseIfNotExists<DbStore>(
modelBuilder);
Database.SetInitializer(initializer);
base.OnModelCreating(modelBuilder);
// todo: 使用 Fluent 定义数据表
}
}
导出 EDMX 文件
该文件可以使用 vs 打开,查看 EF 对应的数据关系
using (var context = new Context())
{
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
using (XmlWriter writer = XmlWriter.Create(@"Model.edmx", settings))
{
EdmxWriter.WriteEdmx(context, writer);
}
}
快速操作说明
products.ToString(); // 返回查询的 sql 语句
storeMode.SaveChanges(); 将更新保存入数据库
using(storeMode) 控制对模型类的释放
storeMode.Database 获得数据库相关的一些信息
storeMode.Database.Log += log => Console.WriteLine($@"==> log:{log}"); 记录日志
// 转为早期接口 IObjectContextAdapter 进行调用
var adapter = (IObjectContextAdapter)storeMode;
var objectContext = adapter.ObjectContext;
var productSet = objectContext.CreateObjectSet<Product>();
var oq = (ObjectQuery<Product>)productSet.Where(p => p.Price > 0);
添加、查找、删除、sql 操作
// 添加
var product = new Product { Name = "demo" };
storeMode.Products.Add(product);
storeMode.SaveChanges();
// 删除
products.RemoveRange(products);
storeMode.SaveChanges();
// 查找 , 原生函数
var product = products.Find(7);
product.title = @"test";
storeMode.Entry(product).Reload(); // 从数据库再次载入数据,就是放弃数据的改变
// .net 6
dbContext.Database.ExecuteSqlRaw(); // 直接执行 sql 脚本
dbContext.Database.ExecuteSqlInterpolated(); // 格式化参数执行 sql 脚本 , 但是目前 sqlite 不支持。只能使用 dapper 2021-12-26 22:45:23
// 原生sql 操作,需要自行处理表名、属性名、格式的转换, 有点烦, 见 EntitySql 部分
var parameter = new SqlParameter("id", 7); // ?? @id or id
var sqlQuery = products.SqlQuery(@"select productName as name, CAST(ProductPrice AS INT) as Price, * from tpProduct where id = @id", parameter);
foreach (var product in sqlQuery) {}
原生Connection操作
查询表名
// .net core 6
/// <summary>
/// 在 DbContext 中获取指定类型的表名, .net 6 中测试有效
/// </summary>
/// <typeparam name="T">表对象类型</typeparam>
/// <returns></returns>
/// <exception cref="System.InvalidOperationException">@"获取类型 {typeof(T)} 表名失败")</exception>
public string GetTableName<T>()
{
// We need dbcontext to access the models
var models = this.Model;
// Get all the entity types information
var entityTypes = models.GetEntityTypes();
// T is Name of class
var entityTypeOfT = entityTypes.First(t => t.ClrType == typeof(T));
var tableNameAnnotation = entityTypeOfT.GetAnnotation("Relational:TableName");
var tableValue = tableNameAnnotation?.Value;
var tableName = $@"{tableValue}";
if (string.IsNullOrWhiteSpace(tableName))
{
throw new InvalidOperationException($@"获取类型 {typeof(T)} 表名失败");
}
return tableName;
}
Local 查询
Entity Framework 在本地属性 Local 中保存了未同步的数据
products.Local.Count 获取本地缓存的数据数量
因为 linq 操作可能会引发多次数据库访问,所以可以使用 load 函数将数据加载到本地。然后对 Local 进行查询
var products = storeMode.Products;
products.Local.Count() // 这里为 0
products.Load(); // 加载所有数据, Load 将数据加载至 dbContext, 相当于 ToList
products.Where(it => it.DbId > 0 ).Load(); // 或是加载部分数据
var product = products.Local.FirstOrDefault(); // 使用本地缓存进行操作
Output(product);
LINQ
常用
SelectMany
var infos = from category in store.Categories from product in store.Products select new { category = category.Name, name = product.Name };
相当于
var products = store.Products;
var infos = store.Categories.SelectMany(category => products.Select(product => new { category = category.Name, name = product.Name }));
var products = from product in store.Products where product.Id == 3 select product;
会自动优化 --> SELECT xx FROM [tpProduct] WHERE 3 = [Id]
但是如果表达式中引用户函数,则会报错。无法优化为 sql 语句
分组
var productGroup = from product in store.Products group product by product.Name
或是
var productGroup = from product in store.Products group product by product.Name.Substring(0, 4) into category select category.Key;
将对象转为可枚举对象
var products = obj.AsEnumerable();
异步操作
var task = store.Products.ToListAsync();
where in
var ids = new int[] { 50592, 50671, 50718, 50747, 50811 };
var result = from @case in this.YxLoanCases where ids.Contains(@case.CaseId) select @case;
Distinct 去掉重复元素
Except 对比 2个集合,返回只出现 1次的元素
Intersect 取出 2个集合中重复的元素
Union 取出 2个集合中所的的元素
DefaultIfEmpty 当集合为空时,创建一个包含一个元素的集合。 因为有一些操作不能对空集合进行操作
Empty 创建一个空集合
JOIN
// Inner Join
var products = from product in storeProducts
join category in storeCategories on product.Name equals category.Name
select new { product.Id, category.Name };
// Left out join , 将 category 保存入 bCategory (多了个 into ), 保证 storeCategories 中的元素都在
var bCategories = from category in storeCategories
join product in storeProducts on category.Name equals product.Name
into bCategory // into 的内容为 product 集合
select bCategory; // 或者 select new{ name = category.Name, products = bCategory };
foreach (var bCategory in bCategories)
{
foreach (var product in bCategory){
将 product 进行了分组
}
}
bCategories.ToString() ==> 得到 sql
Entity SQL
// 可以直接操作 sql 语句
storeProducts.SqlQuery(sql, ObjectParameter)
// 这么使用,但是实际上会出现问题。还未解决
var objectContext = (dbStore as IObjectContextAdapter).ObjectContext;
ObjectParameter[] parameters = { new ObjectParameter("id", typeof(long)) { Value = 2 } };
var sql = @"select value dbitem.* from DbItemTable as dbitem where DbId > @id ;";
var dbItems = objectContext.CreateQuery<DbItem>(sql, parameters);
关联设计
这里有一些文章 : Framework 实践系列
惯例推导
个人认为,在类中出现了其它类或类集合,则认为是导航属性,然后至相关的类去查找以下内容
<导航属性名称><主体主键属性名称> ,
<主体类名称><主键属性名称>,
<主体主键属性名称>,
public class Order
{
public int OId { get; set; } <-- 主键
public string Name { get; set; }
public ICollection<OrderDetail> OrderDetails { get; set; } <--- 导航属性, 可能的外键 OrderDetailsId, OrderDetailId, Id
}
public class OrderDetail
{
public int Id { get; set; } <-- 主键
public Order MOrder { get;set; } <--- 导航属性, 可能的外键 MOrderOId, OrderOId, OId 都可作为外键
}
数据注解
public class OrderDetail
{
public int Id { get; set; }
// 声明外键
public int XOrderId { get; set; }
// 声明 XOrderId 为外键
[ForeignKey("XOrderId")]
public virtual Order Order { get;set; }
}
还有 InverseProperty 可以指定多个外键,未研究,不知道具体
Fluent API
以后有时间再研究
# 可以从 orderdetail 也可以从 order 建立关键
modelBuilder.Entity<OrderDetail>().HasRequired(detail => detail.Order).WithMany(order => order.OrderDetails)
.HasForeignKey(detail => detail.XOrderId);
modelBuilder.Entity<Order>().HasMany(order => order.OrderDetails).WithRequired(detail => detail.Order).HasForeignKey(detail => detail.XOrderId);
n 对 n
// 每个 Order 有一个可选的 Book 0|1 -> 1
// 感觉这个有点复杂,这里有讲解:http://www.cnblogs.com/dudu/archive/2012/01/04/entity_framework_one_to_one_unidirectional.html
modelBuilder.Entity<Order>().HasOptional(order => order.Book).WithRequired(customer => customer.Order);
// 为 author 与 books 创建 * <-> * 的关联
modelBuilder.Entity<Author>().HasMany(author => author.Books).WithMany(book => book.Authors)
.Map(m => m.ToTable("BookAuthor").MapLeftKey("AuthorId").MapRightKey("BookId"));
数据载入优化
批量加载关联数据
因为 Entify Framework 会动态 lazy loading 加载导航属性数据(前面加 virtual)。
- 如果是多对多的,使用 linq 调用 Include 批量加载导航属性数据
- 如果 1 对多 使用 Collection 加载导航属性数据
- 如果 1对1 使用 Reference 加载导航属性数据
LARZY loading, 如果 数据前使用 virtual 由使用 lazy loading, 最后使用的时候再真实的加载。如果不使用 virtual 则,每次运算就加载一次。 也可以通过 dbStore.Configuration.LazyLoadingEnabled = false 关闭
使用 include 设置将要加载的关联数据, 然后使用 load 或是 tolist 之类的取出数据
var store = new KTStoreMode();
store.Database.Log = Console.WriteLine;
if (false)
{
// 初始化数据
InitDemoDatas(store);
}
if (false)
{
/* 获取第一条 customer 数据 没有优化,会逐条取出导航数据
* var customer = store.Customers.First();
*/
// 获取第一条 customer 数据, 使用 Include 预先载入导航数据
var customer = store.Customers.Include("Orders").Include("Orders.OrderDetails").Include("Orders.OrderDetails.Product").First();
Output(customer);
}
if (false)
{
// 加载 linq 表达式中的导航数据, 使用 include
var query = from orderDetail in store.OrderDetails
join order in store.Orders on orderDetail.OrderId equals order.Id
select new { orderDetail, order };
// 使用 Include 预加载 product 数据
/*query.Include(item => item.orderDetail.Product).Load(); 这样调用会报错 */
(from item in query select item.orderDetail).Include(item => item.Product).Load();
foreach (var item in query.ToArray())
{
// item.orderDetail.Product (product 前加 virtual) 是 Lazy Loading 的,每次读取就会从数据库中读一次,会有效率问题
// 可以使用 Include 预先加载
var productName = item.orderDetail.Product.Name;
Console.WriteLine($@"{item.order.Name} # ${item.orderDetail.Name} # ${productName}");
}
}
if (false)
{
// 加载 单个对象的导航数据
var firstOrder = store.Orders.First();
// 如果是一对一,使用 Reference
store.Entry(firstOrder).Reference(order => order.Customer).Load();
// 如果是一对多,使用 Collection
// 批量载入 order 关联的 OrderDetails
store.Entry(firstOrder).Collection(order => order.OrderDetails).Load();
// 先 一对多,然后获得 linq 表达式,再使用 include
// 批量载入 order 关联的 OrderDetails 的 Product
store.Entry(firstOrder).Collection(order => order.OrderDetails).Query().Include(detail => detail.Product).Load();
foreach (var detail in firstOrder.OrderDetails)
{
Output(detail);
}
}
异步加载
await store.Orders.Include(order => order.Customer).ToListAsync();
store.Orders.Include(order => order.Customer).ForEachAsync(item => { item.Name}); // 使用ForEachAsync 在加载完成后进行一些处理
继承
TPH
多个相似的实体类将数据都保存在一个表中, 有一些共同数据,其它为不相同的数据。所有实体类共享一个基类, Entity Framework 会自动在表中添加字段表示具体类型
// 定义
class product { }
class book : product {}
class magazine: product {}
实体类中使用 public virtual DbSet<product> products { get; set; }
// 添加
store.Products.Add(new Book { ISBN = "ISBN" });
store.Products.Add(new Magazine { ISSN = "ISSN" });
// 查询
var books = store.Products.OfType<Book>(); // 使用 oftype 会进行优化, 在查询条件中加入相应的 where
TPT
多个相似的实体类将类型相同的数据的相同部分保存于同一个表中,不同的部分分别保存到不同的表中,然后使用关联,外键进行关联
// 定义
[Table("Products")]
class product { }
[Table("Books")]
class book : product {}
[Table("Magazines")]
class magazine: product {}
实体类中使用 public virtual DbSet<product> products { get; set; }
// 添加
store.Products.Add(new Book { ISBN = "ISBN" });
store.Products.Add(new Magazine { ISSN = "ISSN" });
// 查询
var books = store.Products.OfType<Book>(); // 使用 oftype 会进行优化, 在查询条件中加入相应的 INNER JOIN
TPC
所有子类数据单独保存于各自的数据表中,同时将相同的部分再保存一份于基类表中。 需要使用 Fluent API 设置。感觉相同的数据要保存两次,有此浪费。
因为各个子表插入数据时需要插入id 时可能会重复,这时将公共信息保存到基类表时会出现ID 重复错误。所以这种工作方式需要 手工管理 ID 号。比较麻烦。
类 同 TPH , 在 DbContet 中使用 Fluent API 进行初始化
// 定义
class product { }
class book : product {}
class magazine: product {}
实体类中使用 public virtual DbSet<product> products { get; set; }
// Fluent API
modelBuilder.Entity<Product>().Property(item => item.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); // 因为 ID 由 books 与 magazines 表中生成,所以公共表中 id 不要自动生成
modelBuilder.Entity<Book>().Map( map => {map.MapInheritedProperties(); map.ToTable("Books"); });
modelBuilder.Entity<Magazine>().Map( map => { map.MapInheritedProperties(); map.ToTable("Magazines"); });
// 添加
store.Products.Add(new Book { ISBN = "ISBN" });
store.Products.Add(new Magazine { ISSN = "ISSN" });
// 查询
var books = store.Products.OfType<Book>(); // 使用 oftype 会进行优化, 在查询条件中加入相应的 where
复杂类型
数据都保存于一个数据表中。将数据分组以类的形式保存于主类的属性中。比如 主要信息保存于 product ,书本相关的信息保存于 product.book 属性中
public class Product
{
public Book Book { get; set; } <-- 保存至其它表中, 作为属性读出
public int Id { get; set; }
public string Name { get; set; }
}
public class Book
{
public string ISBN { get; set; } <-- 该属性在 Product 表中的列名为 Book_ISBN 可以使用注释属性更改
}
// 添加
var product = new Product { Name = "product", Book = new Book { ISBN = "ISBN" } };
store.Products.Add(product);
数据编辑与维护
使用 SaveChanges() // 进行保存
public enum EntityState
{
Detached 当数据被 delete 后,保留在本地的数据被打上
Unchanged 数据保存或未修改为 Unchanged
Added 添加未保存
Deleted 删除未保存
Modified 修改未保存
}
store.Entry(product).State // 获取数据的状态
// 使用 attach 将实体加入到本地缓存中并标记为 Unchanged
store.Products.Attach(new Product() { Id = 32 }); // 相当于假装从数据库 load 到了数据
// 可以修改状态, 比如 attach 后修改为 Modified, 然后 SaveChanges
// 操作数据有两种方式
1. 调用函数 store.Products.Add(product), store.Products.Remove(product);
1. 修改状态 store.Entry(product).State = EntityState.Added,
删除数据
// find remove
var product = store.Products.Find(1509);
store.Products.Remove(product);
store.SaveChanges();
// 使用 attach 办法
var product = new Product {Id = 1509};
store.Products.Attach(product); // 书上说,不需要 attach 这一步也行的, 到时试一下
store.Entry(product).State = EntityState.Deleted;
store.SaveChanges();
// 使用原生 sql , 目前还没有找到被删除了几条的办法
var param = new SqlParameter(@"id", 1510);
var query = store.Database.SqlQuery<object>(@"delete from products where id = @id", param);
var result = query.ToArray();
状态查看
// 每次数据状态变更后可以使用 store.ChangeTracker 检测变量状态
var entries = store.ChangeTracker.Entries<Product>();
foreach (var entry in entries)
{
Console.WriteLine($@"{entry.State}");
Console.WriteLine(new string('=', 32));
Console.WriteLine(string.Join("; ", entry.OriginalValues.PropertyNames.ToArray()));
}
数据验证
// 使用 DbEntityValidationException 处理数据验证问题
[Range(0, 100)] <-- 限制
public float Price { get; set; }
try
{
var product = new Product { Price = -1 };
store.Products.Add(product);
store.SaveChanges();
}
catch (DbEntityValidationException exception)
{
foreach (var error in exception.EntityValidationErrors)
{
// error
}
}
自定义数据验证
protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
{
var errors = new List<DbValidationError>();
if (entityEntry.Entity is Product product)
{
if (product.Name.Length < 3)
{
errors.Add(new DbValidationError("Name", @"商品名长度不能小于3"));
}
}
if (errors.Count > 0)
{
return new DbEntityValidationResult(entityEntry, errors);
}
return base.ValidateEntity(entityEntry, items);
}
重载 SaveChange
可以根据需要重载 SaveChange 进行自定义变更
使用 Sql 语句
var products = store.Database.SqlQuery<Product>(sql);
var len = store.Database.ExecuteSqlCommand(sql); // 返回受影响条数
// 调用存储过程
var clientIdParameter = new SqlParameter("@ClientId", 4);
var products = store.Database.SqlQuery<Product>(@"ProductListProcedure @ClientId", clientIdParameter);
存储过程
// 调用存储过程
var clientIdParameter = new SqlParameter("@ClientId", 4);
var products = store.Database.SqlQuery<Product>(@"ProductListProcedure @ClientId", clientIdParameter);
// 自动生成存储过程
modelBuilder.Entity<Product>().MapToStoredProcedures(); // 生成 Pruduct_Insert , Product_Update, Product_Delete 存储过程
// 指定存储过程名称
modelBuilder.Entity<Product>().MapToStoredProcedures(
procedure => procedure
.Update(product => product.HasName("Product_Update"))
.Delete(product => product.HasName("Product_Delete"))
.Insert(product => product.HasName("Pruduct_Insert")));
数据变更冲突
一个可靠的办法,加入版本戳 (rowversion类型, 数据更新时。数据库会自动更新该值),进行处理操作时检测版本戳是否一致
Entity Framework 保存时自动对比 rowversion 值,如果不一致,抛出 DbUpdateConcurrencyException 异常。
当本地数据与数据库值不一致的时候,会出现错误
DbUpdateConcurrencyException exception;
var entry = exception.Entries.Single();
// 数据库优先 (databaes wins)
entry.Reload(); // 使用数据库的值覆盖当前值
model.SaveChanges();
// 客户端优先 (client wins)
entry.OriginalValues.SetValues(entry.GetDatabaseValues()); // 以当前值覆盖数据库值
model.SaveChanges();
ConcurrencyCheck 注解
使用 ConcurrencyCheck 的值,在更新时会比较当前值与数据库中的值是否变动 ,如果不是Entity Framework 改变过该值,则抛出异常, 我还没有测试过
[ConcurrencyCheck]
public string Name { get; set; }
事务处理
using (var transaction = store.Database.BeginTransaction ()) {
try {
// 数据处理
transaction.Commit ();
} catch {
transaction.Rollback ();
}
}
共享事务
// .net core 6
// 启用事务
using var transaction = dbContext.Database.BeginTransaction();
try
{
// 这里 GetDbConnection 就支持事务
using var dbConnection = dbContext.Database.GetDbConnection();
// dbContext.Database.UseTransaction() 应该在这里引用外部事务
var sqlCommand = $"insert or replace into {tableName} ( FileName, FilePath, UpDateTime) values ( @FileName, @FilePath, @UpDateTime ); ";
var count = dbConnection.Execute(sqlCommand, item);
this.loggerHelper.WriteLine($@"插入数据 {count}");
transaction.Commit();
}
catch (Exception)
{
transaction.Rollback();
throw;
}
```csharp using (var transaction = store.Database.BeginTransaction ()) { try { // 指定数据库连接及指明不拥有数据库连接 var otherStore = new KTStoreMode (store.Database.Connection, contextOwnsConnection : false); otherStore.Database.UseTransaction (transaction.UnderlyingTransaction); // 指明事务 otherStore.Products.Add (new Product () { Name = "我是产品" }); // 进行处理
store.dosomeing ()
transaction.Commit ();
} catch {
transaction.Rollback ();
}
} ```
使用 TransactionScope
// 下面的代码都在事务的控制之下
using (TransactionScope scop = new TransactionScope (TransactionScopeAsyncFlowOption.Enabled)) {
var store = new KTStoreMode ();
var otherStore = new KTStoreMode (store.Database.Connection, contextOwnsConnection : false);
otherStore.doSomeThing ();
store.doSomeThing ();
}
并发操作
默认不支持并发操作,所以在线程中可以传入 IServiceScopeFactory factory, 在该线程中新建一个 dbcontext 进行操作
private async Task RunInsertKolInfo(IServiceScopeFactory factory, ConcurrentQueue<KolInfo> queue, CancellationToken token)
{
using var serviceScope = factory.CreateScope();
await using var dbContext = serviceScope.ServiceProvider.GetRequiredService<KolInfoDbContext>();
while (!token.IsCancellationRequested)
{
try
{
if (!queue.TryDequeue(out var info)) break;
await dbContext.AddKolInfoAsync(info, token);
}
catch (Exception exception)
{
this.LogInfo("插入数据发生错误", exception);
}
}
}
数据表自动创建
直接创建数据表
// 一般测试时使用,因为不支持数据更新
dbContext.Database.EnsureCreated()
// 数据表删除
dbContext.Database.EnsureDeleted()
数据表迁移
// 1. 安装数据迁移工具
dotnet tool install --global dotnet-ef
// 2. 生成数据表迁移代码
// 在项目目录中运行(非解决方案目录 )
dotnet ef migrations add updateDateTime -v // 搜索 DbContext 子类(必须有无参数构建代码), 创建目录 Migrations 里面生成 时间戳_updateDateTime.cs 文件,根据需要手工进行修改
// 3. 执行数据更新代码, 在数据库中生成 __EFMigrationsHistory 保存结构数据
dotnet ef database update -v // 自动搜索更新表代码执行。注意, DbContext 子类必须通过比如环境变量之类的加载连接参数
dotnet ef database update --connection 'Data Source=C:\Users\zlz\Desktop\data.db3;' -v // 指定数据库连接参数进行数据表迁移
dotnet ef database update -c 'DataStore2' --connection 'Data Source=C:\Users\zlz\Desktop\data.db3;' -v // 指定 DbContext 子类及数据库连接参数进行数据表迁移, 如果有多个 DbContext 子类是,可以进行定制升级
dotnet ef database update migrationName --connection 'ssss' // 指定升级至指定的名称
dbContext.Database.Migrate(); // 代码执行, 如果是大项目不推荐,因为程序可能会并发执行, 可能会冲突
// 4. 手工处理
dotnet ef migrations script -o intit.sql // 将升级sql 导出,然后手工对 sql 进行编辑然后执行。比如手工搞砸了 __EFMigrationsHistory 表
// 上一次的操作例子
dotnet ef migrations add init // 可以创建一个 IDesignTimeDbContextFactory 子类,见下
dotnet ef database update // 更新至数据库
dotnet-ef.exe migrations remove // 移除上一次生成的迁移代码 , 但是如果已经更新至数据库的话会失败,需要 `database update xxxx ' 将数据库中的版本降下来后再 'migrations remove‘
dotnet-ef.exe datebase update init // 将数据库降至指定的版本
// 迁移时,使用指定的连接进行处理
public class KolInfoDbContextFactory : IDesignTimeDbContextFactory<KolInfoDbContext>
{
public KolInfoDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<KolInfoDbContext>();
optionsBuilder.UseNpgsql("Host=localhost; Database=DyData; Username=postgres; Password=123456 ");
return new KolInfoDbContext(optionsBuilder.Options);
}
}
// 迁移时跳过指定的表
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 指定表不自动生成
modelBuilder.Entity<KolInfoItem>().ToTable(nameof(KolInfoItem), it => it.ExcludeFromMigrations());
// 这个简单一些,我没有试过
modelBuilder.Ignore<KolInfoItem>();
base.OnModelCreating(modelBuilder);
}
其它
动态获取表名与列名
/// <summary>
/// 获取绑定的列名
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="dbSet">The database set.</param>
/// <param name="propertyName">Name of the property.</param>
/// <returns></returns>
internal static string GetTableColumnName<T>(DbSet<T> dbSet, string propertyName) where T : class
{
var property = dbSet.EntityType.FindProperty(propertyName);
if (property == null)
{
throw new ArgumentOutOfRangeException($@"在 {typeof(T)} 中找不到属性 {propertyName}");
}
var columnName = property.GetColumnName();
if (!string.IsNullOrWhiteSpace(columnName))
{
return columnName;
}
var columnNameObj = property[RelationalAnnotationNames.ColumnName];
columnName = $@"{columnNameObj}";
if (!string.IsNullOrWhiteSpace(columnName))
{
return columnName;
}
throw new InvalidOperationException($@"无法获取 {dbSet} 对应 {property} 的表名");
}
/// <summary>
/// 获取绑定的表名
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="dbSet">The database set.</param>
/// <returns></returns>
/// <exception cref="System.InvalidOperationException">@"无法获取 {dbSet} 对应的表名")</exception>
internal static string GetTableName<T>(DbSet<T> dbSet) where T : class
{
var tableName = dbSet.EntityType.GetTableName();
if (!string.IsNullOrWhiteSpace(tableName))
{
return tableName;
}
var tableNameObj = dbSet.EntityType[RelationalAnnotationNames.TableName];
tableName = $@"{tableNameObj}";
if (!string.IsNullOrWhiteSpace(tableName))
{
return tableName;
}
throw new InvalidOperationException($@"无法获取 {dbSet} 对应的表名");
}