こんにちは。
キャスレーコンサルティングSI(システム・インテグレーション)部の西川です。
前回に引き続きMyBatisを使ってDBアクセスするプログラムをご紹介していきたいと思います。
前回の記事で構築した環境がある前提で進めてまいりますので構築手順などは前回の記事を参照くださいませ。
(前回の記事はコチラ)
今回は以下のテーマでMyBatisを動かしてみたいと思います。
・Mapping機能を使用してDB検索結果をエンティティクラス(Java)の構造に沿って格納されるようにする
・自動生成されたMapperのInsert/Update/Deleteを使ってみる
・動的SQLを使って複数行のInsertができるようにする
それでは早速始めていきましょう。
1.Mapping機能を確認する
まずはMyBatisのキモであるMapper.xmlについて確認してみましょう。
1.1.テーブル構造の確認
MyBatisGeneratorで作成したファイルたちはそれぞれテーブル単位となっています。
sakilaデータベースのactorテーブル近辺のER図は以下のようになっています。
filmテーブルはactorテーブル、categoryテーブルと多対多で繋がっています。
映画は複数の俳優が演じているし、俳優は複数の映画に出演しますね。
カテゴリはアクション・ホラーとかアクション・コメディのような区分けに使うのでしょう。
また、langageテーブルと1対1で繋がっています。
洋画の場合は原語(original_langage_id)が英語で、吹き替え版なら音声(langage_id)が日本語になるのでしょう。
filmテーブルに紐づく情報をすべて取得したい場合、自動生成されたエンティティとMapperだけではfilmテーブルを取得→film_actorテーブルを取得→actorテーブルを取得という風に何度もDB操作をしなければならないため煩雑になってしまいますし、実行速度も心配になります。
そこで、これらのテーブルを結合した状態でオブジェクトを取得できるようなMapperを定義してみましょう。
1.2 エンティティクラスの作成
確認したテーブル構造をもとにJavaのエンティティクラス(DAO)を作ります。
各テーブルのエンティティは既にありますのでFilmクラスを継承したサブクラスを作成して簡単に作成してしまいましょう。
package com.example.entity; import java.util.List; /** * Filmの詳細エンティティ. */ public class FilmDetail extends Film { /** * 出演した俳優のリスト. */ private List<Actor> actorList; /** * カテゴリーのリスト. */ private List<Category> categoryList; /** * 映画の言語. */ private Language language; /** * 原語. */ private Language originalLanguage; /** * 出演した俳優のリストを取得します。 * @return 出演した俳優のリスト */ public List<Actor> getActorList() { return actorList; } /** * カテゴリーのリストを取得します。 * @return カテゴリーのリスト */ public List<Category> getCategoryList() { return categoryList; } /** * 映画の言語を取得します。 * @return 映画の言語 */ public Language getLanguage() { return language; } /** * 映画の言語を設定します。 * @param language 映画の言語 */ public void setLanguage(Language language) { this.language = language; } /** * 原語を取得します。 * @return 原語 */ public Language getOriginalLanguage() { return originalLanguage; } /** * 原語を設定します。 * @param original_Language 原語 */ public void setOriginalLanguage(Language original_Language) { this.originalLanguage = original_Language; } }
1.3 Mapper.xmlの作成
「Mapper.xml」には以下の定義を行うことができます。
- DBから取得した値をどのエンティティに格納するかのマッピング定義
- Mapperインターフェースに宣言されたメソッドに対応するSQL
マッピング定義はJavaとSQL両方が準備できてから記述するほうが良いので、SQL→マッピング定義の順で記述していきます。
以下のファイルを/src/main/java/com/example/entityに新規作成します。
<?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="com.example.entity.FilmDetailMapper"> </mapper>
mapperタグのnamespace要素にJavaインターフェースのクラス名を記載します。
これによりMapperインターフェースとMapper.xmlの紐付けが行われます。
続いて内容を追加していきます。
1.3.1 SQL部分の記述
ER図に登場していたテーブルをすべて結合したSQL文です。
検索パラメータとしてshor型のfilmIdを受け取り、BaseResultMap(後述)を戻す、selectByPrimaryKeyというSQLを定義します。
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Short"> SELECT F.film_id, F.title, F.release_year, F.language_id, F.original_language_id, F.rental_duration, F.rental_rate, F.length, F.replacement_cost, F.rating, F.special_features, F.last_update, C.category_id as "category_category_id", C.name as "category_name", C.last_update as "category_last_update", A.actor_id as "actor_actor_id", A.first_name as "actor_first_name", A.last_name as "actor_last_name", A.last_update as "actor_last_update", L1.name as "language_name", L1.last_update as "language_last_update", L2.name as "orglanguage_name", L2.last_update as "orglanguage_last_update" FROM film F INNER JOIN film_actor FA ON FA.film_id = F.film_id INNER JOIN actor A ON A.actor_id = FA.actor_id INNER JOIN film_category FC ON FC.film_id = F.film_id INNER JOIN category C ON C.category_id = FC.category_id INNER JOIN language L1 ON L1.language_id = F.language_id LEFT OUTER JOIN language L2 ON L2.language_id = F.original_language_id WHERE F.film_id = #{filmId,jdbcType=SMALLINT} </select>
selectタグのid要素(この場合selectByPrimaryKey)とJavaインタフェースで宣言するメソッド名は同じである必要があります。
electタグのresultMap要素(この場合はBaseResultMap)はResultMapタグのid要素と一致する必要があります。
カラム名と別名はResultMapタグ内の記述で使用するため、結合したテーブルごとに接頭語をつけています。
F.film_id = #{filmId,jdbcType=SMALLINT}
#{~~}の部分がJavaから渡されるパラメータによって置換される部分になります。
パラメータの記載方法は2種類あり、場所によって使い分ける必要があります。
- #{~~}と記載した場合:値をシングルクォートで囲ったものに置換されます。
- ${~~}と記載した場合:値そのままに置換します。
1.3.2 ResultMap部分の記述
SQLの実行結果をJavaにどのように格納するかを定義します。
<resultMap id="BaseResultMap" type="com.example.entity.FilmDetail" extends="com.example.entity.FilmMapper.ResultMapWithBLOBs"> <association property="language" columnPrefix="language_" resultMap="com.example.entity.LanguageMapper.BaseResultMap" /> <association property="originalLanguage" columnPrefix="orglanguage_" resultMap="com.example.entity.LanguageMapper.BaseResultMap" /> <collection property="actorList" columnPrefix="actor_" ofType="com.example.entity.Actor" resultMap="com.example.entity.ActorMapper.BaseResultMap"/> <collection property="categoryList" columnPrefix="category_" ofType="com.example.entity.Category" resultMap="com.example.entity.CategoryMapper.BaseResultMap"/> </resultMap>
resultMapタグのid要素は先ほど作成したSQLのResultMap要素と一致する必要があります。
resultMapタグのtype要素はSQL結果を格納するJavaのクラスを設定します。
自動生成時の場合、バイナリデータが入る恐れがあるカラムはBaseResultMapのマッピング対象外となります。
バイナリデータ(画像や音声)を含む情報を取得したい場合はResultMapWithBLOBsを使用します(BLOB:Binary Large OBject)。
「file」テーブルのdescriptionはTEXT型のカラムであるため、ResultMapWithBLOBsのほうに追いやられています。BaseResultMapを使うとdescriptionに値が入らないので注意しましょう。
associationタグはマッピング対象が複合クラスである場合に使用します。
associationタグの中にResultタグを記述することも出来ますが、今回はMapping部分を加工する必要がないので既存のResultMapを参照するだけでマッピングできます。
columnPrefix要素を指定するとカラム名に接頭語をつけることができます。
1.3.1のSQLでSELECT文につけていた別名はここで使用するためのものでした。
extendsタグは既存のResultMapを継承する場合に使います。
今回、JavaのエンティティクラスはFilmを継承してFilmDetailを作成しましたのでこれに習ってResultMapもFilmMapperのBaseResutMapを継承しました。
他のXMLファイルに記載されたResultMapを参照したい場合は
{namespase名}.{ResultMap名}
(例:com.example.entity.FilmMapper.BaseResultMap)
で指定することができます。
- association:メンバが複合型の場合はこちらを使用します(String型やDate型を除く)。
- javaTypeにメンバの型を記載します。
- collection:メンバの型が複合型の場合に使用します。1対多の結合をする場合に「多」の部分をListとして格納してくれます。
ofType要素にListの型パラメータを指定し、resultMap要素に使用するResultMapを指定できます。
1.3.3 動作させてみる
以下のプログラムを動かして処理結果を確認します。
public class App3 { public static void main(String[] args) { // resources直下のmybatis-config.xmlを読み込みます try (Reader r = Resources.getResourceAsReader("mybatis-config.xml");) { // 読み込んだ設定ファイルからSqlSessionFactoryを生成します SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(r); // SQLセッションを取得します try (SqlSession session = factory.openSession()) { // ActorテーブルのMapperを取得します FilmDetailMapper map = session.getMapper(FilmDetailMapper.class); FilmDetail dtl = map.selectByPrimaryKey((short) 100); // 結果を表示します System.out.println("dtl.getFilmId : " + dtl.getFilmId()); System.out.println("dtl.getTitle : " + dtl.getTitle()); System.out.println("dtl.getDescription : " + dtl.getDescription()); System.out.println("dtl.getReleaseYear : " + dtl.getReleaseYear()); System.out.println("dtl.getLanguageId : " + dtl.getLanguageId()); System.out.println("dtl.getOriginalLanguage : " + dtl.getOriginalLanguage()); System.out.println("dtl.getRentalDuration : " + dtl.getRentalDuration()); System.out.println("dtl.getRentalRate : " + dtl.getRentalRate()); System.out.println("dtl.getLength : " + dtl.getLength()); System.out.println("dtl.getLanguageId : " + dtl.getLanguageId()); System.out.println("dtl.getOriginalLanguageId : " + dtl.getOriginalLanguageId()); System.out.println("dtl.getReplacementCost : " + dtl.getReplacementCost()); System.out.println("dtl.getRating : " + dtl.getRating()); System.out.println("dtl.getSpecialFeatures : " + dtl.getSpecialFeatures()); System.out.println("dtl.getLastUpdate : " + dtl.getLastUpdate()); System.out.println("--Language--"); Language lang = dtl.getLanguage(); System.out.println(" lang.getLanguageId : " + lang.getLanguageId()); System.out.println(" lang.getName : " + lang.getName()); System.out.println(" lang.getLastUpdate : " + lang.getLastUpdate()); System.out.println("--OriginalLanguage--"); lang = dtl.getOriginalLanguage(); if (lang != null) { System.out.println(" lang.getLanguageId : " + lang.getLanguageId()); System.out.println(" lang.getName : " + lang.getName()); System.out.println(" lang.getLastUpdate : " + lang.getLastUpdate()); } else { System.out.println(" dtl.getOriginalLanguage : null"); } System.out.println("--ActorList--"); for (Actor actor : dtl.getActorList()) { System.out.println(" actor.getActorId : " + actor.getActorId()); System.out.println(" actor.getFirstName : " + actor.getFirstName()); System.out.println(" actor.getLastName : " + actor.getLastName()); System.out.println(" actor.getFilmId : " + actor.getLastUpdate()); System.out.println("------------"); } System.out.println("--CategoryList--"); for (Category cate : dtl.getCategoryList()) { System.out.println(" cate.cate : " + cate.getCategoryId()); System.out.println(" cate.getName : " + cate.getName()); System.out.println(" cate.getLastUpdate : " + cate.getLastUpdate()); System.out.println("----------------"); } } } catch (IOException e) { e.printStackTrace(); } } }
実行結果は以下のとおり表示されました。
dtl.getFilmId : 100
dtl.getTitle : BROOKLYN DESERT
dtl.getDescription : A Beautiful Drama of a Dentist And a Composer who must Battle a Sumo Wrestler in The First Manned Space Station
dtl.getReleaseYear : Sun Jan 01 00:00:00 JST 2006
dtl.getLanguageId : 1
dtl.getOriginalLanguage : null
dtl.getRentalDuration : 7
dtl.getRentalRate : 4.99
dtl.getLength : 161
dtl.getLanguageId : 1
dtl.getOriginalLanguageId : null
dtl.getReplacementCost : 21.99
dtl.getRating : R
dtl.getSpecialFeatures : Commentaries
dtl.getLastUpdate : Wed Feb 15 05:03:42 JST 2006
--Language--
lang.getLanguageId : null
lang.getName : English
lang.getLastUpdate : Wed Feb 15 05:02:19 JST 2006
--OriginalLanguage--
dtl.getOriginalLanguage : null
--ActorList--
actor.getActorId : 41
actor.getFirstName : JODIE
actor.getLastName : DEGENERES
actor.getFilmId : Wed Feb 15 04:34:33 JST 2006
------------
actor.getActorId : 62
actor.getFirstName : JAYNE
actor.getLastName : NEESON
actor.getFilmId : Wed Feb 15 04:34:33 JST 2006
------------
actor.getActorId : 90
actor.getFirstName : SEAN
actor.getLastName : GUINESS
actor.getFilmId : Wed Feb 15 04:34:33 JST 2006
------------
actor.getActorId : 125
actor.getFirstName : ALBERT
actor.getLastName : NOLTE
actor.getFilmId : Wed Feb 15 04:34:33 JST 2006
------------
actor.getActorId : 172
actor.getFirstName : GROUCHO
actor.getLastName : WILLIAMS
actor.getFilmId : Wed Feb 15 04:34:33 JST 2006
------------
--CategoryList--
cate.cate : 9
cate.getName : Foreign
cate.getLastUpdate : Wed Feb 15 04:46:27 JST 2006
----------------
Mapper.xmlに記載したSQLをそのまま実行した場合は、actorの部分以外は同じデータが入った5レコードが抽出されました(一部抜粋)。
これらから今回のようにActorの一覧をとりたい場合は別途SQLを発行するか、Javaで一手間加える必要があるでしょう。
しかし、MyBatisのMappingを使うことでRDBのデータ構造からJavaのクラス構成に沿った形のデータを1発で取得できるようになりました。
このように、Mapping機能をうまく使うと、Java側で使いたい形にデータを配置することができるため、Java側の工数削減につながります。
ここまででデータの取得方法は大まかに確認できたかと思いますので、続いてレコードの操作方法を確認してみましょう。
2.単一レコードの操作
ここでは単一レコードの挿入/更新/削除について確認していきます。
2.1 レコードの挿入(INSERT)
単一テーブルに単一レコードを挿入したい場合は、前回作成したMapperクラスに既に用意されているので簡単に実装することができます。
/** * MyBatisを使ってDBにアクセスするサンプルプログラムです. * */ public class App4 { public static void main(String[] args) { // resources直下のmybatis-config.xmlを読み込みます try (Reader r = Resources.getResourceAsReader("mybatis-config.xml");) { // 読み込んだ設定ファイルからSqlSessionFactoryを生成します SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(r); // SQLセッションを取得します try (SqlSession session = factory.openSession(true)) { // Actorテーブルの独自Mapperを取得します ActorMapper map = session.getMapper(ActorMapper.class); // Actorテーブルに追加する値を設定します Actor actor = new Actor(); actor.setActorId((short) 1234); actor.setFirstName("Taro"); actor.setLastName("Casley"); map.insert(actor); } } catch (IOException e) { e.printStackTrace(); } } }
Java側でやることは追加したいActorのインスタンスを作成し、Mapperクラスのinsertメソッドに引き渡すだけです。
insertメソッドに引き渡されたActorのインスタンスはMapper.xmlの以下の部分に引き渡されます。
<insert id="insert" parameterType="com.example.entity.Actor" > insert into actor (actor_id, first_name, last_name, last_update) values ( #{actorId,jdbcType=SMALLINT}, #{firstName,jdbcType=VARCHAR}, #{lastName,jdbcType=VARCHAR}, #{lastUpdate,jdbcType=TIMESTAMP} ) </insert>
insertタグの要素から解説してきましょう。
idタグはこのタグの名前であり、Mapperクラスのメソッド名と同名となります。
parameterTypeはそのメソッドの引数の型となります。
今回はActorのエンティティクラスを引き渡すので、そのフルパッケージ名が記載されています。
次にタグの中に記載された内容を見ていきます。
タグの中にはSELECT文の時と同様に発行するSQLそのものを記載していきます。
変数の値を#{~~}で囲うのも同様です。
今回はActorを引数の型としているため、Actorに宣言されているプロパティをすべて変数として扱うことができます。
そしてSQL実行時に変数部分が置換されてSQLが発行されます。
実行前後のテーブル情報は以下になります。
実行前
Insert実行後
Javaで設定した値のレコードを追加することができました。
2.2 レコードの更新(UPDATE)
続いて更新を行いましょう。
先ほどのApp4.javaを少し修正します(20行目~27行目です)。
// Actorテーブルに追加する値を設定します Actor actor = new Actor(); actor.setActorId((short) 1234); actor.setFirstName("Hanako"); actor.setLastName("Casley"); actor.setLastUpdate(new Date()); map.updateByPrimaryKey(actor);
上記が実行されると、以下のXMLにActorのデータが引き渡されます。
<update id="updateByPrimaryKey" parameterType="com.example.entity.Actor" > update actor set first_name = #{firstName,jdbcType=VARCHAR}, last_name = #{lastName,jdbcType=VARCHAR}, last_update = #{lastUpdate,jdbcType=TIMESTAMP} where actor_id = #{actorId,jdbcType=SMALLINT} </update>
タグの中身は基本的にはこれまで確認してきた内容と変わりありません。
使用するタグがupdateになるくらいでしょうか。
実行後のテーブルは以下になります。
Update実行後
名前がJavaで指定したものに変更されましたね。
2.3 レコードの削除(DELETE)
最後に削除です。
またApp4.javaの同じところを以下のように修正します。
map.deleteByPrimaryKey((short) 1234);
ここで設定しているのはActorテーブルの主キーであるactor_idの値です。
実行すると以下に引き渡されます。
<delete id="deleteByPrimaryKey" parameterType="java.lang.Short" > delete from actor where actor_id = #{actorId,jdbcType=SMALLINT} </delete>
こちらも同様にタグがdeleteになったこと以外は変更ありません。
実行すると以下になります。
Delete実行後
指定したactor_idのデータが消えました。
3.動的SQL
最後に動的SQLについて確認していきたいと思います。
MyBatisには動的SQLという機能があります。
簡単に言うと、Mapper.xmlに記載したSQLの中で繰り返しや分岐などを表現できる機能です。
今回は繰り返し(foreach)と分岐(if)についてご紹介したいと思います。
3.1 foreachを使って複数行の挿入
複数行の挿入を行いたい場合はforeachタグを使用します。
自動生成したMapperたちには複数行挿入するためのメソッドは追加されていませんので以下を追記します。
/** * 複数のActorを挿入します。 * @param recordList ActorのList * @return 挿入件数 */ int insertList(List<Actor> recordList);
<insert id="insertList" parameterType="java.util.List"> insert into actor (actor_id, first_name, last_name, last_update) values <foreach item="actItem" collection="list" open="" separator="," close="" > ( #{actItem.actorId,jdbcType=SMALLINT}, #{actItem.firstName,jdbcType=VARCHAR}, #{actItem.lastName,jdbcType=VARCHAR}, #{actItem.lastUpdate,jdbcType=TIMESTAMP} ) </foreach> </insert>
単一行と異なり、parameterTypeにList型を宣言します。
そしてその引数を使ってforeachタグを繰り返させることになります。
foreachタグが今回ご紹介する動的SQLの部分です。
まずは要素を確認していきましょう。
item要素は引数のListから1つ要素を取り出した一時変数の名前です。
つまりactItemはActor型の変数となります。
その変数の値を使用するには以下のように記載します。
#{変数名.プロパティ名,jdbc型名}
collection要素にはlist,set,map,arrayなど、どの種類のCollection型なのかを記載します。
open要素にはforeachが始まる直前に追加したい文を記載することができます。
同様にclose要素にはforeachが終わった直後に追加したい文を記載できます。
例えば、foreachの結果を括弧で括りたい場合は
open=”(” close=”)” と記載すればよいわけです。
separatorは繰り返しのタイミングで追加したい文を記載します。
今回は繰り返しの間をカンマ(,)で区切りたいのでそれを指定しています。
この部分を実行するにはApp4.javaを同様に以下のように修正します。
List<Actor> actorList = new ArrayList<>(); // Actorテーブルに追加する値を設定します Actor actor = new Actor(); actor.setActorId((short) 1234); actor.setFirstName("Taro"); actor.setLastName("Casley"); actorList.add(actor); actor = new Actor(); actor.setActorId((short) 5678); actor.setFirstName("Hanako"); actor.setLastName("Casley"); actorList.add(actor); map.insertList(actorList); } } catch (IOException e) { e.printStackTrace(); } } }
実行後は以下になります。
InsertList実行後
2行分のデータを追加することができました。
3.2 ifを使って一部の列だけを更新
続いてifの使用方法について確認しましょう。
自動生成されたMapperには以下のようなupdate文が記載されています。
<update id="updateByPrimaryKeySelective" parameterType="com.example.entity.Actor" > update actor <set > <if test="firstName != null" > first_name = #{firstName,jdbcType=VARCHAR}, </if> <if test="lastName != null" > last_name = #{lastName,jdbcType=VARCHAR}, </if> <if test="lastUpdate != null" > last_update = #{lastUpdate,jdbcType=TIMESTAMP}, </if> </set> where actor_id = #{actorId,jdbcType=SMALLINT} </update>
この中でif文が使用されています。
引数として渡したActorのプロパティがnullだったらその列は更新しないようになっています。
if文を使用するにはifタグを使用します。
test要素に分岐条件を記載します。
test要素の値の中でもfirstNameといったActorのプロパティを使用することができます。
Javaの条件分岐を書くように記載することができるのでわかりやすいですね。
setタグも動的SQLの一種です。
先ほどのSQL、「おや?」と思った方もいらっしゃるかと思います。
last_update = #{lastUpdate,jdbcType=TIMESTAMP},
この部分、カンマ(,)が最後についているのでそのままSQLになるとWHERE句の直前にカンマがつくことになり、SQLエラーになってしまいます。
そこで使うタグがsetタグです。
これはifタグなどの結果でついた不要なゴミを除去してくれるすごいタグです。
今回の場合、最後にカンマが必ずついてくれますのでそれを除去した状態にしてくれます。
これを実行するにはUpdateと同様にApp4.javaを以下のように修正します。
// Actorテーブルに追加する値を設定します Actor actor = new Actor(); actor.setActorId((short) 1234); actor.setFirstName("Jiro"); map.updateByPrimaryKeySelective(actor);
実行すると以下になります。
Update実行後
LastNameを設定せずにFirstNameを更新することが出来ました。
4.最後に
入門編第2回いかがでしたでしょうか。
高度なマッピング機能と動的SQLがあることで、Javaでする必要があった繰り返し処理などをすべてXMLで定義でき、Java側の実装が非常にシンプルになると思います。
これによりJava担当とDB担当のタスクを完全に分けることができ、作業効率の改善につなげることが可能になるのではないでしょうか。
今回ご紹介した機能以外にもまだまだMyBatisの使える機能はたくさんありますので良かったら一度MyBatisで遊んでみてくださいね!