M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

EntityFramework CoreとNpgsqlでPostgreSQLアクセス

そろそろ.NET Core系のプロダクトを使ってアプリを作ってみようと思い立ち調べものをはじめました。

まずはEntityFramework Coreの使い方です。アプリのデータベースにはPostgreSQLを想定しているので、EntityFramework CoreとNpgsqlを使用してPostgreSQLにアクセスするサンプルを作ってみます。

フレームワークのバージョンは.NET Core 2.0にしました。開発環境はVisual Studio for Mac v7.5.2です。

ソリューションの準備

ソリューションを準備します。メニューの[ファイル]→[新しいソリューション]をクリックして表示された画面で .NET Coreアプリのテンプレートを選択します。

f:id:m12i:20180623134201p:plain

対象フレームワークを選択します。

f:id:m12i:20180623134210p:plain

新しいソリューションが作成されます。

NuGetパッケージの追加

作成されたソリューションのプロジェクトを右クリック→[追加]→[NuGetパッケージを追加]をクリックしてパッケージ管理画面を表示。次の3つのパッケージを検索してすべて追加します:

パッケージを選択して[パッケージを追加]をクリック。

f:id:m12i:20180623134722p:plain

互換性のチェックにしばしの待たされたあと、ライセンス条項への同意を求められるので[同意する]をクリックします。

f:id:m12i:20180623134744p:plain

パッケージのインストールが完了します。これで当座の必要には足ります。

モデルクラスの作成

データベースのリレーション(テーブル)に対応するモデルクラスを作成していきます。
今回はあくまでもEF Coreの使用方法を学ぶことが目的なので、「ブログアプリを作っているつもり」という適当な仮定のもとで、以下のようなモデルクラスを用意します:

モデルクラス リレーション 説明
User tt_users アプリのユーザーでありブログの投稿者を表す
Role tm_roles ユーザーのロールを表す。ユーザー:ロール=N:N。
UserRole tt_users_roles ユーザーとロールの結びつきを示す。
Post tt_posts ブログの投稿を表す。ユーザー:投稿=1:N。

