About

ドキュメント

Javadoc

モジュール

プロジェクト文書

Built by Maven

セットアップ

S2JDBC-Tutorial-xxx.zipを解凍し、その中にあるs2jdbc-tutorialを Eclipseにインポートしてください。これだけでセットアップは完了です。

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

エンティティ

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

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

package examples.entity;

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;

@Entity
public class Employee {

    @Id
    @GeneratedValue
    public Integer id;

    public String name;

    public JobType jobType;

    public Integer salary;

    public Integer departmentId;

    @ManyToOne
    public Department department;

    public Integer addressId;

    @OneToOne
    public Address address;

    @Version
    public Integer version;
}

エンティティであることを示すには、@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は一対一の関連があり、 次のように定義されています。

  
@OneToMany(mappedBy = "address")
public Employee employee;

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

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

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

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

複数件検索

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

src/test/java/examples/entity/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/entity/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")

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

結合

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

src/test/java/examples/entity/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/entity/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/entity/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/entity/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/entity/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/entity/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/entity/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/entity/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()の最初の引数をRowMap.classにします。 RowMapはMap<String, Object>なクラスで、 存在しないキーにアクセスすると 例外が発生します。 キーの値は、AAA_BBBのような'_'記法の値ををaaaBbbのようなキャメル記法に 変換したものです。

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

private static final String LABEL_VALUE =
    "select name as label, id as value from employee";
...
List<RowMap> results =
    jdbcManager.selectBySql(RowMap.class, LABEL_VALUE).getResultList();
for (RowMap m : results) {
    System.out.println(m);
}

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

SQLによる1件取得

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

src/test/java/examples/entity/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/entity/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は、エンティティの継承をサポートしていませんが、 列挙型とストラテジパターンを組み合わせて、 多態を実現できます。

従業員の仕事のタイプに応じた振る舞いを JobStrategyとして定義します。

src/main/java/examples/entity/JobStrategy.java を見てみましょう。

package examples.entity;

public interface JobStrategy {

    int calculateBonus(int salary);
}

給料に応じたボーナスを計算するメソッドが定義されています。

仕事のタイプ別にJobStrategyを実装します。

package examples.entity;

public class Clerk implements JobStrategy {

    public int calculateBonus(int salary) {
        return salary;
    }
}
package examples.entity;

public class Salesman implements JobStrategy {

    public int calculateBonus(int salary) {
        return salary * 2;
    }
}
package examples.entity;

public class Manager implements JobStrategy {

    public int calculateBonus(int salary) {
        return salary * 3;
    }
}
package examples.entity;

public class Analyst implements JobStrategy {

    public int calculateBonus(int salary) {
        return salary * 4;
    }
}
package examples.entity;

public class President implements JobStrategy {

    public int calculateBonus(int salary) {
        return salary * 5;
    }
}

JobTypeの列挙型でタイプに応じたストラテジを生成します。 列挙型に対応するカラムは文字列で定義しておけば、 列挙型と文字列の変換はSeasar2によって自動的に行なわれます。

src/main/java/examples/entity/JobType.java を見てみましょう。

package examples.entity;

public enum JobType {

    CLERK {
        @Override
        public JobStrategy createStrategy() {
            return new Clerk();
        }
    },
    SALESMAN {
        @Override
        public JobStrategy createStrategy() {
            return new Salesman();
        }
    },
    MANAGER {
        @Override
        public JobStrategy createStrategy() {
            return new Manager();
        }
    },
    ANALYST {
        @Override
        public JobStrategy createStrategy() {
            return new Analyst();
        }
    },
    PRESIDENT {
        @Override
        public JobStrategy createStrategy() {
            return new President();
        }
    };

    public abstract JobStrategy createStrategy();
}

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

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

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

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