MySQLでNHibernate on Mono

ここんとこ、MySQLNHibernate、Monoの話題を続けたので、それら全部を使ったネタを1つやってみようかと。一応、メインはNHibernateですが、それにしても対象をすごく限定しているネタな気が・・・(^^;

・はじめに
最近、O/Rマッピングという言葉を見かけるようになりましたが、簡単に言うとソース上から具体的なデータベースに関する記述をなくして、プログラミング言語自体でのコーディングに集中できるようにさせるものです。オブジェクト指向とリレーショナルデータベースはあんま相性が良くなかったり、オブジェクト指向とデータベース両方に精通している開発者を探すのが難しくなってきているとか、そんな背景もあったり。

NHibernateについて
NHibernateのオリジナルはHibernateというJavaのプロジェクトで、それを.NETに持ってきたものです。で、どんなことをするかといいますと、データベース接続のためのプロバイダや接続文字列などNHibernate自体の設定とアプリケーション上でのクラスとテーブルの関連を記述したマッピングファイルを使ってデータベース処理を裏方でやってくれる、っというものです。

では、早速サンプルに入ります。まずは、マッピング対象になるクラスとテーブルを見て見ましょう。

ファイル:Part.cs

namespace NHibernate.Sample {
  public class Part {
    string no;  
    string desc;
    int qonhand;
    int qonorder;
    
    public string No {
      get { return no; }
      set { no = value; }
    }

    public string Description {
      get { return desc; }
      set { desc = value; }
    }

    public int QuantityOfHand {
      get { return qonhand; }
      set { qonhand = value; }
    }

    public int QuantityOfOrder {
      get { return qonorder; }
      set { qonorder = value; }
    }
  }
}

ファイル:parts.sql

create table parts (
  partno  char(4) not null primary key,
  description varchar(20),
  qonhand integer,
  qonorder integer
);

Partクラスとpartsテーブルが対応していて、まともにコーディングするとDataReaderやDataAdapterでテーブルからデータ取得してPartオブジェクトに値を設定することになりますが、ここでは代わりにマッピングファイルを記述します。

ファイル:Part.hbm.xml

<?xml version="1.0" encoding="utf-8" ?> 

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0">

    <class name="NHibernate.Sample.Part,Part" table="parts">
    <id name="No" column="partno" type="String" length="4">
      <generator class="assigned" />
    </id>
    
    <property name="Description" column="description" type="String" length="20"/>
    <property name="QuantityOfHand" column="qonhand" type="Int32"/>
    <property name="QuantityOfOrder" column="qonorder" type="Int32"/>
  </class>
  
</hibernate-mapping>

見てのとおりクラスとテーブル、プロパティとカラムの対応を記述しています。で、NHibernateはこの情報を使って、

class name="NHibernate.Sample.Part,Part"

から、

Type t = Type.GetType("NHibernate.Sample.Part,Part");
object o = Activator.CreateInstance(t);

と、オブジェクトを生成して、あとはリフレクションで値を取得/設定している訳です。分かれば結構簡単な仕組みですね。これだけ見れば。(^^;

では、Part.dllとpartsテーブルを作成しましょう。

テーブルの作成

> mysql mydb -uuser -ppass < parts.sql

Part.dllの作成

> mcs /t:library /resource:Part.hbm.xml Part.cs

Part.dllにPart.hbm.xmlを埋め込んでいるところに注目してください。これについては後で解説します。

Partクラスとpartsテーブルとのマッピングが完了したので、実際にこれらを使ってアプリケーションを作ってみましょう。アプリケーションといっても、Partオブジェクトをテーブルに挿入するだけの簡単なものです。

ファイル:Main.cs

using System;
using NHibernate;
using NHibernate.Cfg;

namespace NHibernate.Sample {
  public class Program {
    static readonly Configuration cfg = new Configuration();

    static void Main(string[] args) {
      try {
        // 永続化クラスのアセンブリを登録
        cfg.AddAssembly("Part");

        ISessionFactory factory = cfg.BuildSessionFactory();
        ISession session = factory.OpenSession();

        // トランザクション開始
        ITransaction transaction = session.BeginTransaction();

        // Partオブジェクトをデータベースへ格納する
        Part part = new Part();
        part.No = "P207";
        part.Description = "Gear";
        part.QuantityOfHand = 75;
        part.QuantityOfOrder = 20;

        // 裏方でinsert文が発行されている
        session.Save(part);

        // コミット
        transaction.Commit();

        session.Close();
      }
      catch (ADOException e) {
        Console.WriteLine(e.Message);
      }
    }
  }
}

このソースの

cfg.AddAssembly("Part");

この部分に注目してください。マッピング対象となるクラスを含むアセンブリを指定していますが、内部で次のような処理を行っています。

assembly = Assembly.Load(assemblyName);
foreach(string fileName in assembly.GetManifestResourceNames() ) {
  if ( fileName.EndsWith(".hbm.xml") ) {
    AddInputStream( assembly.GetManifestResourceStream(fileName) );
  }
}

見てのとおり、アセンブリに埋め込まれているファイルの名前が.hbm.xmlで終わっているものを読み込んでいます。マッピングファイルのファイル名をクラス名+.hbm.xmlとしてアセンブリに埋め込んだのはこのためなんですね。マッピングファイルへの疑問が解決したところで、もう一つの設定ファイルである構成ファイルを用意しましょう。

ファイル:Main.exe.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
     <configSections>
      <section name= "nhibernate" 
          type="System.Configuration.NameValueSectionHandler, 
          System, Version=1.0.5000.0,Culture=neutral,PublicKeyToken=b77a5c561934e089" />
  </configSections>
  
  <nhibernate>
    <add 
      key="hibernate.connection.provider"          
      value="NHibernate.Connection.DriverConnectionProvider" 
    />
    <add 
      key="hibernate.dialect"                      
      value="NHibernate.Dialect.MySQLDialect" 
    />
    <add 
      key="hibernate.connection.driver_class"          
      value="NHibernate.Driver.MySqlClientDriver,MySqlDriver" 
    />
    <add 
      key="hibernate.connection.connection_string" 
      value="Data Source=localhost;Database=mydb;User Id=user;Password=pass;" 
    />
  </nhibernate>
</configuration>

hibernate.dialectは各データベースの方言を吸収する仕組みで、ここではMySQLを指定しています。hibernate.connection.driver_classは先日の日記で自作したConnector/Net用のドライバーを指定しています。MySqlDriver.dllはそちらを参考に作成してください。残りは接続文字列と、もう一つはよく分かっていませんが、決め打ちでいいんではないかと・・・(^^;

では、Main.exeを作ってしまいましょう。

> mcs /t:exe /r:NHibernate.dll /r:Part.dll Main.cs

コンパイルし、実行するには次のアセンブリが必要なのでご確認ください。

これで完成です。Main.exeを実行するとPartオブジェクトがpartsテーブルに格納されます。mysqlで確認してみましょう。

mysql> use mydb;
Database changed
mysql> select * from parts;
                                                                                        • +
partno description qonhand qonorder
                                                                                        • +
P207 Gear 75 20
                                                                                        • +
1 row in set (0.02 sec)

確かにテーブルに格納されていますね。このようにO/Rマッピングを使うとデータベースを意識せずにプログラミングが出来るようになります。

(追記) Main.exe.configがMono用になってなかったので修正しました。
Monoと.NETで微妙に動作が違うようです。

<section name= "nhibernate" 
  type="System.Configuration.NameValueSectionHandler, 
  System, Version=1.0.5000.0,Culture=neutral,PublicKeyToken=b77a5c561934e089" />

ここの部分が長いので改行を入れたところ、Monoでエラーが発生するようになりました。Monoで実行する場合は改行を取っ払って1行にしてください。
しかし、これは.NETとMonoでどっちが正しい動作なんでしょうか? マイクロソフトはよくユーザの間違いをリカバーして正常動作させてしまうので、今回もそうなのかしら?