第3回 データベースと連携させよう!

第3回 データベースと連携させよう!

連載最終回は、前回作成したショッピングサイトの注文機能とデータベースを連携させます。
前回は、注文機能のユースケースを考え、それぞれのユースケースについて実装を行い、RESTful Webサービスを作成しました。今回はそこにデータベースを組み合わせて、もう一歩進んだ実践的なRESTful Webサービス構築の解説にするつもりです。
なお、データベースにはMySQL、ORMにはMyBatisという構成で進めたいと思います。



事前準備

事前にMySQLのインストールを済ませておいてください。インストールするバージョンは「5.1.62」にしました。筆者は、mysql-5.1.62-osx10.6-x86_64.dmgをインストールしています。紙面の都合上、インストール方法は割愛します。

インストールが済んだら、データベースを作成しておきます。下記のコマンドは、「/usr/local/mysql」にMySQLがインストールされていることを想定しています。

$ /usr/local/mysql/bin/mysql -u root -p
mysql> create database jaxrs;
Query OK, 1 row affected (0.00 sec)
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| jaxrs              |
| mysql              |
| performance_schema |
| test               |
+--------------------+
5 rows in set (0.09 sec)
mysql> exit

データベース連携には、Spring Framework+MyBatisを利用します。それぞれのアーキテクチャに関する詳細な仕様は、公式ドキュメントをご参照ください。

依存ライブラリの追加

データベースと連携するにあたり、まずはpom.xmlに対して必要なライブラリの依存を設定しましょう。赤字部分が、今回追加した箇所になります。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    ・・・省略・・・
    <dependencies>
    ・・・省略・・・
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.0.6</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.18</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.0.2</version>
        </dependency>
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>3.0.6.RELEASE</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.codehaus.jettison</groupId>
            <artifactId>jettison</artifactId>
            <version>1.3.1</version>
        </dependency>
    </dependencies>
    <build>
        <finalName>jax-rs</finalName>
    </build>
</project>

アプリケーションからMyBatisを利用するための「mybatis」、Spring FrameworkとMyBatisを連携させるための「mybatis-spring」、データベースへ接続するための「mysql-connector-java」と「commons-dbcp」,JSON形式のデータをJavaで手軽に扱うための「jettison」を追加しています。「spring-test」は、後ほどテストコードを記述するときに必要になるので追加しています。

Bean定義ファイルの修正

続いて、applicationContext.xmlを編集して、データソースを追加します。赤字部分が、今回追加した箇所になります。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:jaxrs="http://cxf.apache.org/jaxrs"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
    http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd">

    ・・・省略・・・

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost/jaxrs" />
        <property name="username" value="jaxrs" />
        <property name="password" value="jaxrs" />
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="classpath:mybatis-config.xml" />
        <property name="mapperLocations" value="classpath:mybatis/**/*.xml" />
    </bean>

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>
</beans>

MyBatisの設定

そして、MyBatisがデータベースへアクセスするための設定ファイルを追加します。「src/main/resources」の配下にmybatis-config.xml、mybatis/order.xmlを追加してください。それぞれのファイルは、下記の通りに記述します。

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeAliases>
        <typeAlias type="jp.sample.jaxrs.service.Order" alias="order"/>
    </typeAliases>
</configuration>

mybatis/order.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="order">
    <resultMap type="order" id="result">
        <id column="order_no"   property="orderNo"/>
        <result column="user_name"  property="userName"/>
        <result column="item_name"  property="itemName"/>
        <result column="order_date" property="orderDate"/>
    </resultMap>
</mapper>

以上で、データベース連携に必要な、最低限の設定は完了です。これらをベースとして、アプリケーションに手を加えていきます。

テーブルの作成

さて、データベースは作成したものの、まだテーブルを作成していませんでした。MySQL上に、注文情報のテーブルを作成しましょう。

