ラベル NHibernate の投稿を表示しています。 すべての投稿を表示
ラベル NHibernate の投稿を表示しています。 すべての投稿を表示

2013年1月16日水曜日

[NHibernate]enumを文字列でマッピングする

NHibernate で enum のプロパティ/列をマッピングするときのメモです。

普通にマッピングすると

以下の様な enum のプロパティをマッピングすると、"Gender" は 1, "Female" は 2 という感じで enum の中身の整数(int)でマッピングされます。

モデル

public enum Gender
{
    Male = 1,
    Female = 2
}
public class Student
{
    public virtual int Id { get; set; }

    public virtual string Name { get; set; }

    public virtual Gender Gender { get; set; }
}

テーブル

テーブルのほうはこんな感じですね。

CREATE TABLE [dbo].[Students] (
    [Id]     INT            IDENTITY NOT NULL PRIMARY KEY,
    [Name]   NVARCHAR (100) NOT NULL,
    [Gender] INT            NOT NULL,
    CHECK ([Gender] = 1 OR [Gender] = 2)
);

マッピング

マッピングはこうです。

<class name="Student" table="Students">

    <id name="Id">
        <generator class="identity"></generator>
    </id>

    <property name="Name"></property>
    <property name="Gender"></property>

</class>

データ

Id Name Gender
1 スタン 1
2 カイル 1
3 カートマン 1
4 ケニー 1
5 バターズ 1
6 ウェンディ 2

文字列でマッピングしたいときは

でも整数ではなく文字列でテーブルに持ちたいときもあります。データをこんな風にしたいときですね。

Id Name Gender
1 スタン Male
2 カイル Male
3 カートマン Male
4 ケニー Male
5 バターズ Male
6 ウェンディ Female

そんなときは NHibernate の IUserType インターフェイスを実装してマッピングに利用します。 例えばこんな感じの基底クラスを用意しておいて、

public class EnumStringType<TEnum> : IUserType
{
    public bool Equals(object x, object y)
    {
        if (ReferenceEquals(x, y)) return true;
        if (x == null || y == null) return false;
        return x.Equals(y);
    }

    public int GetHashCode(object x)
    {
        if (x == null) return 0;
        return x.GetHashCode();
    }

    public object NullSafeGet(IDataReader rs, string[] names, object owner)
    {
        var value = NHibernateUtil.String.NullSafeGet(rs, names);

        if (value == null)
            return null;

        return (TEnum) Enum.Parse(typeof (TEnum), (string) value);
    }

    public void NullSafeSet(IDbCommand cmd, object value, int index)
    {
        string stringValue = null;

        if (value != null)
            stringValue = ((TEnum) value).ToString();

        NHibernateUtil.String.NullSafeSet(cmd, stringValue, index);
    }

    public object DeepCopy(object value)
    {
        return value;
    }

    public object Replace(object original, object target, object owner)
    {
        return original;
    }

    public object Assemble(object cached, object owner)
    {
        return cached;
    }

    public object Disassemble(object value)
    {
        return value;
    }

    public SqlType[] SqlTypes
    {
        get { return new[] {NHibernateUtil.String.SqlType}; }
    }

    public Type ReturnedType 
    {
        get { return typeof (TEnum); }
    }

    public bool IsMutable 
    {
         get { return false; }
    }
}

これを派生した、目的の enum 用のクラスを作ればOKです。

public class GenderStringType : EnumStringType<Gender>
{
}

テーブル

あとはテーブルの列の型を文字列に変更して...

CREATE TABLE [dbo].[Students] (
    [Id]     INT            IDENTITY NOT NULL PRIMARY KEY,
    [Name]   NVARCHAR (100) NOT NULL,
    [Gender] NVARCHAR (10)  NOT NULL,
    CHECK ([Gender] = ‘Male‘ OR [Gender] = ‘Female‘)
);

マッピング

マッピングに利用する UserType を指定してあげます。

<class name="Student" table="Students">

    <id name="Id">
        <generator class="identity"></generator>
    </id>

    <property name="Name"></property>
    <property name="Gender" type="Sample.NH.UserTypes.GenderStringType, Sample.NH"></property>

