EntityFramework系列:SQLite的CodeFrist和RowVersion
没什么好说的,能支持DropCreateDatabaseIfModelChanges和RowVersion的Sqlite谁都想要。EntityFramework7正在添加对Sqlite的支持,虽然EF7不知道猴年马月才能完成正式版,更不知道MySql等第三方提供程序会在什么时候跟进支持,但是EF7中的确出现了Sqlite的相关代码。Sqlite支持EF6的CodeFirst,只是不支持从实体生成数据库,估计有很多人因为这个原因放弃了使用它。现在SQLite.CodeFirst的简单实现可以让我们生成数据库,因此在等待EF7的可以预见的长时间等待中,我们可以使用SQLite.CodeFirst,毕竟我们只是开发的时候使用DropCreateDatabaseIfModelChanges,Release时不会使用更不用担心SQLite.CodeFirst的简单实现会带来什么问题。可以直接修改源码,也可以参考最后面通过反射实现自定义DropCreateDatabaseIfModelChanges的方式。
1.RowVersion的支持:
可以从我的上一篇:在MySql中使用和SqlServer一致的RowVersion并发控制中采用相同的策略即可。我已经测试过在Sqlite中的可行性。
1.首先是RowVersion的配置:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Configurations.AddFromAssembly(typeof(SqliteDbContext).Assembly);
modelBuilder.Properties()
.Where(o => o.Name == "RowVersion")
.Configure(o => o.IsConcurrencyToken()
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None));
Database.SetInitializer(new SqliteDbInitializer(Database.Connection.ConnectionString, modelBuilder));
}
2.然后是SaveChanges的重写:
public override int SaveChanges()
{
this.ChangeTracker.DetectChanges();
var objectContext = ((IObjectContextAdapter)this).ObjectContext;
foreach (ObjectStateEntry entry in objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified | EntityState.Added))
{
var v = entry.Entity as IRowVersion;
if (v != null)
{
v.RowVersion = System.Text.Encoding.UTF8.GetBytes(Guid.NewGuid().ToString());
}
}
return base.SaveChanges();
}
3.生成__MigrationHistory:
DropCreateDatabaseIfModelChanges则需要修改SQLite.CodeFirst的代码,SQLite.CodeFirst生成的数据库不包含__MigrationHistory信息,所以我们首先修改SqliteInitializerBase添加__MigrationHistory,__MigrationHistory表是通过HistoryRow实体的映射,我们直接在EF源代码中找到相关部分作为参考。修改SqliteInitializerBase的SqliteInitializerBase方法,配置HistoryRow实体的映射。
public const string DefaultTableName = "__MigrationHistory";
internal const int ContextKeyMaxLength = ;
internal const int MigrationIdMaxLength = ;
protected SqliteInitializerBase(string connectionString, DbModelBuilder modelBuilder)
{
DatabaseFilePath = SqliteConnectionStringParser.GetDataSource(connectionString);
ModelBuilder = modelBuilder;
// This convention will crash the SQLite Provider before "InitializeDatabase" gets called.
// See https://github.com/msallin/SQLiteCodeFirst/issues/7 for details.
modelBuilder.Conventions.Remove<TimestampAttributeConvention>();
modelBuilder.Entity<HistoryRow>().ToTable(DefaultTableName);
modelBuilder.Entity<HistoryRow>().HasKey(
h => new
{
h.MigrationId,
h.ContextKey
});
modelBuilder.Entity<HistoryRow>().Property(h => h.MigrationId).HasMaxLength(MigrationIdMaxLength).IsRequired();
modelBuilder.Entity<HistoryRow>().Property(h => h.ContextKey).HasMaxLength(ContextKeyMaxLength).IsRequired();
modelBuilder.Entity<HistoryRow>().Property(h => h.Model).IsRequired().IsMaxLength();
modelBuilder.Entity<HistoryRow>().Property(h => h.ProductVersion).HasMaxLength().IsRequired();
}
4.初始化__MigrationHistory:
继续修改InitializeDatabase方法,在创建数据库后,初始化__MigrationHistory的信息。虽然采用了HistoryRow,但初始化信息我们只简单的使用生成的SQL语句作为判定实体和配置是否改变的依据,因为后面的InitializeDatabase方法中也是我们自己来判定实体和配置是否改变。
public virtual void InitializeDatabase(TContext context)
{
var model = ModelBuilder.Build(context.Database.Connection); using (var transaction = context.Database.BeginTransaction())
{
try
{
var sqliteDatabaseCreator = new SqliteDatabaseCreator(context.Database, model);
sqliteDatabaseCreator.Create();
/*start*/
context.Set<HistoryRow>().Add(
new HistoryRow
{
MigrationId = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fffffff"),
ContextKey = context.GetType().FullName,
Model = System.Text.Encoding.UTF8.GetBytes(sqliteDatabaseCreator.GetSql().ToCharArray()),
ProductVersion = "6.1.2"
});
/*end*/
transaction.Commit();
}
catch (Exception)
{
transaction.Rollback();
throw;
}
} using (var transaction = context.Database.BeginTransaction())
{
try
{
Seed(context);
context.SaveChanges();
transaction.Commit();
}
catch (Exception)
{
transaction.Rollback();
throw;
}
}
}
5.添加DropCreateDatabaseIfModelChanges支持
添加SqliteDropCreateDatabaseIfModelChanges类,在InitializeDatabase方法中判读实体和配置是否改变。需要注意的是,删除sqlite文件时,即使关闭Connection和调用GC.Collect()仍然在第一次无法删除文件,所以必须进行多次尝试。
public override void InitializeDatabase(TContext context)
{
bool dbExists = File.Exists(DatabaseFilePath);
if (dbExists)
{
var model = ModelBuilder.Build(context.Database.Connection);
var sqliteDatabaseCreator = new SqliteDatabaseCreator(context.Database, model);
var newSql = sqliteDatabaseCreator.GetSql();
var oldSql = "";
oldSql = System.Text.Encoding.UTF8.GetString(context.Set<System.Data.Entity.Migrations.History.HistoryRow>().AsNoTracking().FirstOrDefault().Model);
context.Database.Connection.Close();
GC.Collect();
if (oldSql == newSql)
{
return;
}
for (int i = ; i < ; i++)
{
try
{
File.Delete(DatabaseFilePath);
break;
}
catch (Exception)
{
System.Threading.Thread.Sleep();
}
}
} base.InitializeDatabase(context);
}
核心的代码已经贴出来,SQLite.CodeFirst本身的实现就比较简易,我添加的代码也比较简陋,因此在代码上没什么参考价值,只有使用和实用价值。毕竟只是在Debug开发时才需要这些功能的支持,对Sqlite本身和EF的提供程序没有任何影响。到这里终于松了口气,我们现在可以使用:Sql Server(CE)、Sqlite和Mysql进行Code First开发,采用相同的实体定义和配置,并且采用相同的并发控制。非Sql Server(CE)的并发控制和Sqlite不支持从代码生成数据库这两点终于克服了。
6.不修改源代码,使用反射实现
修改源码是由于SQLite.CodeFirst的内部类无法调用,考虑到可以使用反射,于是有了下面不需要修改源码,通过反射实现直接自定义DropCreateDatabaseIfModelChanges的方式:
using SQLite.CodeFirst.Statement;
using System;
using System.Data.Entity;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Migrations.History;
using System.Data.Entity.ModelConfiguration.Conventions;
using System.IO;
using System.Linq;
using System.Reflection; namespace SQLite.CodeFirst
{
public class SqliteDropCreateDatabaseIfModelChanges<TContext> : IDatabaseInitializer<TContext> where TContext : DbContext
{
protected readonly DbModelBuilder ModelBuilder;
protected readonly string DatabaseFilePath; public const string DefaultTableName = "__MigrationHistory";
private const string DataDirectoryToken = "|datadirectory|"; internal const int ContextKeyMaxLength = ;
internal const int MigrationIdMaxLength = ; public SqliteDropCreateDatabaseIfModelChanges(string connectionString, DbModelBuilder modelBuilder)
{
DatabaseFilePath = ConnectionStringParse(connectionString);
ModelBuilder = modelBuilder; // This convention will crash the SQLite Provider before "InitializeDatabase" gets called.
// See https://github.com/msallin/SQLiteCodeFirst/issues/7 for details.
modelBuilder.Conventions.Remove<TimestampAttributeConvention>();
ConfigMigrationHistory(modelBuilder);
} private string ConnectionStringParse(string connectionString)
{
var path = connectionString.Trim(' ', ';').Split(';').FirstOrDefault(o => o.StartsWith("data source", StringComparison.OrdinalIgnoreCase)).Split('=').Last().Trim();
if (!path.StartsWith("|datadirectory|", StringComparison.OrdinalIgnoreCase))
{
return path;
}
string fullPath; // find the replacement path
object rootFolderObject = AppDomain.CurrentDomain.GetData("DataDirectory");
string rootFolderPath = (rootFolderObject as string);
if (rootFolderObject != null && rootFolderPath == null)
{
throw new InvalidOperationException("The value stored in the AppDomains 'DataDirectory' variable has to be a string!");
}
if (string.IsNullOrEmpty(rootFolderPath))
{
rootFolderPath = AppDomain.CurrentDomain.BaseDirectory;
} // We don't know if rootFolderpath ends with '\', and we don't know if the given name starts with onw
int fileNamePosition = DataDirectoryToken.Length; // filename starts right after the '|datadirectory|' keyword
bool rootFolderEndsWith = ( < rootFolderPath.Length) && rootFolderPath[rootFolderPath.Length - ] == '\\';
bool fileNameStartsWith = (fileNamePosition < path.Length) && path[fileNamePosition] == '\\'; // replace |datadirectory| with root folder path
if (!rootFolderEndsWith && !fileNameStartsWith)
{
// need to insert '\'
fullPath = rootFolderPath + '\\' + path.Substring(fileNamePosition);
}
else if (rootFolderEndsWith && fileNameStartsWith)
{
// need to strip one out
fullPath = rootFolderPath + path.Substring(fileNamePosition + );
}
else
{
// simply concatenate the strings
fullPath = rootFolderPath + path.Substring(fileNamePosition);
}
return fullPath;
} private void ConfigMigrationHistory(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<HistoryRow>().ToTable(DefaultTableName);
modelBuilder.Entity<HistoryRow>().HasKey(
h => new
{
h.MigrationId,
h.ContextKey
});
modelBuilder.Entity<HistoryRow>().Property(h => h.MigrationId).HasMaxLength(MigrationIdMaxLength).IsRequired();
modelBuilder.Entity<HistoryRow>().Property(h => h.ContextKey).HasMaxLength(ContextKeyMaxLength).IsRequired();
modelBuilder.Entity<HistoryRow>().Property(h => h.Model).IsRequired().IsMaxLength();
modelBuilder.Entity<HistoryRow>().Property(h => h.ProductVersion).HasMaxLength().IsRequired();
} public string GetSql(DbModel model)
{
Assembly asm = Assembly.GetAssembly(typeof(SqliteInitializerBase<>));
Type builderType = asm.GetType("SQLite.CodeFirst.Builder.CreateDatabaseStatementBuilder"); ConstructorInfo builderConstructor = builderType.GetConstructor(new Type[] { typeof(EdmModel) });
Object builder = builderConstructor.Invoke(new Object[] { model.StoreModel }); MethodInfo method = builderType.GetMethod("BuildStatement", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public); var statement = (IStatement)method.Invoke(builder, new Object[] { });
string sql = statement.CreateStatement();
return sql;
} public void InitializeDatabase(TContext context)
{
var model = ModelBuilder.Build(context.Database.Connection);
var sqliteDatabaseCreator = new SqliteDatabaseCreator(context.Database, model);
var newSql = this.GetSql(model); bool dbExists = File.Exists(DatabaseFilePath);
if (dbExists)
{
var oldSql = System.Text.Encoding.UTF8.GetString(context.Set<System.Data.Entity.Migrations.History.HistoryRow>().AsNoTracking().FirstOrDefault().Model);
context.Database.Connection.Close();
GC.Collect();
if (oldSql == newSql)
{
return;
}
for (int i = ; i < ; i++)
{
try
{
File.Delete(DatabaseFilePath);
break;
}
catch (Exception)
{
System.Threading.Thread.Sleep();
}
}
}
using (var transaction = context.Database.BeginTransaction())
{
try
{
sqliteDatabaseCreator.Create();
transaction.Commit();
}
catch (Exception)
{
transaction.Rollback();
throw;
}
} using (var transaction = context.Database.BeginTransaction())
{
try
{
context.Set<HistoryRow>().Add(
new HistoryRow
{
MigrationId = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fffffff"),
ContextKey = context.GetType().FullName,
Model = System.Text.Encoding.UTF8.GetBytes(newSql.ToCharArray()),
ProductVersion = "6.1.3"
});
Seed(context);
context.SaveChanges();
transaction.Commit();
}
catch (Exception)
{
transaction.Rollback();
throw;
}
}
} protected virtual void Seed(TContext context)
{
}
}
}
希望你不是找了好久才找到这个解决方案。
EntityFramework系列:SQLite的CodeFrist和RowVersion的更多相关文章
- EntityFramework系列:MySql的RowVersion
无需修改实体和配置,在MySql中使用和SqlServer一致的并发控制.修改RowVersion类型不可取,修改为Timestamp更不可行.Sql Server的RowVersion生成一串唯一的 ...
- EntityFramework系列:SQLite.CodeFirst自动生成数据库
http://www.cnblogs.com/easygame/p/4447457.html 在Code First模式下使用SQLite一直存在不能自动生成数据库的问题,使用SQL Server C ...
- EntityFramework系列:Repository模式与单元测试
1.依赖IRepository接口而不是直接使用EntityFramework 使用IRepository不只是架构上解耦的需要,更重要的意义在于Service的单元测试,Repository模式本身 ...
- EntityFramework连接SQLite
EF很强大,可惜对于SQLite不支持CodeFirst模式(需要提前先设计好数据库表结构),不过对SQLite的数据操作还是很好用的. 先用SQLiteManager随便创建一个数据库和一张表:
- 使用entityframework操作sqlite数据库
首先要安装好,所需要的类库,通过NuGet来处理 http://stackoverflow.com/questions/28507904/vs-2015-sqlite-data-provider 安装 ...
- 通过EntityFramework操作sqlite(DbFirst)
记录一下通过 EntityFramework6 来操作sqlite过程 环境: visual studio 2017 .net 4.5 Console Application(项目类型) sqlite ...
- EntityFramework 系列:实体类配置-根据依赖配置关系和关联
EF实体类的配置可以使用数据注释或Fluent API两种方式配置,Fluent API配置的关键在于搞清实体类的依赖关系,按此方法配置,快速高效合理.为了方便理解,我们使用简化的实体A和B以及A.B ...
- iOS开发系列-SQLite
概述 SQLite3是一款轻型的嵌入式数据库.它占用资源非常低,在嵌入式设备中,可能只需要几百K的内存就够了.它的处理速度比Mysql.PostgreSQL这两款著名的数据库速度还快. 数据库简介 常 ...
- Entity framework 6.0 简明教程 ef6
http://www.entityframeworktutorial.net/code-first/entity-framework-code-first.aspx Ef好的教程 Entity Fra ...
随机推荐
- 【题解】 bzoj2115: [Wc2011] Xor (线性基+dfs)
bzoj2115,戳我戳我 Solution: 看得题解(逃,我太菜了,想不出这种做法 那么丢个链接 Attention: 板子别写错了 又写错了这次 \(long long\)是左移63位,多了会溢 ...
- Problem A: 道路建设 解题报告
一定存在一个最优解是一条链 否则可以接上去,不会更差 边权最小的边一定在这条链上 这个比较显然 可以把所有边都减去这个最后加上就行了 把链上的边按距离当前根的深度从小到大排列,设第一个零边位置为\(k ...
- 汉诺塔hanoi
问题描述: 有一个梵塔,塔内有三个座A.B.C,A座上有诺干个盘子,盘子大小不等,大的在下,小的在上(如图). 把这些个盘子从A座移到C座,中间可以借用B座但每次只能允许移动一个盘子,并且在移动过程中 ...
- vue element-ui表格里时间戳转换成时间显示
工作中遇到后台给的表格数据里时间是一个13位的时间戳,需要转换成时间显示在表格里, 可以用element-ui表格自带的:formatter函数,来格式化表格内容: // 时间戳转换成时间 // 使用 ...
- SQL记录-PLSQL日期与时间
PL/SQL日期及时间 PL/SQL提供两个日期和时间相关的数据类型: 日期时间(Datetime)数据类型 间隔数据类型 datetime数据类型有: DATE TIMESTAMP TIMESTAM ...
- bzoj千题计划194:bzoj2115: [Wc2011] Xor
http://www.lydsy.com/JudgeOnline/problem.php?id=2115 边和点可以重复经过,那最后的路径一定是从1到n的一条路径加上许多环 dfs出任意一条路径的异或 ...
- HDU 1729 类NIM 求SG
每次有n个盒子,每个盒子有容量上限,每次操作可以放入石头,数量为不超过当前盒子中数量的平方,不能操作者输. 一个盒子算一个子游戏. 对于一个盒子其容量为s,当前石子数为x,那么如果有a满足 $a \t ...
- js调试系列: 断点与动态调试[基础篇]
js调试系列目录: - 额,我说的不是张敬轩的 断点 这首歌,是调试用到的断点,进入正题吧. 昨天留的课后练习 1. 分析 votePost 函数是如何实现 推荐 的.其实我们已经看到了源码,只要读下 ...
- systemd的电源管理
ArchLinux早就使用systemd替代了init脚本. 不用图形界面.或者使用 i3.awesome 这样简单的窗口管理器时,systemd 可以替代 acpid 处理 ACPI 事件. 注意: ...
- FPGA学习笔记. DDS
DDS原理 直接数字式频率合成器(Direct Digital Synthesizer) 频率计算公式 Fout = FW * Fclk / 2^N Fout 输出频率, Fw 频率控制字, N 位数 ...