Skip to content

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)。

  1. 如果是多对多的,使用 linq 调用 Include 批量加载导航属性数据
  2. 如果 1 对多 使用 Collection 加载导航属性数据
  3. 如果 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} 对应的表名");
        }