</class>

これで enum を文字列で永続化できます。

補足

例では簡潔にするために省きましたが、整数/文字列どちらで保存する場合も、 実際にはマスタテーブル(この例だと Genders とか)を作って外部キー制約を指定しておいたほうがいいですね。

CREATE TABLE [dbo].[Students] (
    [Id]     INT            IDENTITY NOT NULL PRIMARY KEY,
    [Name]   NVARCHAR (100) NOT NULL,
    [Gender] NVARCHAR (10)  NOT NULL,
    FOREIGN KEY (Gender) REFERENCES Genders (Value) 
);
CREATE TABLE [dbo].[Genders] (
    [Id]     INT            NOT NULL PRIMARY KEY,
    [Value]   NVARCHAR (10) NOT NULL,
    UNIQUE (Value)
);

2012年7月23日月曜日

[C#]エンティティのEqualsとGetHashCode

しばらくの間、曖昧な理解のまま Equals と GetHashCode を実装していたので、反省。

まず、Equals と GetHashCode の実装については、以下の記事が参考になります。

概ね理解しているつもりだったんですが、今日ハッシュのコレクションを扱うときに、コレクション内のある要素を削除(Remove)しようとしても、削除できない(Contains(item) が false になってしまう)、というバグが出たので調べていたら、こちらの記事に行き当たりました。

  1. 2 つのオブジェクトが等しい (== 演算子の定義によって等しい) 場合、それらのオブジェクトからは同じハッシュ値が生成されなければならない。同じハッシュ値が生成されない場合、コンテナからオブジェクトを探しだすためのキーとしてハッシュコードを使用することができない。
  2. 任意のオブジェクト A に対して、A.GetHashCode() は各インスタンス毎に異なる値でなければならない。A のどんなメソッドが呼ばれたとしても、A.GetHashCode() は常に同じ値を返さなければならない。これによって、オブジェクトが格納されている正しいバケツを常に見つけだすことができる。
  3. ハッシュ関数は任意に入力された複数の整数値に対し、ランダム分布となる返り値を返す必要がある。この性質により、ハッシュベースのコンテナへ効率的にオブジェクトを格納できる。

この実装ポリシーのうち、2番を思いっきり無視していました。つまり、ハッシュのコレクションを生成したあとに、コレクション内のある要素の GetHashCode の演算に利用してるプロパティが変化して、ハッシュ値が変わってしまったようです。

また、以下の様な記事もありました。

 

エンティティにおける、Equals と GetHashCode の実装

今回このハッシュのコレクションというのは、いわいるエンティティのコレクションだったわけですが、その実装が以下のようなもの。

public class User
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public override bool Equals(object obj)
    {
        var other = obj as User;
        return 
            other != null && 
            FirstName == other.FirstName && 
            LastName == other.LastName;
    }

    public override int GetHashCode()
    {
        return 
            (FirstName ?? string.Empty).GetHashCode() ^ 
            (LastName ?? string.Empty).GetHashCode();
    }
}

テーブルのほうはこんな感じです。

CREATE TABLE Users (
    Id INT IDENTITY PRIMARY KEY,
    FirstName NVARCHAR(50) NOT NULL,
    LastName NVARCHAR(50) NOT NULL,
    UNIQUE (FirstName, LastName)
)

Id が人工キーで、スキーマ上のメインの識別子になります。自然キーとして FirstName と LastName が利用できる(実際には名前は被るので使えないと思いますが…例です)ので、FirstName と LastName に一意キー制約を付けてあります。

