MySQL
.NETCore
CodeFirst
EntityFramework_Core
VisualStudioForMac

ASP.NET Core 2.0 EntityFrameworkCoreでMySQLを使う CodeFirst

環境

VisualStudio 2017 for Mac (7.3)
MySQL(5.7) on CentOS7

NuGetで必要なライブラリをインストール

  • Microsoft.AspNetCore.All(2.0.0)
  • Microsoft.EntityFrameworkCore.Tools(2.0.1)
  • Pomelo.EntityFrameworkCore.Mysql(2.0.1)

接続文字列の設定

appsettings.json
{
  "ConnectionStrings": {
    "ksContext": "server=[IP/Host];userid=[userId];password=[password];database=[schema];"
  }
}

プロジェクト作成時は "Loging"という設定が存在しているのでそのまま "ConnectionString"を追加してもよい。

フルバージョン(おまけ)appsettings.json
{
 "Logging": {
    "IncludeScopes": false,
    "Debug": {
      "LogLevel": {
        "Default": "Warning"
      }
    },
    "Console": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  },
  "ConnectionStrings": {
    "ksContext": "server=[IP/Host];userid=[userId];password=[password];database=[schema];"
  }
}

開発環境、ステージング、本番で接続を切り替える場合は

appsettings.[ビルド構成].json 

に書いて切り替えるようにする。

Modelの作成(CodeFirst)

作成するテーブルは以下の二つ
* users (ユーザテーブル)
* roles (ロールテーブル)

関連は
users 0..* <-> 1 roles

User.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;


namespace Sample.Models
{
    [Table("users")]
    public class User
    {
        [Key]
        [Column("userId", TypeName = "char(32)")]
        [Required]
        public string UserId { get; set; }

        [Column("userName")]
        [MaxLength(100)]
        [Required]
        public string UserName { get; set; }

        [Column("birthDate")]
        public DateTimeOffset? BirthDate { get; set; }

        [Column("role")]
        [Required]
        public byte RoleId { get; set; }

        [Column("isDeleted")]
        [Required]
        public bool IsDeleted { get; set; }

        [Column("created")]
        [Required]
        public DateTimeOffset Created { get; set; }

        [Column("modified")]
        [Required]
        public DateTimeOffset Modified { get; set; }

        [ForeignKey(nameof(User.RoleId))]
        public Role role { get; set; }
    }
}

実行時に生成されたuserテーブル

mysql> desc users;
+-----------+---------------------+------+-----+---------+-------+
| Field     | Type                | Null | Key | Default | Extra |
+-----------+---------------------+------+-----+---------+-------+
| userId    | char(32)            | NO   | PRI | NULL    |       |
| birthDate | datetime(6)         | YES  |     | NULL    |       |
| created   | datetime(6)         | NO   |     | NULL    |       |
| isDeleted | bit(1)              | NO   |     | NULL    |       |
| modified  | datetime(6)         | NO   |     | NULL    |       |
| role      | tinyint(3) unsigned | NO   | MUL | NULL    |       |
| userName  | varchar(100)        | NO   |     | NULL    |       |
+-----------+---------------------+------+-----+---------+-------+
Role.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;


namespace ksHotelWeb.Models.EF2
{
    [Table("roles")]
    public class Role 
    {
        [Key]
        [Column("roleId")]
        public byte RoleId { get; set; }

        [Column("description")]
        [Required]
        public string Description { get; set; }

        [Column("isDeleted")]
        [Required]
        public bool IsDeleted { get; set; }

        [Column("created")]
        [Required]
        public DateTimeOffset Created { get; set; }

        [Column("modified")]
        [Required]
        public DateTimeOffset Modified { get; set; }

        public virtual ICollection<User> Users { get; set; }
    }
}

実行時に生成されたrolesテーブル

mysql> desc roles;
+-------------+---------------------+------+-----+---------+-------+
| Field       | Type                | Null | Key | Default | Extra |
+-------------+---------------------+------+-----+---------+-------+
| roleId      | tinyint(3) unsigned | NO   | PRI | NULL    |       |
| created     | datetime(6)         | NO   |     | NULL    |       |
| description | longtext            | NO   |     | NULL    |       |
| isDeleted   | bit(1)              | NO   |     | NULL    |       |
| modified    | datetime(6)         | NO   |     | NULL    |       |
+-------------+---------------------+------+-----+---------+-------+

