2. モデルの変更とコンフィギュレーション

2.1. モデルの変更

前章で作成したプログラムを動かして,あらかじめデータベースを 作成しておきましょう.前節で紹介したProductクラスは以下のようなものでした。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CodeFirstTest01
{
    class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public int Price { get; set; }
    }
}

このクラスを元にDBを生成した後、新たに商品の在庫を格納する新しいプロパティを 追加してみます.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.ComponentModel.DataAnnotations;

namespace CodeFirstTest01
{
    class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public int Price { get; set; }
        public int Stock { get; set; }
    }
}

新しくStockというプロパティを追加しました. このまま前章のプログラムを実行させてみましょう. 図 invalid_operation_exception のようなエラーが発生します.

../../_images/invaliderror.JPG

図: Invalid Operation Exception

EFは前回起動したときのProductクラスを元にデータベーステーブルを 作成してしまっています. EFは,最初に作成されたProductクラスの内容を記憶しています. Productクラスの中身が書き換えられると,前回とクラスの内容が異なっていると判断され, DBにアクセスできなくなってしまいます.

この問題を解決する方法として,以下の3通りが考えられます.

  1. データベースを手動で削除する
  2. モデルクラスに変更があった場合に,自動的にデータベースを削除するようにする
  3. データベースマイグレーションを行う

選択肢1は最も単純な方法です.

選択肢2は,DatabaseのInitializerというものを変更することで実現できます.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;

namespace CodeFirstTest01
{
    class Program
    {
        static void Main(string[] args)
        {
            Database.SetInitializer(
                new DropCreateDatabaseIfModelChanges<ShoppingContext>());
            var product = new Product()
            {
                Name = "TestItem",
                Price = 100
            };
            using (var context = new ShoppingContext())
            {
                context.Products.Add(product);
                context.SaveChanges();
            }
        }
    }
}

上記のように,InitializerにDropCreateDatabaseIfModelChangesを設定すると, モデルとなるクラスが変更されていた場合に自動的にデータベースを 破棄して作り直します.

デフォルトでは,このInitializerにCreateDatabaseIfNotExistsが設定されています. 名前の通り,テーブルが存在しない場合にデータベースを作るものです.

そのほか,DropCreateDatabaseAlwaysという,モデルの変更にかかわらず常にデータベース を作り直すオプションをつけることもできます.

選択肢1,2 の方法を取った場合、データベースに保存されている内容をすべて破棄することになってしまいます。 これらは開発初期の段階では良いのかもしれませんが、システム運用が始まった後に気軽にスキーマの変更ができないといった問題がでてきます。 これらの問題を回避する方法として、データベースマイグレーションを使うという選択肢があります。 これが最も現実的な方法であると思います. マイグレーションについては データベースマイグレーション で詳しく取り上げます。

2.2. Annotation及びFluent APIを用いた自動生成テーブルの制御

今までは以下のようなクラスからテーブルデータを作成していました.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CodeFirstTest01
{
    class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public int Price { get; set; }
        public int Stock { get; set; }
    }
}

生成されたテーブルデータは以下のようなものです.

../../_images/entity_pic023.JPG

図:自動生成されたテーブルデータ

NameプロパティはNullを許容していますが,もしこのカラムのデータを必須としたい 場合はどうしたらよいでしょうか.

生成されるデータベースの情報の設定を行う場合,Annotationを使う方法と Fluent APIと呼ばれるものを使う方法との2通りがあります.

AnnotationとFluent APIとの関係は,LINQでの拡張構文とメソッドチェーンの関係に似ています.

まずは,Annotationを使った方法を見ていくこととします.

2.2.1. Annotationを用いたテーブルの設定

Annotationは.NET4 で導入された機能であり, ASP.NET MVC等を使っている方にはお馴染みかと思います.

ProductのNameプロパティへのデータ設定を必須にするためには, Required Annotationを利用します.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;

namespace CodeFirstTest01
{
    class Product
    {
        public int ProductId { get; set; }
        
        [Required]
        public string Name { get; set; }
        public int Price { get; set; }
        public int Stock { get; set; }
    }
}

Annotation機能を利用するためには,System.ComponentModel.DataAnnotationsを usingしておく必要があります.

上記のようにProductクラスを変更した後,プログラムを実行し,データベースを作成してみましょう. 前回利用したデータベースを消す処理を追加するのを忘れないようにしてください (InvalidOperationExceptionが発生してしまいます).

プログラム修正後,実行して生成されたテーブル情報を確認してください. NameプロパティがNull以外を許可するように変更されているかと思います.

../../_images/entity_pic024.JPG

図:NameプロパティをRequiredにした場合

Annotationによるテーブル情報制御の例をもう一つお見せします. 今度はNameプロパティの文字列の長さを200文字にしてみましょう. 文字列の長さを制御するには,MaxLength Annotationを用います.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.DataAnnotations;

namespace CodeFirstTest01
{
    class Product
    {
        public int ProductId { get; set; }
        
        [Required]
        [MaxLength(200)]
        public string Name { get; set; }
        public int Price { get; set; }
        public int Stock { get; set; }
    }
}

プログラムを実行して,SQL Management Studio等でデータベースの値を 確認してください.下図のようにNameカラムの文字列長が200に設定されているかと思います.

../../_images/entity_pic025.JPG

図:Nameプロパティの文字列長を200とした場合

2.2.2. Fluent APIを用いたテーブルの設定

Annotationを使って設定したRequiredとMaxLengthは,Fluent API と呼ばれるものを用いて設定が可能です. Fluent APIはLINQのチェーンメソッドのように,複数の関数をチェーンのように つなげてテーブルの制御・定義を行うものです.

Annotationを使って設定したRequiredとMaxLengthに相当するものを,Fluent APIで 実装すると,以下のようになります.注意しなければならないことは, 今回はProductクラスに修正を加えるのではなく,ShoppingContextに手を加える という点です.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;

namespace CodeFirstTest01
{
    class ShoppingContext : DbContext
    {
        public DbSet<Product> Products { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Product>()
                .Property(d => d.Name).IsRequired();
            modelBuilder.Entity<Product>()
                .Property(d => d.Name).HasMaxLength(200);
        }
    }
}

Fluent APIを使うためには,まずDbContextを継承したContextクラスに OnModelCreatingメソッドをオーバーライドして定義する必要があります. modelBuilderを用いてProductのEntityを取得し, Property関数で設定したいカラムを選択します. 今回はNameプロパティに修正を加えたいので,Property(d => d.Name)としています.

Annotationで行ったRequiredに相当するものは,Fluent APIではIsRequired関数になります. また,AnnotationのMaxLengthに相当するものは,HasMaxLength関数となります. Annotationを使う場合とFluent APIを使う場合とで関数名が異なることに注意してください.

Fluent APIでは,LINQのメソッドチェーンのように,複数の関数を組み合わせて定義することができます. 例えば上記のOnModelCreating関数内の定義は,以下の1ステップにまとめることができます.

modelBuilder.Entity<Product>()
  .Property(d => d.Name)
  .IsRequired()
  .HasMaxLength(200);