S2JDBC - チュートリアル

S2JDBC-Tutorial-xxx.zipを解凍し、その中にあるs2jdbc-tutorialを Eclipseにインポートしてください。

この時点では、コンパイルエラーが発生しますが問題ありません。 下で説明するエンティティの生成と修正を行うことでコンパイルエラーが解消されます。

このチュートリアルのデータベースは、HSQLDBを組み込みモードで使用しているので、 起動など特に必要ありません。 データの追加や変更をしたい場合は、src/test/resources/data/test.script を適当に変更してください。

エンティティのソースコードは、S2JDBC-Genを使ってデータベース上のテーブル定義から自動生成します。 S2JDBC-Genの実行に必要なjarファイルとAntのビルドファイル(s2jdbc-gen-build.xml)はこのチュートリアルに含まれています。

プロジェクト直下にあるs2jdbc-gen-build.xmlに定義されたgen-entityターゲットを実行してください。 実行方法やその際の注意点については、Antタスクの実行を参照してください。

実行後は、F5を押すなどしてプロジェクトをリフレッシュしてください。 src/main/java/examples/entityの下にエンティティのソースコードが生成されていることを確認できます。

エンティティ以外のソースコードも生成されますが、このチュートリアルでは特に言及しません。 詳細はS2JDBC-Genのドキュメントを参照してください。 自動生成されたコードは、次のアノテーションが付与されているかどうかで見分けられます。

@Generated は自動生成されたことを示すアノテーションです。

エンティティクラスを生成してもまだコンパイルは成功しません。 次の修正を行ってください。

src/main/java/examples/entity/Employee.javaを開き、jobTypeプロパティの型をIntegerからJobTypeに変更します。 JobTypeのソースコードはsrc/main/java/examples/entity/JobType.javaにあらかじめ用意されています。

この修正を行うことでコンパイルが成功します。 以上でセットアップは完了です。

テーブルのデータとJavaのオブジェクトのマッピングは、 エンティティに対してアノテーションで指定します。 エンティティというのは、テーブルの1行に対応するJavaのオブジェクトだと 理解していれば良いでしょう。

それでは、Employeeエンティティを見てみましょう。 src/main/java/examples/entity/Employee.javaを開いてください。

エンティティであることを示すには、@Entityが必要です。 詳しくは、 エンティティ定義 を参照してください。

識別子のフィールドには、@Idをつけます。 識別子をSeasar2に自動生成させる場合は、@GeneratedValueをつけます。 詳しくは、 識別子定義 を参照してください。

Seasar2では、publicフィールドを使ってシンプルにプロパティを定義することができます。 詳しくは、 シンプルなプロパティ を参照してください。

カラム名とプロパティ名が同じなら、カラム用のアノテーションは特に必要ありません。 また、AAA_BBBのようなカラム名用の'_'記法を、 aaaBbbのようなプロパティ名用のキャメル記法へ変換することも Seasar2によって自動的に行われるので、 アノテーションを指定する必要はありません。 詳しくは、 カラム定義 を参照してください。

JobTypeは次のような列挙型です。 実際のソースではもう少し複雑ですが、 わかりやすくするために今回は簡略化しています。

job_typeカラムを文字列で定義しておけば、 カラムには、'CLERK'のように文字列として格納され、 エンティティでは、列挙型に自動的にマッピングされます。

EmployeeとDepartmentには、多対一の関連があり、次のように定義されています。

逆の立場から見ると、DepartmentとEmployeeは一対多の関連があり、 次のように定義されています。

   employeeList; ]]>

mappedBy属性によって関連の所有者側のプロパティを指定します。 関連の所有者側とは、外部キーを持っているほうを意味します。 今回のケースは、department_idという外部キー(プロパティ名はdepartmentId)をEmployeeが 持っているのでEmployeeが関連の所有者になります。 mappedBy属性によって、双方の関連がリンクされることになります。

EmployeeとAddressには、一対一の関連があり、次のように定義されています。

逆の立場から見ても、AddressとEmployeeは一対一の関連があり、 次のように定義されています。

  

mappedBy属性によって関連の所有者側のプロパティを指定します。 関連の所有者側とは、外部キーを持っているほうを意味します。 今回のケースは、address_idという外部キー(プロパティ名はaddressId)をEmployeeが 持っているのでEmployeeが関連の所有者になります。 mappedBy属性によって、双方の関連がリンクされることになります。

詳しくは、 関連定義 を参照してください。