この Equals 及び GetHashCode の実装では、以下のガイドラインの推奨に従って、人工キーではなく自然キーによる等価比較の実装を行なっています。(ちなみに ORM は NHibernate です

ビジネスキーの等価性 を使って、 equals() と hashCode() を実装することをお勧めします。ビジネスキーの等価性とは、 equals() メソッドが、ビジネスキー、つまり現実の世界においてインスタンスを特定するキー(自然 候補キー) を形成するプロパティだけを比較することを意味します。

このとき、FirstName と LastName が変化する場合、上記のような実装だとハッシュ値が変わってしまってポリシー違反を起こします。

なので、FirstName と LastName が変化する属性としての側面をもつ場合は、Equals と GetHashCode の実装に使うとマズイということになります。(FirstName と LastName が不変ならオッケーなんですが

このような場合は、素直に変化しない(はずの) Id を用いたほうがよさそうです。

public class User
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public override bool Equals(object obj)
    {
        var other = obj as User;
        return
            other != null &&
            Id == other.Id;
    }

    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

本来なら自然キーでの実装のほうが、なにかと扱いやすいのですが仕方ないですね。

ちなみにこの例でも、Id のセッタが公開されてしまっているので、キチンと実装する場合は、Id が不変になるように工夫したほうがよさそうです。

2012年5月14日月曜日

[NHibernate]インターフェイスのマッピング

この間ふと、「NHibernateってインターフェイスをマッピングできるのかな。」と気になったので、実験をしてみました。

どういうこと?

例えば以下のようなエンティティがあったとします。

public class Employee : IPerson
{
 public virtual int Id { get; set; }

 public virtual string Name { get; set; }

 public string FullName
 {
  get { return Name; }
 }
}
public class Customer : IPerson
{
 public virtual int Id { get; set; }

 public virtual string FirstName { get; set; }

 public virtual string LastName { get; set; }

 public string FullName
 {
  get { return LastName + " " + FirstName; }
 }
}
いずれも見ての通り IPerson というインターフェイスを実装したクラスです。

public interface IPerson
{
 string FullName { get; }
}
マッピングは以下のようにします。

<class name="Employee" table="Employees" lazy="false">
  
 <id name="Id">
  <generator class="identity"></generator>
 </id>

 <property name="Name"></property>

</class>

<class name="Customer" table="Customers" lazy="false">
  
 <id name="Id">
  <generator class="identity"></generator>
 </id>

 <property name="FirstName"></property>
 <property name="LastName"></property>

</class>
この場合に、IPerson のエンティティ全体に対してクエリを投げたりできるのか。ということです。
以下のような問い合わせを想定します。

var persons = session.Query<IPerson>();
こんなことをしたい場合、どうすればいいんだろう?という疑問です。

ちなみに、適当に以下のようなデータを入れておきます。

Employees
Id Name
1 さいとー
2 ぱず
3 ぼーま
4 いしかわ
Customers
Id FirstName LastName
1 かずんど ごうだ
2 ひでお くぜ

モノは試し

せっかくなので上記のコードを試してみます。今は IPerson なんてマッピングファイルには一切記述していないので、失敗するだろうと思って試すと...


何事も無く動いてしまいました。しかも期待通りの結果です。

NHibernateが出したログから実行されたSQLを見てみます。

select employee0_.Id as Id0_, employee0_.Name as Name0_ from Employees employee0_
select customer0_.Id as Id1_, customer0_.FirstName as FirstName1_, customer0_.LastName as LastName1_ from Customers customer0_

どうやら IPerson に対するクエリを書いたので、実装エンティティを順番に問い合わせて、結果を合わせて返してくれたようですね。たまげたなぁ。


公式サイトのドキュメントに載ってた

ここに載ってました。暗黙的ポリモーフィズムと呼ばれるタイプのマッピングアプローチのようです。

インターフェイスのマッピングに関しては、他にも数種類のアプローチがあり、また今度試して見ることにします。(ドキュメントを眺めているだけだとなんだかよくわからないので...)


気になる

ただ、この方法だと実装エンティティの数だけ問い合わせが行われてしまいますね。
また、条件や並び順を指定したときどうなるのかも少し試してみたのですが、投稿をわけてまとめたいと思います。

Factorio: Space Exploration クリア記録

 工場建設クラフトゲーム、Factorio の MOD である Space Exploration のクリア記録です。 はじめに プレイ時間は約 350 時間、2023年10月から2025年2月にかけて15ヶ月間に及びました。この期間中3人の友人と毎週末、工場勤務に明け暮れました...