Userクラスは次のようになります:

    [Table("tt_users")]
    public class User
    {
        [Key]
        [Column("id")]
        public long Id { get; set; }

        [Required]
        [Column("name")]
        public string Name { get; set; }

        [Required]
        [Column("password")]
        public string Password { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
        public virtual ICollection<Post> Posts { get; set; }
    }

属性によりテーブル名やカラム名、プライマリキーを指定しています。User.UserRolesやUser.Postsのようなナビゲーション・プロパティ(内部的にはもちろんリレーション同士の結合がなされる)にはvirtual修飾子を指定します。

Roleクラスも同様にしてコーディングします:

    [Table("tm_roles")]
    public class Role
    {
        [Key]
        [Column("id")]
        public long Id { get; set; }

        [Required]
        [Column("name")]
        public string Name { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }

UserRoleクラスでは外部キー制約を指定します:

    [Table("tt_users_roles")]
    public class UserRole
    {
        [ForeignKey("User")]
        [Column("user_id")]
        public long UserId { get; set; }

        [ForeignKey("Role")]
        [Column("role_id")]
        public long RoleId { get; set; }

        public virtual User User { get; set; }
        public virtual Role Role { get; set; }
    }

UserRole.UserIdとUserRole.RoleIdの2つで複合キーを作成したいのですが、これには属性では役不足で後述のC#コードによる設定をして上げる必要があります。

Postクラスにはこれまで見てきたクラスとくらべてあまり目新しいものはありません。せいぜいのところRequired属性のオプション値を変更しているくらいです:

    [Table("tt_posts")]
    public class Post
    {
        [Key]
        [Column("id")]
        public long Id { get; set; }

        [Required]
        [Column("title")]
        public string Title { get; set; }

        [Required(AllowEmptyStrings = true)]
        [Column("body")]
        public string Body { get; set; }

        [ForeignKey("Author")]
        [Column("author_id")]
        public long AuthorId { get; set; }

        [Column("published_at")]
        public DateTime PublishedAt { get; set; }

        public virtual User Author { get; set; }
    }

これでモデルの作成は終わりです。実際のアプリではもっと多くのモデルが必要なはずですが、今回の目的はアプリ開発ではないのでスキップします。

DbContextクラスの派生クラスの作成

モデルクラスができたらDbContextクラスの派生クラスを定義します。

DbSet<TModel>を戻り値とするプロパティを定義したあと、DbContext.OnConfiguring(...) と DbContext.OnModelCreating(...) の2つのメソッドをオーバーライドします:

    public class MyDbContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<Role> Roles { get; set; }
        public DbSet<UserRole> UserRoles { get; set; }
        public DbSet<Post> Posts { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder ob)
        {
            // Npgsql.EntityFrameworkCore.PostgreSQLパッケージから提供されているAPI。
            // 接続文字列をしていすることで、PostgreSQLに接続できる。
            ob.UseNpgsql("Host=localhost;Username=postgres;Password=;Database=efcore_npgsql_study");

            // Microsoft.EntityFrameworkCore.Proxiesパッケージから提供されているAPI。
            // 遅延ロードプロキシーの機能を有効化する。
            // この機能をONにしない場合、ナビゲーションプロパティを都度明示的にロードする必要がある。
            ob.UseLazyLoadingProxies();
        }

        protected override void OnModelCreating(ModelBuilder mb)
        {
            // tt_users_rolesテーブルのPK指定。
            // 複合キーは属性では指定できないのでコードで指定する。
            mb.Entity<UserRole>().HasKey(x => new { x.UserId, x.RoleId });

            // tt_users, tm_roles, tt_postsのPKの値を自動生成するように指定。
            mb.Entity<User>().Property(x => x.Id).ForNpgsqlUseSequenceHiLo();
            mb.Entity<Role>().Property(x => x.Id).ForNpgsqlUseSequenceHiLo();
            mb.Entity<Post>().Property(x => x.Id).ForNpgsqlUseSequenceHiLo();
        }
    }

OnConfiguring(...)メソッドの中では、PostgreSQLに接続するためのDB接続文字列を指定し、遅延ロードプロキシーの機能を有効化を行っています。

コメントにも書いている通り、遅延ロードプロキシーの機能の有効化は重要です。Microsoftリファレンスによれば、EF Core 2.1にはナビゲーション・プロパティの値の設定方法に関して3つのオプション「先行ローディング」「明示的ローディング」「遅延ローディング」があります。プロキシーを有効化しない場合は「先行ローディング」もしくは「明示的ローディング」を使用することになり、ナビゲーション・プロパティに値を設定するのに一手間が増えます(そして手間をかけ忘れるとNullReferenceExceptionやArgumentNullExceptionに悩まされることになります)。

個人的には「それなしではありえない」という印象のある「遅延ローディング」ですが、EF Coreに導入されたのは直近のv2.1からです。

OnModelCreating(...)メソッドの中では、複合キーの設定やプライマリキー自動生成の設定を行っています。

DbContextを使用する

今回データベース側にテーブルを作成するのはEF Coreにお任せしてしまうので、これでおおよその準備は整ったことになります。あとは実際にPostgreSQLに対するCRUDを試みるサンプルコードを作成するだけです:

    using (var db = new MyDbContext())
    {
        // もしDBがまだ存在しなかった場合は作成する。
        db.Database.EnsureCreated();

        // マスタ登録がまだだった場合は登録する。
        if (db.Roles.Count() == 0)
        {
            db.Roles.Add(new Role { Name = "administrator" });
            db.Roles.Add(new Role { Name = "editor" });

            // 変更を反映する(DMLを実行する)。
            db.SaveChanges();
        }

        // ユーザーが存在しない場合はダミーを登録する。
        if (db.Users.Count() == 0)
        {
            db.Users.Add(new User { Name = "foo", Password = "foo***" });
            db.Users.Add(new User { Name = "bar", Password = "bar***" });
            db.Users.Add(new User { Name = "baz", Password = "baz***" });

            // 変更を反映する(DMLを実行する)。
            db.SaveChanges();

            db.UserRoles.Add(new UserRole
            {
                UserId = db.Users.First(x => x.Name == "foo").Id,
                RoleId = db.Roles.First(x => x.Name == "administrator").Id
            });
            db.UserRoles.Add(new UserRole
            {
                UserId = db.Users.First(x => x.Name == "foo").Id,
                RoleId = db.Roles.First(x => x.Name == "editor").Id
            });
            db.UserRoles.Add(new UserRole
            {
                UserId = db.Users.First(x => x.Name == "bar").Id,
                RoleId = db.Roles.First(x => x.Name == "editor").Id
            });

            // 変更を反映する(DMLを実行する)。
            db.SaveChanges();
        }
    }

DbContext.Database.EnsureCreated()メソッドを呼び出すことでPostgreSQL上に新規のデータベースを作成し、モデルクラスの属性やC#コードのかたちで定義したテーブルや付属のDBオブジェクトが自動生成されます。ただし、メソッドが呼び出された時点でデータベースがすでに存在する場合は何も起きません。

あとはひたすらDbContextを通じてリレーションにアクセスしてC#コードを通じてCRUDを実行する(内部的にはDMLが実行される)だけです。