楽観的排他制御をするには、int, long, Integer, Longの型を持つフィールドに @Versionをつけます。 詳しくは、 バージョン定義 を参照してください。

これで、エンティティの基本的な説明は終わりました。 それでは、早速動かしてみましょう。

Seasar2の機能をいろいろ試してみるには、 S2TestCaseを継承したクラスを使うと便利です。

src/test/java/examples/GetResultListTest.java を見てみましょう。

setUp()でapp.diconを読み込み、JdbcManagerのフィールドを定義しておけば、 testXxx()の中で、JdbcManagerを使うことができます。 このJdbcManagerを使ってデータベースにアクセスします。

複数件検索を行うには、from()の引数に検索したいエンティティのクラスを指定し、 getResultList()を呼び出します。 このテストケースを実行するには、ソースを右クリックして、 Run As -> JUnit Testを選びます。

results = jdbcManager.from(Employee.class).getResultList(); for (Employee e : results) { System.out.println(e.name); } ]]>

詳しくは、 複数件検索 を参照してください。

1件検索を行うには、from()の引数に検索したいエンティティのクラスを指定し、 getSingleResult()を呼び出します。

src/test/java/examples/GetSingleResultTest.java を見てみましょう。

where()で条件を指定することができます。 SQLでできることはすべて指定することができます。 SQLとの違いは、カラム名のかわりにプロパティ名を指定することです。

where()の2番目以降の引数は、可変長引数になっています。 例えば、次のように複数指定できます。

詳しくは、 1件検索検索条件 を参照してください。

getResultList() を使うと、検索結果を全て含むリストが返されます。 このため、検索結果が膨大な場合は大量のメモリを消費してしまいます.

このような場合は、エンティティ1件ごとにコールバックされるイテレーションを使うと効果的です。

src/test/java/examples/IterateTest.java を見てみましょう。

() { private long sum; public Long iterate(Employee emp, IterationContext context) { sum += emp.salary; return sum; } }); System.out.println(sum); } ]]>

この例では、全従業員の給与の合計を求めています。 エンティティ1件ごとに匿名クラスの iterate() メソッドがコールバックされ、 その中で給与の累計を求めてその時点の累計を戻り値としています。 イテレーションの最後の戻り値が全体の戻り値となります。

イテレーションを途中で打ち切ることもできます。

() { private long sum; public Employee iterate(Employee emp, IterationContext context) { sum += emp.salary; if (sum > 10000) { context.setExit(true); } return emp; } }); System.out.println(emp.name); } ]]>

この例では、従業員の給与の合計が10000を越えると IterationContextsetExit() を呼び出しすことで、 イテレーションを終了します。 イテレーションの最後の戻り値が全体の戻り値となります。

詳しくは、 イテレーションによる検索 を参照してください。

検索結果の行数を select count(*) で取得するには、from()の引数に検索したいエンティティのクラスを指定し、 getCount()を呼び出します。

src/test/java/examples/GetCountTest.java を見てみましょう。

詳しくは、 検索結果の行数取得 を参照してください。

他のエンティティと結合するには、 innerJoin()またはleftOuterJoin()の引数に 関連のプロパティ名 を指定します。 エンティティ名ではないので注意してください。

src/test/java/examples/JoinTest.java を見てみましょう。

results = jdbcManager .from(Employee.class) .leftOuterJoin("department") .leftOuterJoin("address") .getResultList(); for (Employee e : results) { System.out.println(e.name + ", " + e.department.name + ", " + e.address.name); } ]]>

結合した関連エンティティのプロパティは、結合名.プロパティ名(例えばaddress.name)で指定します。 ネストした指定(aaa.bbb.ccc)も可能です。 ネストした指定をする場合は、必ずinnerJoin()/leftOuterJoin()で指定しておく必要があります。 例えば、aaa.bbb.cccのプロパティを指定するには、leftOuterJoin("aaa.bbb")を指定します。

詳しくは、 結合 を参照してください。

where句を文字列で組み立てる場合、 条件が指定されなかったらwhere句からはずしたり、 最初の条件にはandをつけないけど2番名の条件からはandをつけたりなど、 いろいろなことを考慮しながら文字列を組み立てる必要があります。

これらの面倒な処理を簡易に行えるようにしたのがSimpleWhereです。

src/test/java/examples/SimpleWhereTest.java を見てみましょう。

