2011年3月19日土曜日

EF4.1 CodeFirstでenumを使いたい

最近、いろんなお誘いメールがケータイに舞い込んできて、いやもうマジクリックしちゃうぞコノヤロー。そんなに誘惑するんじゃないよ!

なんか迷惑メールが地震以降強烈に増えましたね。

結局enumはサポートされないことが決定してしまったCodeFirst。しょうがないですね。RCからRTMまでの間で機能追加はないので、ここは潔く諦めましょう。

でも、やっぱりenumを使いたいですよね。プロパティをパースするときにenumかenumのジェネリックは完全にスルーされてデータベーステーブルがScaffoldingされます。なのでComplex Typeとしてenumをラップしたクラスを用意し「オレenumじゃないよ、全然関係ないから!」とEFを騙す必要があります。

Tip 23 – How to fake Enums in EF 4 - Meta-Me - Site Home - MSDN Blogs

古のテクニックですね。2009年です。今もこれしか方法がないみたいです。

コンソールアプリを作りながら同じようにやってみよー!VS2010SP1を前提にしますよ?いいですか?だってSQLCE4使ってみたいでしょ。なのでSQLCE4も入ってる前提で行きます。なきゃないで問題ないので、SQLCE4関連の部分は読み替えてください。

ソリューション作ったらまずは、NuGet!NuGetで必要なパッケージを入れてしまいましょう。以下の2つ。

install-package EFCodeFirst
install-package EFCodeFirst.SqlServerCompact

↑こうです。

efenum1 efenum2

WebActivatorは不要なので、参照設定から削除しましょう。WebActivatorについては先日書いたので、気になる人はチェックしてみてね。

無聊を託つ: WebActivatorでお手軽Bootstrapper

App_Start/SQLCEEntityFramework.csもEFCodeFirst 0.8ベースなので少しいじっちゃいます。といっても、DbDatabaseをDatabaseにするのとWebActivatorの属性を削除。あと、SqlCe用のnamespace(System.Data.Entity.Infrastructure)が変わってるので、そこも変更。

efenum3

これで準備完了。ワクワクするね!ワクワクの強要。

最初にモデルクラスをつくって、次にDbContext派生。行きます。

using System.Data.Entity;

namespace EFEnum
{
    public enum Role
    {
        Unknown,
        Sniper,
        Captain
    }

    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Role Role { get; set; }
    }

    public class EFEnumContext : DbContext
    {
        public DbSet<Person> People { get; set; }
    }
}

↑EFEnumContext.cs

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="SampleContext" connectionString="Data Source=|DataDirectory|db.sdf;" providerName="System.Data.SqlServerCE.4.0"/>
  </connectionStrings>
  <system.data>
    <DbProviderFactories>
      <remove invariant="System.Data.SqlServerCe.4.0" />
      <add name="Microsoft SQL Server Compact Data Provider 4.0" invariant="System.Data.SqlServerCe.4.0" description=".NET Framework Data Provider for Microsoft SQL Server Compact" type="System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91" />
    </DbProviderFactories>
  </system.data>
</configuration>

↑App.config。

とりあえず、enumのまま作ってどういう動作になるのかを見てみます。

using System;
using System.Linq;
using EFEnum.App_Start;

namespace EFEnum
{
    class Program
    {
        static void Main(string[] args)
        {
            SQLCEEntityFramework.Start();
            var db = new EFEnumContext();

            var query = from p in db.People
                        select p;
            var count = query.Count();
            Console.WriteLine("{0}人いるよ!", count);

            if (count == 0)
            {
                var person1 = new Person
                                  {
                                      Name = "ルフィー",
                                      Role = Role.Captain
                                  };
                var person2 = new Person
                                  {
                                      Name = "ウソップ",
                                      Role = Role.Sniper
                                  };
                db.People.Add(person1);
                db.People.Add(person2);
                db.SaveChanges();
            }

            count = query.Count();
            Console.WriteLine("{0}人いるよ!", count);
            foreach (var person in query)
            {
                Console.WriteLine(person.Name + string.Format("({0})", person.Role));
            }


            Console.ReadLine();
        }
    }
}

↑main.cs。

    public static class SQLCEEntityFramework {
        public static void Start() {
            Database.DefaultConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0");

            // Sets the default database initialization code for working with Sql Server Compact databases
            // Uncomment this line and replace CONTEXT_NAME with the name of your DbContext if you are 
            // using your DbContext to create and manage your database
            Database.SetInitializer(new DropCreateCeDatabaseIfModelChanges<EFEnumContext>());
        }
    }

SQLCEEntityFrameworkのStartメソッドにDbContextのクラスを指定するのも忘れずに。

ちょっと、コード長いけどドンマイ。行くぜ!

efenum4

クリックして拡大してみてね。ちゃんと名前の横にロールが表示されました。これはAddするインスタンスに設定したものが、そのまま表示されただけです。次に一度終了して、もう一度実行してみます。

efenum5

そうすると、今度は"Unknown"と出ました。なんでかというと、なんとテーブルには保存されてないからです!そういう仕様なので驚かないでね。VSから確認してみます。

efenum6

efenum7

カラム無いですね。ちょっと切ないですね。

さぁ本題です!ちょこちょこっとクラス用意しましょう。

    public abstract class EnumWrapper<T>
    {
        private T _value;
        public string Value
        {
            get
            {
                return _value.ToString();
            }
            set
            {
                _value = (T)Enum.Parse(typeof(T), value, true);
            }
        }

        public static implicit operator string (EnumWrapper<T> value)
        {
            return value.Value;
        }
    }

これを基底クラスとしてenumの型毎にクラスを用意していきます。

    public class RoleWrapper : EnumWrapper<Role>
    {
        public static implicit operator RoleWrapper(Role value)
        {
            return new RoleWrapper { Value = value.ToString() };
        }
    }

enum型1個しかないので、1個つくりましょう。ホントはimplicit operatorも書きたくないんだけど、こればっかりはしょうがない。何かいい方法ありますかね~?最終的にはT4で自動生成(プロジェクト内のenumを全部列挙してしまえばいいかな)させちゃえばいいでしょう。

そして、モデルクラスをこのクラスを使うように変更します。

    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public RoleWrapper Role { get; set; }
    }

このままだと、mainのConsole.WriteLineでクラス名がでちゃうのでそこはRole.Valueに変えておきましょう。そうして実行してみると...。

efenum9 efenum8

タターン!左が1回目。0件で追加して2件。右が2回目。2件あるから2件表示。ちゃんとロールも表示されますね。テーブル見てみましょう。

efenum10

カラムも追加されてるし、値も入ってる~。文字列にしてるのはパースしやすいのと、パッと見データ見ただけでわかるからっていうだけの理由です。intがよければそのように。

いいんじゃないでしょうか。こんな感じで。

packages含んじゃってるのでちょっと大きいですがご容赦を。