[EF6][SQLite] SQLite Code First 和 Migration

SQLite 跟 SQL Server Server 有很大的不一樣,透過 EF 讓它們操作起來幾乎沒有不一樣,不過還是會碰到 SQLite 不支援的情況,但 CRUD 的使用是沒有問題的;這次我要演練 SQLite Code First,這當中有碰到一些問題,幸好都可以解決,趕緊寫下筆記...

管理工具

DB Browser for SQLite

管理的工具只是用來確定 Code First 產生的資料結構如預期,我採用 DB Browser for SQLite,下載位置如下

https://sqlitebrowser.org/

開發環境

  • VS 2019
  • .NET Framework 4.8
  • EF 6.3
  • System.Data.SQLite 1.0.112.0

實作

安裝套件

Install-Package System.Data.SQLite.EF6.Migrations

連同相關的套件都裝好了

<?xml version="1.0" encoding="utf-8"?> 
<packages>  
 <package id="EntityFramework" version="6.3.0" targetFramework="net48" />   <package id="MSTest.TestAdapter" version="1.3.2" targetFramework="net48" />   
  <package id="MSTest.TestFramework" version="1.3.2" targetFramework="net48" />   
  <package id="SQLite.CodeFirst" version="1.5.3.29" targetFramework="net48" />   
 <package id="System.Data.SQLite" version="1.0.112.0" targetFramework="net48" />  
 <package id="System.Data.SQLite.Core" version="1.0.112.0" targetFramework="net48" />   <package id="System.Data.SQLite.EF6" version="1.0.112.0" targetFramework="net48" />   <package id="System.Data.SQLite.EF6.Migrations" version="1.0.106" targetFramework="net48" />   <package id="System.Data.SQLite.Linq" version="1.0.112.0" targetFramework="net48" />
</packages>

 

組態

增加 Provider 

<provider invariantName="System.Data.SQLite" type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" />
<entityFramework>     <providers>       <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />       <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>

增加連線字串

<connectionStrings>
    <add name="DefaultConnection" connectionString="data source=lab.db" providerName="System.Data.SQLite" /> 
</connectionStrings>

 

Configuration

這是 Code First 很重要的一個物件,Migration 會需要用到它

public class Configuration : DbMigrationsConfiguration<LabDbContext>
{
    public Configuration()
    {
        this.AutomaticMigrationsEnabled        = true;
        this.AutomaticMigrationDataLossAllowed = true;
        this.SetSqlGenerator("System.Data.SQLite", new SQLiteMigrationSqlGenerator());
    }
 
    protected override void Seed(LabDbContext context)
    {
        //This method will be called after migrating to the latest version.
 
        //You can use the DbSet<T>.AddOrUpdate() helper extension method
        //to avoid creating duplicate seed data.E.g.
 
        //context.Employees
        //       .AddOrUpdate(p => p.Name,
        //                    new Employee {Name = "Andrew Peters"},
        //                    new Employee {Name = "Brice Lambson"},
        //                    new Employee {Name = "Rowan Miller"}
        //                   );
    }
}

調用 Enable-Migrations 會得到以下錯誤,只好手動增加 Configuration

No MigrationSqlGenerator found for provider 'System.Data.SQLite'. Use the SetSqlGenerator method in the target migrations configuration class to register additional SQL generators.

 

POCO

先建立單一資料表 Employee

[Table("Employee")]
public class Employee
{
	[Required]
	[Key]
	public Guid Id { get; set; }

	[StringLength(100)]
	public string Name { get; set; }

	public int Age { get; set; }
}

 

SQLite 中的 Guid

SQLite 的 Guid 使用 BLOB 類型存儲 (binary),我試著查詢資料,資料可以順利的對應到 C# 的 GUID,用 Where(p=>p.Id==id) 會找不到資料,原因很簡單 EF 翻成 SQLite 的時候並不會把 GUID 轉成 Binary,若真的需要用 Id 當 Where 條件,可以考慮改用 string 存放

var employee = db.Employees.Where(p => p.Id == id).FirstOrDefault();

 

LabDbContext

public class LabDbContext : DbContext
{
    private static readonly bool[] s_migrated = {false};
 