results = jdbcManager .from(Employee.class) .leftOuterJoin("address") .where( new SimpleWhere().starts("name", "A").ends( "address.name", "1")) .getResultList(); for (Employee e : results) { System.out.println(e.name + ", " + e.address.name); } ]]>

starts()の最初の引数はプロパティ名で、like '?%'に変換されます。 ends()の最初の引数はプロパティ名で、like '%?'に変換されます。 それぞれの条件は、andで結合されます。 上記のサンプルでは、"A"や"1"のように直接リテラルを渡していますが、 変数を渡した場合、変数がnullの場合は、条件に含まれなくなります。

詳しくは、 検索条件 を参照してください。

orderBy()でソート順を指定することができます。 SQLでできることはすべて指定することができます。 SQLとの違いは、カラム名のかわりにプロパティ名を指定することです。

src/test/java/examples/OrderByTest.java を見てみましょう。

results = jdbcManager .from(Employee.class) .orderBy("name desc") .getResultList(); for (Employee e : results) { System.out.println(e.name); } ]]>

詳しくは、 ソート順 を参照してください。

ページングを指定する場合は、 limit(), offset()を使います。 limit()には、取得する行数を指定します。 offset()には、最初に取得する行の位置を指定します。 最初の行の位置は0になります。 ページングを指定するには、必ず ソート順 の指定も必要です。

src/test/java/examples/PagingTest.java を見てみましょう。

results = jdbcManager .from(Employee.class) .orderBy("id") .limit(5) .offset(4) .getResultList(); for (Employee e : results) { System.out.println(e.id); } ]]>

詳しくは、 ページング を参照してください。

エンティティを挿入するには、 insert()とexecute()を組み合わせます。

src/test/java/examples/InsertTest.java を見てみましょう。

テストメソッドがTxで終わっていると、テスト時実行前にトランザクションが開始され、 テスト終了後に自動的にロールバックされます。 そのため、何度でも同じテストを繰り返すことができます。

識別子は@GeneratedValueが指定されているので自動的に設定されます。

詳しくは、 挿入 を参照してください。

エンティティを更新するには、 update()とexecute()を組み合わせます。

src/test/java/examples/UpdateTest.java を見てみましょう。

versionプロパティには、@Versionが指定されているので、 Seasar2による楽観的排他制御が行なわれて、 更新に成功するとversionの値がインクリメントされます。

詳しくは、 更新 を参照してください。

エンティティを削除するには、 delete()とexecute()を組み合わせます。

src/test/java/examples/DeleteTest.java を見てみましょう。

詳しくは、 削除 を参照してください。

SQLの自動生成は便利な機能ですが、 SQLを自分で書きたいこともあるでしょう。 SQLを使って複数件検索するには、 selectBySql()とgetResultList()を組み合わせます。

src/test/java/examples/SqlGetResultListTest.java を見てみましょう。

results = jdbcManager .selectBySql(EmployeeDto.class, SELECT_EMPLOYEE_DTO, 1) .getResultList(); for (EmployeeDto e : results) { System.out.println(e.name + " " + e.departmentName); } ]]>

selectBySql()の最初の引数は、結果を受け取るJavaBeansです。 結果セットのカラム名とJavaBeansのプロパティ名を あわせておけば自動的にマッピングされます。 AAA_BBBのような'_'記法とaaaBbbのようなキャメル記法の マッピングも自動的に行なわれます。

selectBySql()の3番目以降の引数は、可変長引数になっています。 例えば、次のように複数指定できます。

詳しくは、 SQLによる複数件検索 を参照してください。

SQLを使って結果をマップで返すには、 selectBySql()の最初の引数をBeanMap.classにします。 BeanMapはMap<String, Object>なクラスで、 存在しないキーにアクセスすると 例外が発生します。 キーの値は、AAA_BBBのような'_'記法の値ををaaaBbbのようなキャメル記法に 変換したものです。

src/test/java/examples/SqlMapTest.java を見てみましょう。

results = jdbcManager.selectBySql(BeanMap.class, LABEL_VALUE).getResultList(); for (BeanMap m : results) { System.out.println(m); } ]]>

詳しくは、 SQLによる複数件検索 を参照してください。

SQLを使って1件検索するには、 selectBySql()とgetSingleResult()を組み合わせます。

src/test/java/examples/SqlGetSingleResultTest.java を見てみましょう。

selectリストが1つだけの場合は、 selectBySql()の最初の引数に、 JavaBeansではなく、Integer.classやString.class などのカラムの型に応じたクラスを指定します。

