<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE document PUBLIC
  "-//Apache Software Foundation//DTD XDOC 1.0//EN"
  "http://maven.apache.org/dtd/xdoc_1_0.dtd">

<document>
  <properties>
    <title>データアクセスオブジェクトのテストと実装 - DAO Test &amp; Implement</title>
    <author email="kurokkie@users.sourceforge.jp">kurokkie</author>
  </properties>
  <body>
    <section name="データアクセスオブジェクトのテストと実装">
      <p>
データアクセスオブジェクトのテストと実装を作成します。ここでは、<strong>テストファースト</strong> による開
発プロセスを採用しています。
      </p>
    </section>
    <section name="テストケース">
      <p>
データアクセスインターフェースに対し、JUnit を使った単体テストコードを作成します。インターフェースに対してテスト
を行う必要があるため、実装に依存するようなテストコードを書かないように注意する必要があります。このテストコード
はデータアクセス実装とは無関係に利用することができます。
      </p>
      <p>
<a href="apidocs/org/unitedfront2/dao/BlogDao.html"><code>BlogDao</code></a> は
<code>register</code> 、<code>find</code> 、<code>update</code> 、<code>delete</code>
といった基本メソッドを持つ一般的なデータアクセスインターフェースです。テストケース <code>BlogDaoTest</code>
は基本的なテストケースを既に実装している <a href="xref-test/org/unitedfront2/test/DaoTestCaseSupport.html"><code>DaoTestCaseSupport</code></a>
を継承して作成します。テスト用のドメインオブジェクトを生成する <code>createTestDomain</code> メソッド
と、テスト用ドメインオブジェクトを変更する <code>editTestDomain</code> メソッドなどをオーバーライドする必
要があります。<code>DaoTestCaseSupport</code> は <a href="xref-test/org/unitedfront2/test/TransactionalTestCaseWithInitialData.html"><code>TransactionalTestCaseWithInitialData</code></a>
を継承しており、いくつかのテストデータが利用可能な状態になっています。例えば変数 <code>simpleUser1</code>
はテスト用のユーザデータです。
      </p>
<pre name="code" class="java">
public class BlogDaoTest extends DaoTestCaseSupport&lt;Blog&gt; {

    @Autowired
    private BlogDao blogDao;

    @Autowired
    private MessageTable messageTable;

    @Override
    protected Blog createTestDomain() {
        AccessControl readAccessControl = domainFactory.prototype(Public.class);
        AccessControl writeAccessControl = domainFactory.prototype(Public.class);

        // ドメインファクトリを使っていない点に注意。
        // データアクセスオブジェクトのテストでは、データアクセスを伴うドメインモデルの機能は使わない。
        return new Blog(
                "testblog", createOverview(readAccessControl, writeAccessControl),
                simpleUser1.getId(), readAccessControl, writeAccessControl,
                domainFactory.prototype(UserOnly.class));
    }

    private Message createOverview(AccessControl readAccessControl,
            AccessControl writeAccessControl) {
        Message overview = domainFactory.prototype(
                new Message(null, simpleUser1.getId(), simpleUser1.getId(),
                        readAccessControl, writeAccessControl));
        overview.setSubject("テストブログ", Locale.JAPANESE);
        overview.setBody("これはテストブログです。", Locale.JAPANESE);
        return overview;
    }

    @Override
    protected void editTestDomain(Blog blog) {
        blog.getOverview().setSubject("編集後のテストブログ", Locale.JAPANESE);
        blog.getOverview().setBody("編集後のテストブログです。", Locale.JAPANESE);
        blog.getReadAccessControl().changeTo(UserOnly.class);
        blog.getCommentAccessControl().changeTo(FriendOnly.class);
    }

    @Override
    protected Object getDao() {
        return blogDao;
    }

    // test methods ...
}
</pre>
      <p>
テストメソッド名は次のように命名します。メソッド名に日本語を含んでいますが、テストコードとしては問題ありません。
      </p>
<source>
test${テストするメソッド名}${引数を表す文字列（任意）}${確認する事項（任意、日本語でもよい）}
</source>
      <p>
ここでは、<code>findByCode</code> メソッドに対するテストケース
<code>testFindByCode見つかる</code> および <code>testFindByCode見つからない</code> を紹
介します。<code>BlogDaoTest</code> の完全なコードは
<a href="xref-test/org/unitedfront2/dao/BlogDaoTest.html"><code>BlogDaoTest</code> のソースコード</a>
を参照してください。
      </p>
<pre name="code" class="java">
public class BlogDaoTest ... {

    // ...

    @Test
    public void testFindByCode見つかる() throws BlogCodeUsedByOtherException {
        Blog expected = createDomain();
        blogDao.register(expected);
        Blog actual = blogDao.findByCode(expected.getCode());
        Assert.assertEquals(expected, actual);
    }

    @Test
    public void testFindByCode見つからない() throws BlogCodeUsedByOtherException {
        Blog actual = blogDao.findByCode("-1");
        Assert.assertNull(actual);
    }

    // ...
}
</pre>
    </section>
    <section name="データベーススキーマ">
      <p>
次に、データアクセスの実装に入っていきます。まずはデータベーススキーマを設計し、
<a href="http://svn.sourceforge.jp/cgi-bin/viewcvs.cgi/trunk/src/main/sql/unitedfront2-ddl.sql?root=unitedfront2&amp;view=markup">unitedfront2-ddl.sql</a>
に追加します。ここでは次のようなデータベーススキーマを定義しました。
      </p>
<pre name="code" class="sql">
-- ブログ表
CREATE TABLE Blog (
    Id INTEGER NOT NULL AUTO_INCREMENT,      -- ID
    Code CHARACTER VARYING(32) NOT NULL,     -- コード
    OwnerId INTEGER,                         -- 所有者 ID
    OverviewId INTEGER NOT NULL,             -- 概要（メッセージ ID）
    ReadAccessControlId INTEGER NOT NULL,    -- 参照権限 ID
    WriteAccessControlId INTEGER NOT NULL,   -- 編集権限 ID
    CommentAccessControlId INTEGER NOT NULL, -- コメント投稿権限 ID
    PRIMARY KEY (Id),
    UNIQUE (Code),
    CONSTRAINT FK_Blog_OwnerId FOREIGN KEY (OwnerId) REFERENCES User(Id) ON DELETE SET NULL,
    CONSTRAINT FK_Blog_OverviewId FOREIGN KEY (OverviewId) REFERENCES Message(Id),
    CONSTRAINT FK_Blog_ReadAccessControlId FOREIGN KEY (ReadAccessControlId) REFERENCES AccessControl(Id),
    CONSTRAINT FK_Blog_WriteAccessControlId FOREIGN KEY (WriteAccessControlId) REFERENCES AccessControl(Id),
    CONSTRAINT FK_Blog_CommentAccessControlId FOREIGN KEY (CommentAccessControlId) REFERENCES AccessControl(Id)
);

-- ブログ記事表
CREATE TABLE BlogEntry (
    EntryId INTEGER NOT NULL,  -- 記事 ID
    BlogId INTEGER NOT NULL,   -- ブログ ID
    PRIMARY KEY (EntryId),
    INDEX(BlogId),
    CONSTRAINT FK_BlogEntry_EntryId FOREIGN KEY (EntryId) REFERENCES Message(Id) ON DELETE CASCADE,
    CONSTRAINT FK_BlogEntry_BlogId FOREIGN KEY (BlogId) REFERENCES Blog(Id)
);

-- ブログコメント表
CREATE TABLE BlogComment (
    CommentId INTEGER NOT NULL,  -- コメント ID
    EntryId INTEGER NOT NULL,    -- ブログ ID
    PRIMARY KEY (CommentId),
    INDEX(EntryId),
    CONSTRAINT FK_BlogComment_CommentId FOREIGN KEY (CommentId) REFERENCES Comment(Id) ON DELETE CASCADE,
    CONSTRAINT FK_BlogComment_EntryId FOREIGN KEY (EntryId) REFERENCES BlogEntry(EntryId)
);
</pre>
      <p>
次の Maven コマンドの実行してテスト用データベースを更新します。
      </p>
