give IT a try

プログラミング、リモートワーク、田舎暮らし、音楽、etc.

O/Rマッピングツールに対する誤解をときたい -実装編 Part7-

Part6を読む

データベース関連の説明

Part1でもお話しした通り、ここまで「あ・え・て」データベース関連の説明をしてきませんでした。
なぜならO/Rマッピングツールは


SQLを書きたくない人のためのツールではない


からです。


そうではなく、O/Rマッピングツールは

  • 最初からプログラムをオブジェクト指向で設計できる
  • 実装する上でもデータベースのことは気にせず、クラスやオブジェクトを操作することだけに開発者が集中できる
  • ポリモーフィズムや継承など、オブジェクト指向プログラミングのメリットを存分に活用することができる
  • 結果、プログラムの開発効率や保守性、拡張性を高めることができる


・・・といったことを可能にしてくれるツールであり、それを強調したいがゆえにここまでずっとデータベースの話は無しにして、クラス設計やオブジェクト指向プログラミングの話題とともにO/Rマッピングツールを説明してきました。
(元ネタのエントリもその点にフォーカスしています)


とはいえ、データは結局リレーショナルデータベースに保存されます。
NHibernateが勝手にデータベースを作ってマッピングしてくれるわけではないので、データベース関連のセットアップは必要になってきます。
ここでは順番に以下のような内容を説明していきます。

  • テーブルの設計と実装
  • NHibernate用のマッピングファイル
  • ASP.NET側で必要な設定
  • 発行されるSQLの確認
テーブルの設計と実装

Part2でクラス図を説明する際に、これをそのままテーブルとして実装することはできないとお話ししました。


データベースのテーブルには基本的に継承という概念はありません。
また、オブジェクト同士の関連はプログラム上では「オブジェクトの参照」によって実現されますが、データベースの場合では「外部キー」が必要になってきます。しかし、クラスには外部キーは存在しません。


などなど、データベース上のテーブルとプログラム上のクラスはかなり似た構造になりますが、全く同じというわけにはいかない部分が出てきます。


それではこのサンプルプログラムではどのような違いが出てくるのか、実際に見てみましょう。


こちらがクラス図です。


f:id:JunichiIto:20101223052810p:image:w600


そしてこちらがデータベース上のER図です。
f:id:JunichiIto:20101224050647p:image:w600
比較してみると「似ているがやはり微妙に違う」ということが理解できると思います。
このサンプルプログラムはオブジェクト指向で設計し、かつNHibernateを利用する、ということが前提条件になっているので、テーブル設計もそれに引きずられる形になっています。


たとえば注文明細テーブル(book_order_item)は注文番号(book_order_id)と行番号(item_order)だけで複合主キーが作れそうですが、NHibernateでの実装を考慮して人工キー(book_order_item_id)を主キーとしています。


このように、普通のデータベース設計に慣れている人はO/Rマッピングツールに最適化したテーブル設計に違和感を覚えるかもしれません。
しかし、主従関係で言うとクラスやNHibernateが「主」でデータベースが「従」となるため、多少不自然なテーブル設計となってしまうのは避けられないのかなと思います。


逆にデータベースを「主」とし、データベースを最適化するテーブル設計を行うと、NHibernateの設定やオブジェクト指向プログラミングにおいて、やりにくい部分が出てくると思います。
このあたりがまさに「インピーダンスミスマッチ問題」となるわけです。


少し話がそれてしまいましたが、あと注目したいのが書籍テーブル(book)です。
クラス図上では3つのクラスに分かれていましたが、テーブルとしては1つのテーブルになっています。
和書と洋書の違いはbook_typeカラムで識別することになります。


それから、テーブル名やカラム名は「小文字+アンダーバー」とし、あえてクラス名やプロパティ名と異なるようにしています。
テーブル名やカラム名にも全く同じ名前が付いていると、ソースコードやHQLを読む際に「オブジェクトではなくテーブルを操作している → SQLを書かずに済んでいる」という錯覚に陥ってしまうのではないかと考えたからです。
繰り返しになりますが、コード上に現れるのは「クラスやオブジェクトだけ」です。
テーブルやカラムの存在は意識する必要がないという点は何度も説明している通りです。


なお、テーブルを作成するDDLは以下のようになります。

-- 書籍
CREATE TABLE book
(
    book_id     INT IDENTITY PRIMARY KEY,
    book_type   VARCHAR(10) NOT NULL,
    book_name   NVARCHAR(50) NOT NULL,
    stock_count INT NOT NULL
)

-- 注文
CREATE TABLE book_order
(
    book_order_id INT IDENTITY PRIMARY KEY,
    customer_name NVARCHAR(16) NOT NULL,
    order_date    DATETIME NOT NULL
)

-- 注文明細
CREATE TABLE book_order_item
(
    book_order_item_id INT IDENTITY PRIMARY KEY,
    book_order_id      INT REFERENCES book_order(book_order_id),
    item_order         INT,
    book_id            INT NOT NULL REFERENCES book(book_id),
    shipping_date      DATETIME NOT NULL
)
NHibernate用のマッピングファイル

次にNHibernateで必要なマッピングファイルを見ていきましょう。
これを正しく作成しておかないとNHibernateはきちんと動作してくれません。
このサンプルプログラムでは「書籍クラス用」「注文クラス用」「注文明細クラス用」と3つのマッピングファイルを作成しています。


まずは書籍クラス用のマッピングファイル(Book.hbm.xml)です。

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
    namespace="Junichi.Ito.NHibernateBookStore" assembly="NHibernateBookStoreLib">

	<!-- 書籍 -->
	<class name="Book" table="book">
		<!-- 書籍ID -->
		<id name="ID" type="Int32" column="book_id" unsaved-value="0">
			<generator class="identity"/>
		</id>

		<!-- クラスの識別子(DB上のカラム) -->
		<discriminator column="book_type" type="String"/>

		<!-- 書籍名 -->
		<property name="BookName">
			<column name="book_name" length="50" not-null="true" />
		</property>

		<!-- 在庫数 -->
		<property name="StockCount">
			<column name="stock_count" not-null="true" />
		</property>

		<!-- クラスの識別子(DB上の値) -->
		<!-- 和書 -->
		<subclass name="JapaneseBook" discriminator-value="JAPANESE" />
		<!-- 洋書 -->
		<subclass name="ForeignBook" discriminator-value="FOREIGN" />
	</class>

</hibernate-mapping>


細かいところまで理解する必要はありませんが、クラスとテーブル、プロパティとカラムの対応関係をそれぞれマッピングしていることが何となく読み取れるかと思います。
また、最後のセクションでは和書と洋書を切り分ける識別子の設定も行われています。
NHibernateが適切なクラスを使い分ける秘密はここにあったわけです。


では次に注文クラス用のマッピングファイル(BookOrder.hbm.xml)を見てみましょう。

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
    namespace="Junichi.Ito.NHibernateBookStore" assembly="NHibernateBookStoreLib">

	<!-- 注文 -->
	<class name="BookOrder" table="book_order">
		<!-- 注文ID -->
		<id name="ID" type="Int32" column="book_order_id" unsaved-value="0">
			<generator class="identity"/>
		</id>

		<!-- 注文者 -->
		<property name="CustomerName">
			<column name="customer_name" length="16" not-null="true" />
		</property>

		<!-- 注文日 -->
		<property name="OrderDate">
			<column name="order_date" not-null="true" />
		</property>

		<!-- 注文明細 -->
		<list name="Items" cascade="all">
			<key column="book_order_id"/>
			<index column="item_order" />
			<one-to-many class="BookOrderItem"/>
		</list>
	</class>

</hibernate-mapping>


こちらも先ほどと同様、対応関係が色々と定義されています。
注文明細の設定部分ではcascade属性が"all"となっているため、注文オブジェクトを保存したり削除したりすると、注文明細オブジェクトも自動的に保存・削除されることになります。


では、最後に注文明細クラス用のマッピングファイル(BookOrderItem.hbm.xml)です。

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
    namespace="Junichi.Ito.NHibernateBookStore" assembly="NHibernateBookStoreLib">
	
	<!-- 注文明細 -->
	<class name="BookOrderItem" table="book_order_item">
		<!-- 注文明細ID -->
		<id name="ID" type="Int32" column="book_order_item_id" unsaved-value="0">
			<generator class="identity"/>
		</id>

		<!-- 発送予定日 -->
		<property name="ShippingDate">
			<column name="shipping_date" not-null="true" />
		</property>

		<!-- 書籍 -->
		<many-to-one name="Book" class="Book" column="book_id"/>		
	</class>
	
</hibernate-mapping>


こちらのファイルでは書籍クラスとの関係が定義されています。
"many-to-one"なので、注文明細と書籍の関係は「多対一」になっていることが分かります。


ちなみにこれらのマッピングファイルはVisual Studio上のファイルプロパティで「Build Action = Embedded Resource」としておかないと動作しないので注意してください。

ASP.NET側で必要な設定

ASP.NET側ではWeb.configでNHibernate用にデータベースの接続情報等を定義する必要があります。

<?xml version="1.0"?>
<configuration>
	<configSections>
		<section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate"/>
	</configSections>

	<!-- Hibernate用の設定情報 -->
	<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
		<session-factory>
			<!-- Consoleに実行したSQLを出力する。
                             ただし、Global.asax内で出力先をConsoleからDebugウインドウに変更している。 -->
			<property name="show_sql">true</property>
			
			<!-- 使用するSQL方言(ここではSQL Server2005) -->
			<property name="dialect">
				NHibernate.Dialect.MsSql2005Dialect
			</property>

			<!-- ConnectionProvider(デフォルトのまま) -->
			<property name="connection.provider">
				NHibernate.Connection.DriverConnectionProvider
			</property>

			<!-- 接続文字列 -->
			<property name="connection.connection_string">
				Server=xxx\xxx;initial catalog=xxx;Integrated Security=True
			</property>

			<!-- ProxyFactoryクラス(デフォルトのまま) -->
			<property name="proxyfactory.factory_class">
				NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu
			</property>

			<!-- 永続化クラスが存在するアセンブリ名 -->
			<mapping assembly="NHibernateBookStoreLib" />
		</session-factory>
	</hibernate-configuration>