$ /usr/local/mysql/bin/mysql -u root -p
mysql> use jaxrs;
Database changed
mysql> CREATE TABLE order_tbl (
    -> order_no int(11) unsigned NOT NULL AUTO_INCREMENT,
    -> user_name varchar(20) NOT NULL DEFAULT '',
    -> item_name varchar(20) NOT NULL DEFAULT '',
    -> order_date datetime DEFAULT NULL,
    ->   PRIMARY KEY (`order_no`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.09 sec)

mysql> show tables;
+-----------------+
| Tables_in_jaxrs |
+-----------------+
| order_tbl       |
+-----------------+
1 row in set (0.00 sec)

テーブル作成のついでに、アプリケーションからデータベースへアクセスするためのユーザーを作成しておきましょう。

mysql> use mysql;
mysql> grant all PRIVILEGES ON *.* TO jaxrs@localhost IDENTIFIED by 'jaxrs' with grant option;
mysql> exit

注文機能をデータベースと連携する

いよいよ本題に入ります。注文機能のユースケースは、前回から変更はありません。下記のユースケースになります。

  • 商品を注文する
  • 注文した商品を確認する
  • 注文した商品を更新する
  • 注文した商品を取り消す
  • 注文した商品の履歴を見る

また、テストケースも変更はありませんので、前回作成したものをそのまま利用します

注文処理の修正

はじめに、DAOインターフェースを作成しましょう。このインターフェースに、データベースに対する振る舞いを記述します。まずは、「注文する」というユースケースが使用する、データ作成の振る舞いから記述します。

public interface Dao<T> {

    T create(T entity);
}

そして、このインターフェースの具象クラスを作成します。クラス名は、OrderDaoとします。クラスの中身は、以下のように記述します。

@Repository
public class OrderDao extends SqlSessionDaoSupport implements Dao<Order> {

    @Override
    public Order create(Order order) {
        getSqlSession().insert("order.create", order);
        return order;
    }
}

org.apache.ibatis.session.SqlSession#insertを使って、第2引数に指定したオブジェクトをインサートする処理になります。第1引数は、order.xmlのnamespace+insertタグのidです。
最後に、order.xmlにSQL文を追加します。赤字部分が、追加したSQLになります。

<mapper namespace="order">
    <resultMap type="order" id="result">
        <id column="order_no"   property="orderNo"/>
        <result column="user_name"  property="userName"/>
        <result column="item_name"  property="itemName"/>
        <result column="order_date" property="orderDate"/>
    </resultMap>

    <insert id="create" parameterType="order"
        useGeneratedKeys="true" keyProperty="orderNo">
        insert into order_tbl (user_name, item_name, order_date)
        values (#{userName}, #{itemName}, #{orderDate})
    </insert>
</mapper>

動作確認のためのテストコードを掲載しておきますので、正しく動作するか確認してみてください。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "file:src/main/webapp/WEB-INF/spring/applicationContext.xml" })
@Transactional
public class OrderTest {

    @Resource
    private OrderDaoorderDao;

    @Test
    public void testOrderItem01() {

        Order order = new Order();
        order.setUserName("Tarou");
        order.setItemName("item01");

        Order actual = orderDao.create(order);

        Assert.assertNotNull(actual);
        Assert.assertNotNull(actual.getOrderNo());
        Assert.assertEquals(order.getUserName(), actual.getUserName());
        Assert.assertEquals(order.getItemName(), actual.getItemName());
    }
}

これで、データベースへアクセスできるようになったので、前回作成したItemOrderResource#createを修正して、データベースに注文情報をインサートできるようにしてみましょう。
まずは、OrderDaoをDIします。ItemOrderResourceに以下のフィールドを定義してください。

@Resource
private Dao orderDao;

そして、createメソッドを、下記の通りに修正します。

@Override
public Order create(Order order) {
    return orderDao.create(order);
}

修正が終わったら、WTPとTomcatを使ってプロジェクトを起動し、テストケース「testOrderItem01」を実行してみてください。はじめてインサートを実行された方はテストが正常に終わるかもしれませんが、何度かインサートを実行された方はエラーになると思います。そこで、テストコードの一部を、下記の通りに修正して再度、テストを実行してみてください。

・・・省略・・・
try {
    String result = IOUtils.readStringFromStream((InputStream) actual.getEntity());
    JSONObjectjson = new JSONObject(result);
    AbstractXMLStreamReader reader = new MappedXMLStreamReader(json);
    reader.next();
    reader.next();
    String orderNo = reader.getText();

    Assert.assertEquals(Status.OK.getStatusCode(), actual.getStatus());
    Assert.assertEquals(
            "{\"order\":{\"orderNo\":" + orderNo
                    + ",\"userName\":\"Tarou\",\"itemName\":\"item01\"}}",
            result);
} catch (Exception e) {
    Assert.fail();
}

今度はテストが正常に完了するはずです。

RESTful Webサービスの構成

ここでいったん、RESTful Webサービスの構成や動作について補足しておきます。全体的な構成については、下図をご参照ください。


図 1 RESTful Webサービス全体図

クライアントがリクエストを行うと(@)、サーブレットがリクエストを受け取ってItemOrderResourceにそれを渡します(A)。ItemOrderResourceはOrderDaoを介して(B)データベースにアクセスして、対象のレコードを取得します(CD)。ItemOrderResourceはデータベースから取得したレコードをクライアントへ返します(EF)。