    public DbSet<Employee> Employees { get; set; }
 
    public LabDbContext() : base("DefaultConnection")
    {
        Migrate();
    }
    private static void Migrate()
    {
        if (!s_migrated[0])
        {
            lock (s_migrated)
            {
                if (!s_migrated[0])
                {
                    Database.SetInitializer(new MigrateDatabaseToLatestVersion<LabDbContext,
                                                Configuration>());
                    s_migrated[0] = true;
                }
            }
        }
    }
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
     ...
    }
}

 

第一個案例

最後來個簡單的新增案例

新增成功會回傳異動筆數,我只驗證筆數,沒有驗證欄位內容,真實的專案可以用 Specflow 驗證多個欄位內容

[TestMethod]
public void Insert()
{
    var toDb = new Employee
    {
        Id   = Guid.NewGuid(),
        Name = "yao",
        Age  = 18
    };
    using (var db = new LabDbContext())
    {
        db.Employees.Add(toDb);
        var count = db.SaveChanges();
        Assert.AreEqual(1, count);
    }
}

 

用工具打開看看,測試跑出來的有沒有錯

 

異動 POCO

我想要增加 Identity 資料表並關聯到 Employee

[Table("Identity")]
public class Identity
{
	[Index("UK_Account",IsUnique = true)]
	public string Account { get; set; }

	public string Password { get; set; }

	[ForeignKey("Employee")]
	[Key]
	public Guid Employee_Id { get; set; }

	public virtual Employee Employee { get; set; }
}

 

public class Employee
{
    [Required]
    [Key]
    public Guid Id { get; set; }
 
    [StringLength(100)]
    public string Name { get; set; }
 
    public int Age { get; set; }
 
    public virtual Identity Identity { get; set; }  
}

 

調用 Migration

PM> Add-Migration "Add Identity table and relational to employee"

更多的參數可以參考

https://www.entityframeworktutorial.net/code-first/code-based-migration-in-code-first.aspx

 

調用 Update-Database

PM> Update-Database

Update-Database 後,資料庫也跟著異動了,觀察一下 __MigrationHistory 資料表,它會記錄著異動過程

還原測試資料

為了讓每一個案例都能獨立、不互相影響,還原是一個很重要的步驟;換言之,測試的目標是檔案的話,就還原目錄。

還原資料表

在每一次的案例開始之前和結束之後把資料通通都清掉

[TestCleanup]
public void After()
{
    using (var db = new LabDbContext())
    {
        db.Database.ExecuteSqlCommand("delete from Identity;");
        db.Database.ExecuteSqlCommand("delete from Employee;");
    }
}
 
[TestInitialize]
public void Before()
{
    using (var db = new LabDbContext())
    {
        db.Database.ExecuteSqlCommand("delete from Identity;");
        db.Database.ExecuteSqlCommand("delete from Employee;");
    }
}

 

還原資料庫

在 SQL Server 我會這樣把測試專案用的 Local DB 整個移除,讓 CI Server 的環境保持乾淨,不過很可惜的是 SQLite 不支援 dbContext.Database.Delete()

[AssemblyCleanup]
public static void AssemblyCleanup()
{
    using (var dbContext = new LabDbContext())
    {
        if (dbContext.Database.Exists())
        {
            dbContext.Database.Delete();
        }
    }
}

 

讓測試案例跑本地端的 DB,可避免網路問題造成測試失敗

 

最後我就直接把檔案砍掉

[TestClass]
public class MsTestHook
{
    [AssemblyCleanup]
    public static void AssemblyCleanup()
    {
        DeleteDb();
    }
 
    [AssemblyInitialize]
    public static void AssemblyInitialize(TestContext context)
    {
        DeleteDb();
    }
 
    private static void DeleteDb()
    {
        var filePath = "lab.db";
        if (File.Exists(filePath))
        {
            File.Delete(filePath);
        }
    }
}

 

延伸閱讀

[EF Core 3] 如何使用 Code First 的 Migration

SQLite CodeFirst、Migration 的趟坑过程 [附源码]

範例位置

https://github.com/yaochangyu/sample.dotblog/tree/master/ORM/EF6/Lab.EF6.SqliteCodeFirst

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo