Entity Framework を使用して SQLite に migration するまで

  • 今回作成する Models の ER図(dbdiagram.io)
    • dotnet ef migrations script により作成した DDL から生成

セットアップ

まずは、3系の sdk を使用して .NET Core で開発できるようセットアップします。

dotnet --list-sdks

# sdk のバージョンを固定
dotnet new globaljson --sdk-version 3.1.408

Spring Initilizar 的な scaffolding 用のコマンドで webapi を開発します。

必要な tool や package を追加します。

dotnet new webapi -n sandbox

# tool のバージョンをディレクトリで固定
dotnet new tool-manifest

# tool の install 
dotnet tool install dotnet-ef --version 3.1.4

# package の追加
dotnet add package Microsoft.EntityFrameworkCore.Design --version 3.1.4

dotnet add package Microsoft.EntityFrameworkCore --version 3.1.4

dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 3.1.4

dotnet new gitignore

この時点で一度、起動するか確認。

dotnet run

Entity の定義

models ディレクトリをつくり、その中に Entity の定義をします。

hibernate と比較して、 Entity 自体に oneToMany といった relation を示す定義は書かないみたいです。

DbContext を継承した Context の設定用クラスが味噌っぽいです。

このクラスに Context 内で扱いたい Entity を記載します。 _context.Books.Where() のように扱えます。

        public DbSet<Book> Books { get; private set; }
        public DbSet<Author> Authors { get; private set; }
        public DbSet<Review> Reviews { get; private set; }
        public DbSet<Reviewer> Reviwers { get; private set; }
        public DbSet<BookAuthor> BookAuthors { get; private set; }

ManyToMany(N:N) の relation については中間テーブルが必要になるため、 OnModelCreating メソッドに定義を追加します。

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<BookAuthor>().HasKey(ba => new { ba.BookId, ba.AuthorId});
            modelBuilder.Entity<BookAuthor>()
                .HasOne(ba => ba.Author)
                .WithMany(a => a.BookAuthors)
                .HasForeignKey(ba => ba.AuthorId);
            modelBuilder.Entity<BookAuthor>()
                .HasOne(ba => ba.Book)
                .WithMany(b => b.BookAuthors)
                .HasForeignKey(ba => ba.BookId);
        }

Framework による migration

Spring の Active Profiles のように複数の設定ファイルを切り替えて設定情報を注入できるようです。

開発用の設定として、 SQLite の接続情報を記載します。

// appsettings.Development.json

    "ConnectionStrings": {
        "DefaultConnection": "Data source=sandbox.db",
        "MSSQLConnection": "Server=localhost,1433;Initial Catalog=Sandbox; User ID=sa; Password=P@ssw0rd!;"
    }

Startup.cs の ConfigureServices メソッドが DI コンテナへの追加を行います。

ここに DI に必要な設定として、 interface と使用する実装のペアを記載して使用するのですが、 今回は、 Datasource への接続情報を Context に登録するようなイメージで設定を記載します。

が、接続情報は外部の設定ファイルから注入したいので、 appsettings.Development.json の設定名の key で指定します。

// Startup.cs

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddDbContext<DataContext>(options =>
            {
              options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"));
            });
        }

あとは、 migration を実行し、 Migrations フォルダが作成された後に、 update で DB に適用されます。

$ dotnet ef --version
Entity Framework Core .NET Command-line Tools
3.1.4

dotnet ef migrations add InitialCreate

# 削除する場合
# dotnet ef migrations list
# dotnet ef migrations remove

# DB に適用
dotnet ef database update

# 適用したものを削除
# dotnet ef database drop

migration を行わず、 DDL を生成することもできるようです。 ここで MSSQL むけの DDL を生成して、 dbdiagram.io で ER図を作成としました。

-- dotnet ef migrations script
-- CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
--     "MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
--     "ProductVersion" TEXT NOT NULL
-- );

CREATE TABLE "Authors" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Authors" PRIMARY KEY AUTOINCREMENT,
    "FirstName" TEXT NULL,
    "LastName" TEXT NULL
);

CREATE TABLE "Books" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Books" PRIMARY KEY AUTOINCREMENT,
    "Title" TEXT NULL,
    "Published" TEXT NULL
);

CREATE TABLE "Reviwers" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Reviwers" PRIMARY KEY AUTOINCREMENT,
    "FirstName" TEXT NULL,
    "LastName" TEXT NULL
);

CREATE TABLE "BookAuthors" (
    "BookId" INTEGER NOT NULL,
    "AuthorId" INTEGER NOT NULL,
    CONSTRAINT "PK_BookAuthors" PRIMARY KEY ("BookId", "AuthorId"),
    CONSTRAINT "FK_BookAuthors_Authors_AuthorId" FOREIGN KEY ("AuthorId") REFERENCES "Authors" ("Id") ON DELETE CASCADE,
    CONSTRAINT "FK_BookAuthors_Books_BookId" FOREIGN KEY ("BookId") REFERENCES "Books" ("Id") ON DELETE CASCADE
);

CREATE TABLE "Reviews" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Reviews" PRIMARY KEY AUTOINCREMENT,
    "Headline" TEXT NULL,
    "ReviewText" TEXT NULL,
    "Rating" INTEGER NOT NULL,
    "ReviewerId" INTEGER NULL,
    "BookId" INTEGER NULL,
    CONSTRAINT "FK_Reviews_Books_BookId" FOREIGN KEY ("BookId") REFERENCES "Books" ("Id") ON DELETE RESTRICT,
    CONSTRAINT "FK_Reviews_Reviwers_ReviewerId" FOREIGN KEY ("ReviewerId") REFERENCES "Reviwers" ("Id") ON DELETE RESTRICT
);

CREATE INDEX "IX_BookAuthors_AuthorId" ON "BookAuthors" ("AuthorId");

CREATE INDEX "IX_Reviews_BookId" ON "Reviews" ("BookId");

CREATE INDEX "IX_Reviews_ReviewerId" ON "Reviews" ("ReviewerId");

-- INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
-- VALUES ('20210531222210_InitialCreate', '3.1.4');

作成された DB の確認

SQLite なので単一のファイルとして存在しています。 Mac には標準でクライアントが入っているので、繋いで確認します。

$ file sandbox.db
sandbox.db: SQLite 3.x database, last written using SQLite version 3028

$ sqlite3 sandbox.db 
SQLite version 3.32.3 2020-06-18 14:16:19

sqlite> .database
main: /Users/kiyotakeshi/gitdir/c-charp/c-charp-sandbox/sandbox/sandbox.db

sqlite> .table
Authors                Books                  Reviwers             
BookAuthors            Reviews                __EFMigrationsHistory

DB が作成されていることが確認できました。

普段の開発では任意の SQLite に対応したクライアントを使用するといいと思います。 vscode なら以下のものが使いやすかったです。

Name: SQLite
Id: alexcvzz.vscode-sqlite
Description: Explore and query SQLite databases.
Version: 0.12.0
Publisher: alexcvzz
VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=alexcvzz.vscode-sqlite