</configuration>


SQL方言や接続文字列、マッピングファイルや永続化クラスが存在するアセンブリ名等はプログラムに合わせて変更する必要があります。


最後にglobal.asaxでNHibernate用の共通処理を実装します。

<%@ Application Language="C#" %>
<%@ Import Namespace="Junichi.Ito.NHibernateBookStore" %>

<script runat="server">
    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        // ASP.NETの場合、NHibernateSQLがConsole出力から確認できないため
        // 出力先をConsoleからDebugウインドウに変更する
        NHibernateHelper.SetConsoleOutToDebugWindow();
    }       
    
    protected void Application_EndRequest(object sender, EventArgs e)
    {
        // リクエストの処理終了時にSessionをクローズする
        NHibernateHelper.CloseSession();
    }
</script>


Application_BeginRequestで行っているのは開発者向けの設定変更で、なくてもプログラムの実行に支障はありません。
Application_EndRequestで行っているのはSessionのクローズです。
SessionFactoryの作成やSessionのオープンはNHibernateHelperが自動的に行ってくれますが、クローズは明示的に行う必要があります。
NHibernateのLazyLoading機能を活用したいので、Sessionは最後の最後までクローズせずにおいておきます。


なお、NHibernateHelperはこちらで作成したヘルパークラスです。
実装コードはPart6に載せてあります。

発行されるSQLの確認

それでは最後に「書籍を注文する」際にNHibernateが発行するSQLを確認してみましょう。
在庫が存在する和書と洋書を一冊ずつ注文したときのSQLは以下のようになります(コメントはこちらで付けました)。

-- 書籍を取得する
SELECT book0_.book_id as book1_1_0_, book0_.book_name as book3_1_0_, book0_.stock_count as stock4_1_0_, book0_.book_type as book2_1_0_ FROM book book0_ WHERE book0_.book_id=@p0;@p0 = 1241
SELECT book0_.book_id as book1_1_0_, book0_.book_name as book3_1_0_, book0_.stock_count as stock4_1_0_, book0_.book_type as book2_1_0_ FROM book book0_ WHERE book0_.book_id=@p0;@p0 = 1238

-- 注文を登録する
INSERT INTO book_order (customer_name, order_date) VALUES (@p0, @p1); select SCOPE_IDENTITY();@p0 = '伊藤淳一', @p1 = 2010/12/24 7:23:21

-- 注文明細を登録する
INSERT INTO book_order_item (shipping_date, book_id) VALUES (@p0, @p1); select SCOPE_IDENTITY();@p0 = 2010/12/27 0:00:00, @p1 = 1241
INSERT INTO book_order_item (shipping_date, book_id) VALUES (@p0, @p1); select SCOPE_IDENTITY();@p0 = 2010/12/24 0:00:00, @p1 = 1238

-- 書籍の在庫数を更新する
UPDATE book SET book_name = @p0, stock_count = @p1 WHERE book_id = @p2;@p0 = 'Design Pattern', @p1 = 9, @p2 = 1241
UPDATE book SET book_name = @p0, stock_count = @p1 WHERE book_id = @p2;@p0 = 'はじめてのC', @p1 = 4, @p2 = 1238

-- 注文明細に注文を関連づける
UPDATE book_order_item SET book_order_id = @p0, item_order = @p1 WHERE book_order_item_id = @p2;@p0 = 453, @p1 = 0, @p2 = 698
UPDATE book_order_item SET book_order_id = @p0, item_order = @p1 WHERE book_order_item_id = @p2;@p0 = 453, @p1 = 1, @p2 = 699


個人的には特に大きな問題はないと思うのですが、どうなんでしょうねえ?
こういうSQLを見せたりすると、色々と発行されたSQLのあら探しをして「だからO/Rマッピングツールは使えない!」とか言い切っちゃう人が出てきたりするんでしょうか?
あまり深追いすると宗教論争みたいになってしまうので、とりあえずここではSQLを簡単に紹介するだけで終わっておきます。


今回はNHibernateとデータベースの関係を中心に説明しました。
HibernateNHibernateの紹介記事はたいていコードを見せる前にまずこういう情報から説明していくことが多いですが、O/Rマッピングツールが誤解されないよう、あえて最後の方に持ってきたという点を理解してもらえると嬉しいです。


次回はNHibernateに関する補足説明とこれまでのまとめを行いたいと思います。


Part8を読む