<source>
mvn process-test-resources
</source>
    </section>
    <section name="データアクセス実装">
      <p>
データベースアクセス実装クラスである
<a href="apidocs/org/unitedfront2/dao/jdbc/BlogDaoImpl.html"><code>BlogDaoImpl</code></a>
を作成します。<a href="apidocs/org/unitedfront2/dao/jdbc/SimpleDaoSupport.html"><code>SimpleDaoSupport</code></a>
を継承することで、基本的なメソッドを再利用しています。データベースから取得した値をドメインオブジェクトにマッピング
するマッパークラス <code>BlogRowMapper</code> を内部クラスとして定義します。
      </p>
      <p>
マッパークラスの生成には <code>createRowMapper</code> メソッドを使用します。<code>createRowMapper</code>
は <a href="apidocs/org/unitedfront2/dao/jdbc/RowMapperFactory.html"><code>RowMapperFactory</code></a>
を実装したメソッドで、他のデータアクセス実装クラスがブログマッパーオブジェクトを使いたい場合に利用されます。
      </p>
<pre name="code" class="java">
@Repository(value = "blogDao")
public class BlogDaoImpl extends SimpleDaoSupport&lt;Blog&gt; implements BlogDao {

    /** ブログマッパークラス */
    private static class BlogRowMapper implements ParameterizedRowMapper&lt;Blog&gt; {

        /** ブログファクトリ */
        private DomainFactory domainFactory;

        /** メッセージテーブル */
        private MessageTable messageTable;

        /** アクセス制御テーブル */
        private AccessControlTable accessControlTable;

        public BlogRowMapper(DomainFactory domainFactory,
                MessageTable messageTable,
                AccessControlTable accessControlTable) {
            super();
            this.domainFactory = domainFactory;
            this.messageTable = messageTable;
            this.accessControlTable = accessControlTable;
        }

        // O / R マッピング
        @Override
        public Blog mapRow(ResultSet rs, int rowNum) throws SQLException {
            Blog blog = domainFactory.prototype(Blog.class);
            blog.setId(rs.getInt("Id"));
            blog.setCode(rs.getString("Code"));
            blog.setOwnerId(rs.getInt("OwnerId"));
            blog.setOverview(messageTable.find(rs.getInt("OverviewId")));
            blog.setReadAccessControl(accessControlTable.find(rs.getInt(
                    "ReadAccessControlId")));
            blog.setWriteAccessControl(accessControlTable.find(rs.getInt(
                    "WriteAccessControlId")));
            blog.setCommentAccessControl(accessControlTable.find(rs.getInt(
                    "CommentAccessControlId")));
            return blog;
        }
    }

    /** メッセージテーブル */
    @Autowired private MessageTable messageTable;

    /** アクセス制御テーブル */
    @Autowired private AccessControlTable accessControlTable;

    @Override
    public ParameterizedRowMapper&lt;Blog&gt; createRowMapper() {
        return new BlogRowMapper(getDomainFactory(), messageTable,
                accessControlTable);
    }

    @Override
    protected Class&lt;Blog&gt; getDomainClass() {
        return Blog.class;
    }

    // ドメインオブジェクトのプロパティをデータベースの列名と値のマップに変換します。
    // ドメインオブジェクトのプロパティ名と値がそのままデータベースの列名と値に対応する場合、
    // このメソッドをオーバーライドする必要はありませんが、ブログのように他のドメインオブジェクトを
    // プロパティに持つ場合などはそれらを追加するようサブクラスで再実装する必要があります。
    @Override
    protected Map&lt;String, Object&gt; toColumnValueMap(Blog blog) {
        Map&lt;String, Object&gt; map = super.toColumnValueMap(blog);
        map.put("OverviewId", blog.getOverview().getId());
        map.put("ReadAccessControlId", blog.getReadAccessControl().getId());
        map.put("WriteAccessControlId", blog.getWriteAccessControl().getId());
        map.put("CommentAccessControlId",
                blog.getCommentAccessControl().getId());
        return map;
    }

    // ...
}
</pre>
      <p>
