セットアップ
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(value = {"S2JDBC-Gen 2.4.35", "org.seasar.extension.jdbc.gen.internal.model.EntityModelFactoryImpl"}, date = "2009/04/02 14:21:20")
@Generated は自動生成されたことを示すアノテーションです。
エンティティ
テーブルのデータとJavaのオブジェクトのマッピングは、 エンティティに対してアノテーションで指定します。 エンティティというのは、テーブルの1行に対応するJavaのオブジェクトだと 理解していれば良いでしょう。
それでは、Employeeエンティティを見てみましょう。 src/main/java/examples/entity/Employee.javaを開いてください。
package examples.entity; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.ManyToOne; import javax.persistence.OneToOne; import javax.persistence.Version; /** * Employeeエンティティクラス * * @author S2JDBC-Gen */ @Entity public class Employee { /** idプロパティ */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(nullable = false, unique = true) public Integer id; /** nameプロパティ */ @Column(length = 255, nullable = false, unique = false) public String name; /** jobTypeプロパティ */ @Column(nullable = false, unique = false) public Integer jobType; /** salaryプロパティ */ @Column(nullable = true, unique = false) public Integer salary; /** departmentIdプロパティ */ @Column(nullable = true, unique = false) public Integer departmentId; /** addressIdプロパティ */ @Column(nullable = true, unique = true) public Integer addressId; /** versionプロパティ */ @Version @Column(nullable = false, unique = false) public Integer version; /** address関連プロパティ */ @OneToOne public Address address; /** department関連プロパティ */ @ManyToOne public Department department; }
エンティティであることを示すには、@Entityが必要です。 詳しくは、 エンティティ定義 を参照してください。
識別子のフィールドには、@Idをつけます。 識別子をSeasar2に自動生成させる場合は、@GeneratedValueをつけます。 詳しくは、 識別子定義 を参照してください。
Seasar2では、publicフィールドを使ってシンプルにプロパティを定義することができます。 詳しくは、 シンプルなプロパティ を参照してください。
カラム名とプロパティ名が同じなら、カラム用のアノテーションは特に必要ありません。 また、AAA_BBBのようなカラム名用の'_'記法を、 aaaBbbのようなプロパティ名用のキャメル記法へ変換することも Seasar2によって自動的に行われるので、 アノテーションを指定する必要はありません。 詳しくは、 カラム定義 を参照してください。
JobTypeは次のような列挙型です。 実際のソースではもう少し複雑ですが、 わかりやすくするために今回は簡略化しています。
package examples.entity; public enum JobType { CLERK, SALESMAN, MANAGER, ANALYST, PRESIDENT; }
job_typeカラムを文字列で定義しておけば、 カラムには、'CLERK'のように文字列として格納され、 エンティティでは、列挙型に自動的にマッピングされます。
EmployeeとDepartmentには、多対一の関連があり、次のように定義されています。
@ManyToOne public Department department;
逆の立場から見ると、DepartmentとEmployeeは一対多の関連があり、 次のように定義されています。
@OneToMany(mappedBy = "department") public List<Employee> employeeList;
mappedBy属性によって関連の所有者側のプロパティを指定します。 関連の所有者側とは、外部キーを持っているほうを意味します。 今回のケースは、department_idという外部キー(プロパティ名はdepartmentId)をEmployeeが 持っているのでEmployeeが関連の所有者になります。 mappedBy属性によって、双方の関連がリンクされることになります。
EmployeeとAddressには、一対一の関連があり、次のように定義されています。
@OneToOne public Address address;
逆の立場から見ても、AddressとEmployeeは一対一の関連があり、 次のように定義されています。
@OneToOne(mappedBy = "address") public Employee employee;
mappedBy属性によって関連の所有者側のプロパティを指定します。 関連の所有者側とは、外部キーを持っているほうを意味します。 今回のケースは、address_idという外部キー(プロパティ名はaddressId)をEmployeeが 持っているのでEmployeeが関連の所有者になります。 mappedBy属性によって、双方の関連がリンクされることになります。
詳しくは、 関連定義 を参照してください。
楽観的排他制御をするには、int, long, Integer, Longの型を持つフィールドに @Versionをつけます。 詳しくは、 バージョン定義 を参照してください。
これで、エンティティの基本的な説明は終わりました。 それでは、早速動かしてみましょう。
複数件検索
Seasar2の機能をいろいろ試してみるには、 S2TestCaseを継承したクラスを使うと便利です。
src/test/java/examples/GetResultListTest.java を見てみましょう。
package examples.entity; import java.util.List; import org.seasar.extension.jdbc.JdbcManager; import org.seasar.extension.unit.S2TestCase; public class GetResultListTest extends S2TestCase { private JdbcManager jdbcManager; protected void setUp() throws Exception { include("app.dicon"); } public void testGetResultList() throws Exception { ... } }
setUp()でapp.diconを読み込み、JdbcManagerのフィールドを定義しておけば、 testXxx()の中で、JdbcManagerを使うことができます。 このJdbcManagerを使ってデータベースにアクセスします。
複数件検索を行うには、from()の引数に検索したいエンティティのクラスを指定し、 getResultList()を呼び出します。 このテストケースを実行するには、ソースを右クリックして、 Run As -> JUnit Testを選びます。
List<Employee> results = jdbcManager.from(Employee.class).getResultList(); for (Employee e : results) { System.out.println(e.name); }
詳しくは、 複数件検索 を参照してください。
1件検索
1件検索を行うには、from()の引数に検索したいエンティティのクラスを指定し、 getSingleResult()を呼び出します。
src/test/java/examples/GetSingleResultTest.java を見てみましょう。
Employee result = jdbcManager .from(Employee.class) .where("id = ?", 1) .getSingleResult(); System.out.println(result.name);
where()で条件を指定することができます。 SQLでできることはすべて指定することができます。 SQLとの違いは、カラム名のかわりにプロパティ名を指定することです。
where()の2番目以降の引数は、可変長引数になっています。 例えば、次のように複数指定できます。
where("id = ? or name = ?", 1, "SCOTT")
イテレーション
getResultList()
を使うと、検索結果を全て含むリストが返されます。
このため、検索結果が膨大な場合は大量のメモリを消費してしまいます.
このような場合は、エンティティ1件ごとにコールバックされるイテレーションを使うと効果的です。
src/test/java/examples/IterateTest.java を見てみましょう。
public void testIterate() throws Exception { long sum = jdbcManager.from(Employee.class).iterate( new IterationCallback<Employee, Long>() { private long sum; public Long iterate(Employee emp, IterationContext context) { sum += emp.salary; return sum; } }); System.out.println(sum); }
この例では、全従業員の給与の合計を求めています。 エンティティ1件ごとに匿名クラスの
iterate()
メソッドがコールバックされ、 その中で給与の累計を求めてその時点の累計を戻り値としています。
イテレーションの最後の戻り値が全体の戻り値となります。
イテレーションを途中で打ち切ることもできます。
public void testIterateExit() throws Exception { Employee emp = jdbcManager.from(Employee.class).iterate( new IterationCallback<Employee, Employee>() { 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を越えると
IterationContext
の
setExit()
を呼び出しすことで、 イテレーションを終了します。 イテレーションの最後の戻り値が全体の戻り値となります。
詳しくは、 イテレーションによる検索 を参照してください。
行数取得
検索結果の行数を
select count(*)
で取得するには、from()の引数に検索したいエンティティのクラスを指定し、
getCount()を呼び出します。
src/test/java/examples/GetCountTest.java を見てみましょう。
public void testGetCount() throws Exception { long count = jdbcManager.from(Employee.class).getCount(); System.out.println(count); }
詳しくは、 検索結果の行数取得 を参照してください。
結合
他のエンティティと結合するには、 innerJoin()またはleftOuterJoin()の引数に 関連のプロパティ名 を指定します。 エンティティ名ではないので注意してください。
src/test/java/examples/JoinTest.java を見てみましょう。
List<Employee> 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句を文字列で組み立てる場合、 条件が指定されなかったらwhere句からはずしたり、 最初の条件にはandをつけないけど2番名の条件からはandをつけたりなど、 いろいろなことを考慮しながら文字列を組み立てる必要があります。
これらの面倒な処理を簡易に行えるようにしたのがSimpleWhereです。
src/test/java/examples/SimpleWhereTest.java を見てみましょう。
List<Employee> 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 を見てみましょう。
List<Employee> 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 を見てみましょう。
List<Employee> 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 を見てみましょう。
public void testInsertTx() throws Exception { Employee emp = new Employee(); emp.name = "test"; emp.jobType = JobType.ANALYST; emp.salary = 300; jdbcManager.insert(emp).execute(); System.out.println(emp.id); }
テストメソッドがTxで終わっていると、テスト時実行前にトランザクションが開始され、 テスト終了後に自動的にロールバックされます。 そのため、何度でも同じテストを繰り返すことができます。
識別子は@GeneratedValueが指定されているので自動的に設定されます。
詳しくは、 挿入 を参照してください。
更新
エンティティを更新するには、 update()とexecute()を組み合わせます。
src/test/java/examples/UpdateTest.java を見てみましょう。
Employee emp = jdbcManager .from(Employee.class) .where("id = ?", 1) .getSingleResult(); emp.name = "hoge"; System.out.println(emp.version); jdbcManager.update(emp).execute(); System.out.println(emp.version);
versionプロパティには、@Versionが指定されているので、 Seasar2による楽観的排他制御が行なわれて、 更新に成功するとversionの値がインクリメントされます。
詳しくは、 更新 を参照してください。
削除
エンティティを削除するには、 delete()とexecute()を組み合わせます。
src/test/java/examples/DeleteTest.java を見てみましょう。
Employee emp = jdbcManager .from(Employee.class) .where("id = ?", 1) .getSingleResult(); jdbcManager.delete(emp).execute(); emp = jdbcManager .from(Employee.class) .where("id = ?", 1) .getSingleResult(); System.out.println(emp);
詳しくは、 削除 を参照してください。
SQLによる複数件検索
SQLの自動生成は便利な機能ですが、 SQLを自分で書きたいこともあるでしょう。 SQLを使って複数件検索するには、 selectBySql()とgetResultList()を組み合わせます。
src/test/java/examples/SqlGetResultListTest.java を見てみましょう。
private static final String SELECT_EMPLOYEE_DTO = "select e.*, d.name as department_name" + " from employee e left outer join department d" + " on e.department_id = d.id" + " where d.id = ?"; ... List<EmployeeDto> 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番目以降の引数は、可変長引数になっています。 例えば、次のように複数指定できます。
selectBySql(EmployeeDto.class, "... id = ? or name = ?", 1, "SCOTT")
詳しくは、 SQLによる複数件検索 を参照してください。
SQLによるマップで返す複数件検索
SQLを使って結果をマップで返すには、 selectBySql()の最初の引数をBeanMap.classにします。 BeanMapはMap<String, Object>なクラスで、 存在しないキーにアクセスすると 例外が発生します。 キーの値は、AAA_BBBのような'_'記法の値ををaaaBbbのようなキャメル記法に 変換したものです。
src/test/java/examples/SqlMapTest.java を見てみましょう。
private static final String LABEL_VALUE = "select name as label, id as value from employee"; ... List<BeanMap> results = jdbcManager.selectBySql(BeanMap.class, LABEL_VALUE).getResultList(); for (BeanMap m : results) { System.out.println(m); }
詳しくは、 SQLによる複数件検索 を参照してください。
SQLによる1件検索
SQLを使って1件検索するには、 selectBySql()とgetSingleResult()を組み合わせます。
src/test/java/examples/SqlGetSingleResultTest.java を見てみましょう。
private static final String SELECT_COUNT = "select count(*) from employee"; ... Integer result = jdbcManager .selectBySql(Integer.class, SELECT_COUNT) .getSingleResult(); System.out.println(result);
selectリストが1つだけの場合は、 selectBySql()の最初の引数に、 JavaBeansではなく、Integer.classやString.class などのカラムの型に応じたクラスを指定します。
詳しくは、 SQLによる1件検索 を参照してください。
SQLファイル
複雑で長いSQL文はソースコードに直接記述するよりも、 ファイルに書いたほうがメンテナンスがしやすくなります。
SQLファイルは、クラスパス上にあるならどこにおいてもかまいませんが、 ルートパッケージ.sql.テーブル名 のパッケージに対応したディレクトリ配下に置くことを推奨します。 例えば、 employeeテーブルに関するSQLファイルは、 examples/sql/employeeディレクトリにおくと良いでしょう。
何のパラメータもない単純なSQLファイルは次のようになります。
select * from employee where salary >= 1000 and salary <= 2000
1000の部分をsalaryMin というパラメータで置き換えるには、 次のように置き換えたいリテラルの左にSQLコメントでパラメータ名を埋め込みます。 リテラルを文字列として直接置き換えるのではなく、 PreparedStatmentを使ったバインド変数に置き換えるので、 SQLインジェクション対策も問題ありません。
select * from employee where salary >= /*salaryMin*/1000 and salary <= 2000
同様に2000の部分も salaryMaxというパラメータで置き換えます。
select * from employee where salary >= /*salaryMin*/1000 and salary <= /*salaryMax*/2000
検索条件の入力画面などによくあるパターンで、 何か条件が入力されていれば検索条件に追加し、 入力されていなければ条件には追加しないということを実装してみましょう。 このような場合は、IFコメントとENDコメントを組み合わせます。
select * from employee where /*IF salaryMin != null*/ salary >= /*salaryMin*/1000 /*END*/ /*IF salaryMax != null*/ and salary <= /*salaryMax*/2000 /*END*/
IFコメントの内容がtrueなら、 IFコメントとENDコメントで囲んでいる内容が出力されます。 IFコメントの条件は、OGNLによって評価されます。 詳しくは、 OGNLガイド を参照してください。
上記のように記述すると、salaryMinがnullではなくて、 salaryMaxがnullのときには、 下記のように正しいSQLになります。
select * from employee where salary >= ?
しかしsalaryMinがnullでsalaryMaxがnullではないときは、 次のような不正(andがwhereの直後にある)なSQLになります。
select * from employee where and salary <= ?
また、salaryMinとsalaryMaxがnullの場合も、 次のような不正(whereだけがある)なSQLになります。
select * from employee where
この問題に対応するためには、where句の部分を次のように、 BEGINコメントとENDコメントで囲みます。
select * from employee /*BEGIN*/ where /*IF salaryMin != null*/ salary >= /*salaryMin*/1000 /*END*/ /*IF salaryMax != null*/ and salary <= /*salaryMax*/2000 /*END*/ /*END*/
このようにすると、salaryMinがnullでsalaryMaxがnullではないときは、 salaryMaxの条件は、BEGINコメントとENDコメントで囲まれた最初の条件なので、 andの部分が自動的に削除されて次のようになります。
select * from employee where salary <= ?
また、salaryMinとsalaryMaxがnullの場合は、 BEGINコメントとENDコメントで囲まれた部分に1つも条件に一致するものがないので、 BEGINコメントとENDコメントで囲まれた部分がカットされて次のようになります。
select * from employee
src/main/resources/examples/sql/employee/selectWithDepartment.sql を見てみましょう。
select e.*, d.name as department_name from employee e left outer join department d on e.department_id = d.id /*BEGIN*/ where /*IF salaryMin != null*/ e.salary >= /*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 を見てみましょう。
private static final String SQL_FILE = "examples/sql/employee/selectWithDepartment.sql"; ... SelectWithDepartmentDto dto = new SelectWithDepartmentDto(); dto.salaryMin = 1200; dto.salaryMax = 1800; List<EmployeeDto> results = jdbcManager .selectBySqlFile(EmployeeDto.class, SQL_FILE, dto) .getResultList(); for (EmployeeDto e : results) { System.out .println(e.name + " " + e.salary + " " + e.departmentName); }
package examples.dto; public class SelectWithDepartmentDto { public Integer salaryMin; public Integer salaryMax; }
詳しくは、 SQLファイル を参照してください。
多態
S2JDBCは、エンティティの継承をサポートしていませんが、 列挙型を使って多態を実現できます。
src/main/java/examples/entity/JobType.java を見てみましょう。Enumのそれぞれの値にボーナスを計算するcalculateBonus()が 定義されています。
package examples.entity; public enum JobType { CLERK { @Override public int calculateBonus(int salary) { return salary; } }, SALESMAN { @Override public int calculateBonus(int salary) { return salary * 2; } }, MANAGER { @Override public int calculateBonus(int salary) { return salary * 3; } }, ANALYST { @Override public int calculateBonus(int salary) { return salary * 4; } }, PRESIDENT { @Override public int calculateBonus(int salary) { return salary * 5; } }; public abstract int calculateBonus(int salary); }
全従業員のボーナスの合計を求めるロジックは次のようになります。
src/test/java/examples/TypeStrategyTest.java を見てみましょう。
List<Employee> 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);
このやり方は、継承より委譲という良いプログラミングスタイルに従っています。