ハマりどころ

ハイフンなしGUIDで固定長32文字charとしたい場合
[Column("userId", TypeName = "char(32)")]

.NETのStringはnullが入るので、[Required]をつけるかつけないかでNotNullを切り替える。

structの場合[Require]は影響せず、NullableかどうかでNotNullが決まる。

未解決問題

※2017年12月現在
生成されたテーブルを確認するとカラムの順序がバラバラです。
[Column(Order=0)]
という属性を指定することができますが、反映されません。

DbContextの作成

SampleContext.cs
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace Sample.Models
{
    public class SampleContext : DbContext
    {
        /// <summary>
        /// Optionつきコンストラクタ
        /// </summary>
        /// <param name="options">Options.</param>
        public SampleContext(DbContextOptions<SampleContext> options)
            : base(options) { }

        /// <summary>
        /// デフォルトコンストラクタ
        /// </summary>
        public SampleContext() { }

        #region テーブルの宣言

        public DbSet<User> Users { get; set; }

        public DbSet<Role> Roles { get; set; }
        #endregion

        /// <summary>
        /// 接続文字列の設定
        /// </summary>
        /// <param name="optionsBuilder">Options builder.</param>
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            IConfigurationRoot configuration = new ConfigurationBuilder()
                .SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
                .AddJsonFile("appsettings.json")
                .Build();
            optionsBuilder.UseMySql(configuration.GetConnectionString("SampleContext"));
            base.OnConfiguring(optionsBuilder);
        }
    }
}

ハマりどころ

接続文字列の取得は OnConfiguringメソッドの中身で行う。
ソースについては手続きに従うので内容を理解してもいいけどコピペになるのではないかと思います。

appsettings.jsonはプロパティウインドウで出力ディレクトリにコピーする設定にしておかないと、Fileがない!って怒られます。

リレーション(Navigation Property)

外部キーを設定するときは何パターンかやり方があるみたいです。

NavigationPropertyに外部キーを設定する

User.cs
[Column("role")]
[Required]
public byte RoleId { get; set; }

[ForeignKey(nameof(User.RoleId))]
//[ForeignKey("RoleId")] これでもいい
public Role role { get; set; }

外部キーとなるプロパティにNavigationPropertyを設定する。

User.cs
[Column("role")]
[Required]
[ForeignKey(nameof(User.Role))]
//[ForeignKey("Role")] これでもいい
public byte RoleId { get; set; }


public Role role { get; set; }

DbContextのOnModelCreatingメソッドをオーバーライドする

SampleContext.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
     base.OnModelCreating(modelBuilder);
}

ModelBuilderを使って関数式(ラムダ)で制約を追加できます。
Modelで属性指定をしてるので、ここでは省略します。

実行時にNavigationPropertyがNullになる問題

以下のコード user.Roleがnullになるんです。

any.cs
using(var context = new SampleContext()) 
{
    var user = context.Users.First();
    user.Role  // <--これがnullになる。 
}

手動でNavigationPropertyをLoadする

user.Roleを読み込む.cs
using(var context = new SampleContext()) 
{
    var user = context.Users.First();
    context.Entry(user).Reference(m => m.Role).Load();
}
role.Usersを読み込む.cs
using(var context = new SampleContext()) 
{
    var role = context.Roles.First();
    context.Entry(role).Collection(m => m.Users).Load();
}

NavigationPropertyが1の場合
context.Entry(user).Reference(m => m.Role).Load();
で、user.Roleをロードします。

NavigationPropertyが多の場合
context.Entry(role).Collection(m => m.Users).Load();
で、role.Usersをロードします。

NavigationPropertyを使ったLinqToEntitiesができない(課題)

role.Usersに対して直接Where句で絞り込むということがLinqToEntitiesのレベルでできません。

LinqToObjectになる問題.cs
using(var context = new SampleContext()) 
{
    var role = context.Roles.First();
    context.Entry(role).Collection(m => m.Users).Load();
    //この時点で関連する全Userをロードしてしまっている。

    // 以下のコードは LinqToObjectになる。
    var users = role.Users.Where(m => m.IsDeleted).ToArray();
}