次に <a href="apidocs/org/unitedfront2/dao/BlogDao.html"><code>BlogDao</code></a>
インターフェース実装を記述します。ここでは <code>register</code> , <code>findByCode</code> ,
<code>update</code> ,  <code>delete</code> の実装を紹介します。プロパティがドメインオブジェクト
の場合はそれぞれのドメインオブジェクトの <<code>storre</code> メソッドを呼び出している点に注意してくださ
い。<a href="apidocs/org/unitedfront2/dao/jdbc/BlogDaoImpl.html"><code>BlogDaoImpl</code></a>
の完全なコードは <a href="xref/org/unitedfront2/dao/jdbc/BlogDaoImpl.html"><code>BlogDaoImpl</code> のソースコード</a>
を参照ください。
      </p>
<pre name="code" class="java">
public class BlogDaoImpl ... {

    // ...

    // プロパティがドメインモデルである場合は store メソッドで永続化
    @Override
    public void register(Blog blog) {
        try {
            blog.getOverview().store();
        } catch (MessageCodeUsedByOtherException e) {
            // メッセージコードは自動発行させるため、起こりえない
            logger.error(e);
            throw new IllegalStateException(e);
        }
        blog.getReadAccessControl().store();
        blog.getWriteAccessControl().store();
        blog.getCommentAccessControl().store();
        super.register(blog);
    }

    @Override
    public Blog findByCode(String code) {
        try {
            return getSimpleJdbcTemplate().queryForObject(
                "SELECT * FROM Blog WHERE Code = ?", createRowMapper(), code);
        } catch (EmptyResultDataAccessException e) {
            // データが見つからない場合は null
            if (logger.isDebugEnabled()) {
                logger.debug(e.getMessage(), e);
            }
            return null;
        }
    }

    @Override
    public void update(Blog blog) {
        try {
            blog.getOverview().store();
        } catch (MessageCodeUsedByOtherException e) {
            // メッセージコードは変更しないため、起こりえない
            logger.error(e);
            throw new IllegalStateException(e);
        }
        blog.getReadAccessControl().store();
        blog.getWriteAccessControl().store();
        blog.getCommentAccessControl().store();
        super.update(blog);
    }

    @Override
    public void delete(int id) {
        Blog b = find(id);
        super.delete(id);

        // ドメインオブジェクトは極力削除
        try {
            b.getOverview().delete();
        } catch (DataIntegrityViolationException e) {
            logger.debug(e.getMessage());
        }
        try {
            b.getReadAccessControl().delete();
        } catch (DataIntegrityViolationException e) {
            logger.debug(e.getMessage());
        }
        try {
            b.getWriteAccessControl().delete();
        } catch (DataIntegrityViolationException e) {
            logger.debug(e.getMessage());
        }
        try {
            b.getCommentAccessControl().delete();
        } catch (DataIntegrityViolationException e) {
            logger.debug(e.getMessage());
        }
    }

    // ...
}
</pre>

<script type="text/javascript" src="js/dp.SyntaxHighlighter/Scripts/shCore.js"></script>
<script type="text/javascript" src="js/dp.SyntaxHighlighter/Scripts/shBrushJava.js"></script>
<script type="text/javascript" src="js/dp.SyntaxHighlighter/Scripts/shBrushSql.js"></script>
<script type="text/javascript" src="js/dp.SyntaxHighlighter/Scripts/shBrushXml.js"></script>
<script type="text/javascript">
dp.SyntaxHighlighter.ClipboardSwf = 'js/dp.SyntaxHighlighter/Scripts/clipboard.swf';
dp.SyntaxHighlighter.HighlightAll('code');
</script>

<script type="text/javascript">
var ID='100354378-sbs_daoTestImpl';
var AD=1;
var FRAME=0;
</script>
<script src="http://j1.ax.xrea.com/l.j?id=100354378" type="text/javascript"></script>
<noscript>
<a href="http://w1.ax.xrea.com/c.f?id=100354378" target="_blank"><img src="http://w1.ax.xrea.com/l.f?id=100354378&amp;url=X" alt="AX" border="0"/></a>
</noscript>

    </section>
  </body>
</document>