詳しくは、 SQLによる1件検索 を参照してください。

複雑で長いSQL文はソースコードに直接記述するよりも、 ファイルに書いたほうがメンテナンスがしやすくなります。

SQLファイルは、クラスパス上にあるならどこにおいてもかまいませんが、 ルートパッケージ.sql.テーブル名 のパッケージに対応したディレクトリ配下に置くことを推奨します。 例えば、 employeeテーブルに関するSQLファイルは、 examples/sql/employeeディレクトリにおくと良いでしょう。

何のパラメータもない単純なSQLファイルは次のようになります。

= 1000 and salary <= 2000 ]]>

1000の部分をsalaryMin というパラメータで置き換えるには、 次のように置き換えたいリテラルの左にSQLコメントでパラメータ名を埋め込みます。 リテラルを文字列として直接置き換えるのではなく、 PreparedStatmentを使ったバインド変数に置き換えるので、 SQLインジェクション対策も問題ありません。

= /*salaryMin*/1000 and salary <= 2000 ]]>

同様に2000の部分も salaryMaxというパラメータで置き換えます。

= /*salaryMin*/1000 and salary <= /*salaryMax*/2000 ]]>

検索条件の入力画面などによくあるパターンで、 何か条件が入力されていれば検索条件に追加し、 入力されていなければ条件には追加しないということを実装してみましょう。 このような場合は、IFコメントとENDコメントを組み合わせます。

= /*salaryMin*/1000 /*END*/ /*IF salaryMax != null*/ and salary <= /*salaryMax*/2000 /*END*/ ]]>

IFコメントの内容がtrueなら、 IFコメントとENDコメントで囲んでいる内容が出力されます。 IFコメントの条件は、OGNLによって評価されます。 詳しくは、 OGNLガイド を参照してください。

上記のように記述すると、salaryMinがnullではなくて、 salaryMaxがnullのときには、 下記のように正しいSQLになります。

= ? ]]>

しかしsalaryMinがnullでsalaryMaxがnullではないときは、 次のような不正(andがwhereの直後にある)なSQLになります。

また、salaryMinとsalaryMaxがnullの場合も、 次のような不正(whereだけがある)なSQLになります。

この問題に対応するためには、where句の部分を次のように、 BEGINコメントとENDコメントで囲みます。

= /*salaryMin*/1000 /*END*/ /*IF salaryMax != null*/ and salary <= /*salaryMax*/2000 /*END*/ /*END*/ ]]>

このようにすると、salaryMinがnullでsalaryMaxがnullではないときは、 salaryMaxの条件は、BEGINコメントとENDコメントで囲まれた最初の条件なので、 andの部分が自動的に削除されて次のようになります。

また、salaryMinとsalaryMaxがnullの場合は、 BEGINコメントとENDコメントで囲まれた部分に1つも条件に一致するものがないので、 BEGINコメントとENDコメントで囲まれた部分がカットされて次のようになります。

src/main/resources/examples/sql/employee/selectWithDepartment.sql を見てみましょう。

= /*salaryMin*/1000 /*END*/ /*IF salaryMax != null*/ and e.salary <= /*salaryMax*/2000 /*END*/ /*END*/ order by e.salary ]]>

SQLファイルを使って複数件検索するには、 selectBySqlFile()とgetResultList()を組み合わせます。

src/test/java/examples/SqlFileTest.javaと src/main/java/examples/dto/SelectWithDepartmentDto.java を見てみましょう。

results = jdbcManager .selectBySqlFile(EmployeeDto.class, SQL_FILE, dto) .getResultList(); for (EmployeeDto e : results) { System.out .println(e.name + " " + e.salary + " " + e.departmentName); } ]]>

詳しくは、 SQLファイル を参照してください。

S2JDBCは、エンティティの継承をサポートしていませんが、 列挙型を使って多態を実現できます。

src/main/java/examples/entity/JobType.java を見てみましょう。Enumのそれぞれの値にボーナスを計算するcalculateBonus()が 定義されています。

全従業員のボーナスの合計を求めるロジックは次のようになります。

src/test/java/examples/TypeStrategyTest.java を見てみましょう。

results = jdbcManager.from(Employee.class).getResultList(); int totalBonus = 0; for (Employee e : results) { totalBonus += e.jobType.calculateBonus(e.salary); } System.out.println("Total Bonus:" + totalBonus); ]]>

このやり方は、継承より委譲という良いプログラミングスタイルに従っています。