テストケースでは、org.apache.cxf.jaxrs.client.WebClientが上図のクライアントの役割をしています。

その他のユースケースの修正

それでは、その他のユースケースの機能についても、データベースと連携させてみましょう。Daoインターフェースに、それぞれのユースケースで必要になりそうな振る舞いを定義します。赤字部分が追加した振る舞いになります。

public interface Dao<T> {

    T create(T entity);

    T findById(Long id);

    T update(T entity);

    List<T>findAll();
}

そして、OrderDaoに各振る舞いの実装を記述します。赤字部分が、追加した実装です。

@Repository
public class OrderDao extends SqlSessionDaoSupport implements Dao<Order> {

    @Override
    public Order create(Order order) {
        getSqlSession().insert("order.create", order);
        return order;
    }

    @Override
    public Order findById(Long id) {
        return (Order) getSqlSession().selectOne("order.findById", id);
    }

    @Override
    public Order update(Order order) {
        getSqlSession().update("order.update", order);
        return order;
    }

    @Override
    public List<Order>findAll() {
        return getSqlSession().selectList("order.findAll");
    }
}

最後に、order.xmlに各振る舞いが使用するSQL文を追加します。赤字部分が、追加したSQLになります。

<mapper namespace="order">
    <resultMap type="order" id="result">
        <id column="order_no"   property="orderNo"/>
        <result column="user_name"  property="userName"/>
        <result column="item_name"  property="itemName"/>
        <result column="order_date" property="orderDate"/>
    </resultMap>

    <insert id="create" parameterType="order"
        useGeneratedKeys="true" keyProperty="orderNo">
        insert into order_tbl (user_name, item_name, order_date)
        values (#{userName}, #{itemName}, #{orderDate})
    </insert>

    <select id="findById" parameterType="long" resultMap="result">
        select * from order_tbl where order_no = #{id}
    </select>

    <update id="update" parameterType="order">
        update order_tbl
        <set>
            <if test="userName != null">user_name = #{userName},</if>
            <if test="itemName != null">item_name = #{itemName},</if>
            <if test="orderDate != null">order_date = #{orderDate}</if>
        </set>
        where order_no = #{orderNo}
    </update>

    <select id="findAll" resultMap="result">
        select * from order_tbl
    </select>
</mapper>

以上で、各ユースケースがデータベースへアクセスするための振る舞いを定義できました。ItemOrderResourceの各メソッドから呼び出してみましょう。


注文した商品を確認する

@Override
public Order getOrder(Long orderNo) {
    logger.info(String.format("orderNo=%d", orderNo));
    return orderDao.findById(orderNo);
}

注文した商品を更新する

@Override
public Order update(Order order) {
    Order result = orderDao.update(order);
    return orderDao.findById(result.getOrderNo());
}

注文した商品を取り消す

@Override
public Order cancel(Long orderNo) {
    logger.info(String.format("orderNo=%d", orderNo));

    Date orderDate = new Date();

    Order order = new Order();
    order.setOrderNo(orderNo);
    order.setOrderDate(orderDate); // キャンセルを表す項目がないので、注文日の更新で代替している
    Order result = orderDao.update(order);
    return orderDao.findById(result.getOrderNo());
}

注文した商品の履歴を見る

@Override
public List<Order> history() {
    return orderDao.findAll();
}

各テストケースも修正しておきましょう。コードが長くなりますので、添付ファイルにしておきました。

おわりに

最終回はいかがだったでしょうか。データベース自体のセットアップが必要なものの、アプリケーションと連携させることは簡単だったと思います。今回はMyBatis+MySQLという構成で説明しましたが、他のORMやデータベースでも少ない変更で利用可能だと思います。また、注文情報については、もっと改良の余地があると思います。今回の連載では、1回の注文の中で複数の商品を扱えなかったり、注文取消しを表現する情報がなかったりと、不足している点が多くあります。他に、処理中にトランザクションを組み込んでいません(一応、Springの設定では使用できるようにしてあります)。 本連載で使用したプロジェクトは、添付ファイルとして提供させていただきますので、興味の有る方はダウンロードしていろいろ改良してみてください。

さて、今回で「JAX-RSでRESTful Webサービス」の巻は終了です。ご購読いただいた読者の皆さま、そして校正とウェブ化を行っていただいた弊社スタッフに感謝したいと思います。いつかまた、JAX-RSに関して深く掘り下げた内容をお届けできればと思います。お楽しみに!


以上



※.記載されているロゴ、システム名、製品名は各社及び商標権者の登録商標あるいは商標です。