Seasar DI Container with AOP

S2JDBCとは

S2JDBCは、データベースプログラミングの生産性を10倍以上高めることを目標として作成した Seasar2のO/R Mapperです。 何に比べて生産性が10倍かというとJava標準のJPA(Java Persistence API)に対してです。 次のような特徴があります。

流れるようなインターフェースと脱CoC

「流れるようなインターフェース」とは、文章を記述していくようにメソッドを呼び出していく手法です。 詳しくは、ファウラーたんのFluentInterface を参照してください。あれこれ説明するよりも、検索の例を見たほうがわかりやすいでしょう。

List<Employee> results = jdbcManager.from(Employee.class)
                             .join("department")
                             .where("id in (? , ?)", 11, 22)
                             .orderBy("name")
                             .getResultList();

一番の特徴は、可読性の高さです。何をやっているのかが一目瞭然ではないでしょうか。

二番目の特徴は、IDEと組み合わせることで、APIを覚えていなくても自然に使えることです。 何か記述したかったら、コード補完によって使える単語(メソッド)が自動的に選択されます。 これが脱Convention over Configuration(以後CoCと省略)につながってきます。

CoCは、「規約を守っておけばフレームワークが自動的に設定してあげる」というもので、 CoCによって開発者は、あまりソースコードを書かなくてもすむようになります。 CoCは、確かに私たちを設定ファイル地獄から救ってくれました。

しかし、CoCにも暗黒面があります。 ソースコードに明示されている部分が少ないので、 自動化されている部分がブラックボックスになり、 規約を知らない人が見ると何をやっているのかがまったくわからなくなってしまうのです。

また、規約を知らないと何もできなくなるので、 ちょっとしたことでも、自分の知らないことであれば、 いろいろ調べたり試行錯誤を繰り返すことになります。 このような試行錯誤の時間は馬鹿になりません。 最終的なソースコードは確かに少なくなったけど、 かかった時間は大して変わらなかったなんてことも十分にありえます。 自動化されているので最初はとっつきやすいのですが、 知らないことやイレギュラーなことに弱いのです。

それに対して「流れるようなインターフェース」の場合は、 可読性が高いので、誰が見ても何をやっているのかが明快です。 また、IDEがコード補完によって自動的にできることを教えてくれるので、 事前に知らなくても使いながら学習していくことができます。 知らない機能を使うために試行錯誤を繰り返す必要はありません。 単語(メソッド)の間違いもそんなメソッドないよとIDE(コンパイラ)が教えてくれます。

「流れるようなインターフェース」によって、CoCより安全で効率的な開発が可能になるのです。

90%のSQLを自動生成する

SQLは文字列で組み立てるため、書きづらく間違いやすいものです。 S2JDBCは、「流れるようなインターフェース」により、SQLを自動生成します。 先ほどの例をもう一度見てみましょう。

List<Employee> results = jdbcManager.from(Employee.class).join("department")
                            .where("id in (? , ?)", 11, 22)
                            .orderBy("name desc")
                            .getResultList();

これは、次のようなSQLに展開されます。

select T1_.ID, T1_.NAME, T1.DEPARTMENT_ID, T2_.ID, T2_.NAME
  from EMPLOYEE T1_ left outer join DEPARTMENT T2_ on T1_.DEPARTMENT_ID = T2_.ID
 where T1_.ID in (?, ?)
 order by T1_.NAME desc

クエリの呼び出しがSQLに対応しているので可読性が高いだけでなく、 selectリスト句でのカラムの指定や結合の指定など面倒な指定を する必要がありません。

これに対し、Javaの標準であるJPA(Java Persistence API)はどうでしょうか。 JPAもSQLを自動生成できます。それでは、同じことをJPAでやってみましょう。

List<Employee> results = (List<Employee>) entityManager.createQuery(
                            "select e from Employee e left join fetch e.department" +
                            " where e.id in (?1, ?2) order by e.name")
                            .setParameter(1, 11)
                            .setParameter(2, 22)
                            .getResultList();

JPQL(select ... の文字列)の部分は、Seasar2のクエリとほぼ同じように見えますが、 文字列で組み立てているので、書き間違える可能性が増えます。 文字列で組み立てるより、「流れるようなインターフェース」のほうが、 間違いも減るし、IDEによる自動補完が効くのでより生産性があがるのです。

departmentの関連がEAGERであれば、left join fetchの部分は省略できますが、 関連はLAZYにするのが安全なので、left join fetchを記述しています。

Javaの良さを生かす

最近、JavaからRubyへ なんて言われ、生産性の低い言語としてすっかりレッテルが貼られてしまったJavaですが、 古いJavaのイメージで語られていることが多い気がします。

アノテーション、Generics、列挙型、可変長引数、Auto Boxingなど、 今のJavaには、生産性を向上させるための機能がいろいろ用意されています。 ただ、残念なことにこれらのすばらしい機能をフルに活用したフレームワークは あまりないのが現状です。

Seasar2では、Javaのよさを生かし、生産性を向上させます。 例えば、先ほどのJPAの戻り値のキャストを見てみましょう。

List<Employee> results = (List<Employee>) entityManager.createQuery("select e from Employee e ...")
                            ...
                            .getResultList();

Seasar2では、Genericsを使って、うざったいキャストを不要にしています。

List<Employee> results = jdbcManager.from(Employee.class)
                            ...
                            .getResultList();

JPAでは、文字列でEmployeeと指定していますが、Seasar2ではfrom()の引数でEmployee.classと クラスリテラルを渡しているので、Genericsを利用できます。

キャストの手間自体はたいしたことはないのですが、JPQLでEmployeeと指定しているのに、 さらに(List<Employee>)とキャストしなければいけないという二度手間感が 開発者のやる気を低下させてしまいます。

それに対し、Seasar2では、select文を書くような気持ちで、自然にメソッドを呼び出していけば、 キャスト不要で欲しいデータを取り出すことができます。 リズムに乗って開発できるので、開発者の生産性はさらに向上します。 開発者のやる気が、生産性を向上させる最も重要なポイントなのです。

もう1つ、バインド変数の例を見てみましょう。JPAでは次のようにバインド変数を指定しています。

entityManager.createQuery("select ... where e.id in (?1, ?2) ...")
    .setParameter(1, 11)
    .setParameter(2, 22)
    ...

setParameter()の呼び出しがうざったい感じです。Seasar2の場合は次のようになります。

jdbcManager.from(...
    .where("id in (? , ?)", 11, 22)
    ...

where()の二番目以降の引数は可変長引数になっていて、?の数にあわせて、引数を可変にできます。 例えば、次のようにすることも可能です。

where("id in (? , ?, ?, ?)", 11, 22, 33, 44)

Javaも良さを生かせば、さくさくソースを書けることがわかったいただけたでしょうか。

SQLの扱いが簡単

SQLの自動生成ですべてがうまくいけば問題ありませんが、 実案件では、複雑なSQLを自前で組み立てる必要が出てくる場合があります。 複雑なSQLで複雑な結果を返す場合、JPAでは次のようになります。

List<Object[]> results = (List<Object[]>) entityManager.createNativeQuery("複雑なSQL")
                                       ...
                                       .getResultList();

1行に相当する部分がオブジェクトの配列で返ってくるなんてがっかりです。 Seasar2では、結果セットのカラム名とJavaBeansのプロパティ名を同じにしておけば、 結果セットの1行をJavaBeansに自動的にマッピングします。 また、aaa_bbbのような'_'を使ったカラム名をaaaBbbのようなキャメル記法のプロパティ名に 自動的にマッピングします。 Seasar2を使った例は次のようになります。

List<MyDto> results = jdbcManager.selectBySql(MyDto.class, "複雑なSQL")
                                       ...
                                       .getResultList();

オブジェクトの配列かJavaBeansかの違いですが、 開発者としては、配列にインデックスでアクセスすると型も不明だし、 何が返ってくるかわからないので不安ですが、 プロパティにアクセスすると型も名前もわかっているので安心です。 このようなところも開発者のやる気につながってくるのです。

明示的にSQLを指定する必要があるのは、複雑なSQLの場合だけとは限りません。 既存システムをベースに新システムを作るというのは、よくある話です。 その場合に既存の資産で生かせるものはできる限り生かしたいものです。 そのような既存の資産の代表的なものがSQLです。

JPAで既存のSQLをそのまま生かそうとするとオブジェクトの配列が返ってきちゃうし、 SQLをJPQLに変換するのは結構面倒な作業です。 どちらを選ぶにせよ、やるきがそがれるのは間違いないでしょう。 Seasar2を使えば、JavaBeansにスムーズにマッピングできるので、 快適に既存のSQLを再利用できます。

シンプルなプロパティ

Javaでプロパティを作るには、フィールドとgetter、setterメソッドの定義が必要でした。 getter、setterメソッドは、カプセル化の観点から必要とされてきましたが、 本当にどんな場合でも必要なのでしょうか。

Javaでは、テーブルの一行に対応するデータの入れ物をエンティティと呼ぶことが一般的です。 テーブルのカラムのデータは、もともとpublicな存在なので、 エンティティのプロパティがカラムに一対一に対応するなら、 プロパティのgetter, setterメソッドを省略して、 publicフィールドにしても良いのではないでしょうか。

エンティティはテーブルの鏡であるという位置づけにするなら、 エンティティのプロパティをpublicフィールドにするのも別におかしなことではありません。 Seasar2では、エンティティはテーブルの鏡であるという立場をとることによって、 エンティティのプロパティをpublicフィールドとして定義できるようにしています。 もちろん、publicフィールドに抵抗のある方は、 これまでどおりに、getter, setterメソッドを定義することもできます。 それでは、getter, setterメソッドのありなしを比べて見ましょう。

@Entity
public class Employee {
    @Id
    private Integer id;
    
    private String name;
    
    private BigDecimal salary;
    
    public Integer getId() {
        return id;
    }
    
    public void setId(Integer id) {
        this.id = id;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public BigDecimal getSalary() {
        return salary;
    }
    
    public void setSalary(BigDecimal salary) {
        this.salary = salary;
    }
}
@Entity
public class Employee {
    @Id
    public Integer id;
    
    public String name;
    
    public BigDecimal salary;
}

どっちがすっきりして見やすいかは一目瞭然ですね。 これは、すべてのプロパティをpublicフィールドにすべきだという意味ではありません。 カプセル化が必要なケースももちろんあります。 でも、エンティティがテーブルの鏡だとする全体が成り立つなら、 プロパティをpublicフィールドにするのも十分にありだということです。

プロパティへのアクセスも

String s = employee.getName();
employee.setName("hoge");

よりも

String s = employee.name;
employee.name = "hoge";

のほうが直感的で、気分よく記述できるでしょう。 このような気分のよさも生産性につながってくるのです。

学習コストが低い

昔のフレームワークなら、機能が盛りだくさんで、使いこなすには時間がかかるけど、 使いこなせれば、威力を発揮するというのも許されたでしょう。 Hibernateは、 まさしくそのようなフレームワークです。

しかし、時代は変わりました。 今は、一ヶ月以内に成果を出すことを求められるのもよくある話です。 使いこなすのに時間がかかっては間に合わないのです。

そのような時代の要求を受け、機能を盛りだくさんにするのではなく、 データアクセス用のフレームワークとして本質的な機能に集中することで、 短期間で使えるようになることを可能にしました。

このドキュメントに対応するのががHibernateの Hibernate AnnotationsのドキュメントHibernate EntityManagerのドキュメント になります。学習コストが低いということが実感していただけるのではないでしょうか。

トラブリにくい

学習コストが低いでも取り上げましたが、 機能が盛りだくさんだと、覚えるのに時間がかかるだけではなく、 トラブルに見舞われやすくなり、その解決に時間がかかるようになります。

例えば、一対一、一対多、多対一などのエンティティ間の関連を取り上げてみます。 それぞれの関連は、データをロードするタイミングを指定することができ、 関連のプロパティにアクセスしたときにはじめてデータをロードするLazy loadingと、 関連元のエンティティにアクセスするのと同時に関連先のデータをロードするEager loadingがあります。

従業員と部署のエンティティに多対一の関連がある場合、 Eager loadingだと、従業員を取得するのと同時に部署も取得されます。 Lazy loadingだと、従業員を取得したときは、 単に従業員のデータだけが取得され、部署のプロパティにアクセスしたときにはじめて 部署のデータが取得されます。

また、それとは別に、関連はLazy loadingにしておいて、 クエリを実行するときに、どの関連を結合で取得してくるかを指定する方法もあります。 この方法は、フェッチ結合と呼ばれます。 以前のサンプルでも次のようにフェッチ結合が使われていました。

select e from Employee e left join fetch e.department

どのようなケースにも対応できるようにするため、 このようにいろいろな選択肢が用意されているのですが、 いろんな選択肢があるのでトラブルが起こる確率も増えてしまうのです。

例えば、一対一、多対一にはEager loading、 一対多にはLazy loadingを適用するというルールにしたとしましょう。JPAのデフォルトはそうなっています。

Eager loadingが設定されている関連元のエンティティにアクセスするときは、 必要かどうかにかかわらず常に関連先のデータまで取得されてしまうため、 大量のデータを取得する場合は、パフォーマンスやメモリ不足の問題を引き起こすことがあります。 少量のデータでテストしていたときには気づかず、本番相当のデータを用意したときにはじめて 発覚するというのも困りものです。

それでは、Eager loadingLazy loadingに変えれば、 すべての問題は解決するのでしょうか。残念ながら次の「N+1検索問題」を引き起こします。

1000件の従業員のリストを表示するという機能があるとしましょう。 従業員のデータ以外に部署名も表示させる必要があるとします。 最初の1回の検索で1000件の従業員のデータを取得します。 その後、部署名を表示させるために、従業員.部署.名前のようにアクセスすると、 部署のプロパティにアクセスした瞬間にLazy loadingがおきます。 これが従業員の件数分(N)起こるので、「N+1検索問題」といわれています。

大量の検索が発行されるので、深刻なパフォーマンス問題を引き起こします。 実際は、同じ部署へのアクセスはキャッシュが使われるので、N件起こることはないのですが、 それでも部署の件数分の検索が行なわれパフォーマンス問題を引き起こすことには変わりはありません。

最も良い解決策は、関連はLazy loadingにしておき、 検索のときにフェッチ結合を使うことです。 Lazy loadingにするのは、Lazy loadingにしたいからではなく、 Eager loadingさせないための回避策です。 フェッチ結合は、特定の検索のときだけ、Eager loadingになるような効果があります。

ここまで説明してきたようにJPAは、機能が豊富だけどトラブリやすいという問題があります。 Seasar2では、トラブリにくくするために、いたずらに機能を増やすのではなく、 トラブリやすい機能は意図的に削っています。

例えば、関連のLoding問題についていえば、Eager loadingLazy loadingもサポートせず、次のようなフェッチ結合のみをサポートしています。 join()メソッドのデフォルトはフェッチ結合になります。

jdbcManager.from(Employee.class).join("department")

実際の開発において、予想外に多くの時間を費やしているのはトラブルシューティングの時間でないでしょうか。 トラブリにくくすることも、生産性向上の重要な要因なのです。

より良いJPA

これまで、JPAの問題点をいくつか指摘してきましたが、 JPAは標準であり、一方的に無視してよいものでもありません。 そこで、Seasar2では、エンティティとテーブルのマッピングの仕様は、 JPAとあわせ、いまいちなEntityManagerやJPQLの部分は、 独自仕様でいくことにしました。

JPAを使ってパフォーマンスが出なかったり、 トラブルに巻き込まれて困っている方は、 より良いJPAとしてSeasar2を使ってみてはいかがでしょうか。

セットアップ

Seasar2の基本的なセットアップをした後に、 JDBCのセットアップをします。

S2JDBCの設定は、s2jdbc.diconに対して行ないます。 s2jdbc.diconは、S2Tiger-2.4.x.zipを解凍してできたs2-tigerのresourcesにあるので、 コピーしてCLASSPATHに含めます。

s2jdbc.diconの中身は、次のようになっているので、 dialectプロパティをお使いのデータベースにあわせて書き換えます。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
    "http://www.seasar.org/dtd/components24.dtd">
<components>
    <include path="jdbc.dicon"/>
    <include path="s2jdbc-internal.dicon"/>
    <component name="jdbcManager" class="org.seasar.extension.jdbc.manager.JdbcManagerImpl">
        <property name="maxRows">0</property>
        <property name="fetchSize">0</property>
        <property name="queryTimeout">0</property>
        <property name="dialect">hsqlDialect</property>
    </component>
</components>

maxRows, fetchSize, queryTimeoutは、JdbcManagerのデフォルト値になります。 個別のクエリの呼び出し時に上書きすることもできます。 最大行数フェッチサイズクエリタイムアウトを参照してください。

ダイアレクト

ダイアレクトは次のものが用意されています。

名前 説明
db2390Dialect DB2/390
db2400Dialect DB2/400
db2Dialect DB2
derbyDialect Derby
firebirdDialect Firebird
h2Dialect H2
hsqlDialect HSQLDB
interbaseDialect Interbase
maxdbDialect MaxDB
mssql2005Dialect MS SQLServer 2005
mssqlDialect MS SQLServer
mysqlDialect MySQL
oracleDialect Oracle
postgreDialect PostgreSQL
standardDialect どれにも当てはまらないときに選ぶ標準的なダイアレクト
sybaseDialect Sybase

エンティティ

エンティティの定義の仕方を説明します。 ここでいっているエンティティとは、 データベースに永続化されるものだと考えてください。

注意点

エンティティ用のアノテーションは、JPAのものをそのまま利用していますが、 すべてのアノテーションや属性をサポートしているわけではありません。 ここに取り上げられているもののみサポートされているとお考えください。

エンティティ定義

エンティティにするためには、@Entityを必ず指定する必要があります。

@Entity
public class Employee {
    ...
}

name属性でエンティティ名を指定することができます。 次の例では、Empというエンティティ名を指定しています。

@Entity(name = "Emp")
public class Employee {
    ...
}

name属性を指定しなかった場合、エンティティのクラス名からパッケージ名を除いた部分が、 デフォルトのエンティティ名になります。 例えば、クラス名がexamples.entity.Employeeの場合、 デフォルトのエンティティ名は、Employeeになります。

テーブル定義

テーブル情報を指定するには、@Tableを使います。

name属性でテーブル名を指定することができます。 指定しなかった場合、テーブル名はエンティティ名と同じになります。 エンティティ名が、AaaBbbのようなキャメル記法の場合、 テーブル名は、AAA_BBBのように'_'区切りだとみなされます。

このルールは、convention.diconで指定されている org.seasar.framework.convention.impl.PersistenceNamingConventionImplの fromEntityNameToTableName()の実装を変えることで、カスタマイズすることができます。

次の例では、EMPというテーブル名を指定しています。

@Entity
@Table(name = "EMP")
public class Employee {
    ...
}

schema属性でスキーマを指定することができます。 指定した場合、自動生成されるSQLのテーブル名がスキーマ.テーブル名になります。

カラム定義

カラム情報を指定するには、@Columnを使います。

name属性でカラム名を指定することができます。 指定しなかった場合、カラム名はフィールド名と同じになります。 フィールド名が、aaaBbbのようなキャメル記法の場合、 カラム名は、AAA_BBBのように'_'区切りだとみなされます。

このルールは、convention.diconで指定されている org.seasar.framework.convention.impl.PersistenceNamingConventionImplの fromPropertyNameToColumnName()の実装を変えることで、カスタマイズすることができます。

デフォルトでは、プロパティ名とフィールド名は同じになりますが、convention.diconで指定されている org.seasar.framework.convention.impl.PersistenceNamingConventionImplの fromFieldNameToPropertyName()の実装を変えることで、カスタマイズすることができます。

次の例では、AB1234というカラム名を指定しています。

@Column(name = "AB1234")
public String shortName;

insertable属性で挿入可能かどうかを指定することができます。 デフォルトはtrueです。 falseの場合、挿入用のSQLにこのカラムは含まれません。 カラムにデフォルト値を適用したい場合は、falseにすると良いでしょう。

updatable属性で更新可能かどうかを指定することができます。 デフォルトはtrueです。 falseの場合、更新用のSQLにこのカラムは含まれません。 カラムにデフォルト値を適用したい場合は、falseにすると良いでしょう。

識別子定義

識別子(主キー)であることを指定するには、@Idを使います。

@Id
public Integer id;

複合主キーの場合は、@Idを複数つけます。

@Id
public Integer id;

@Id
public Integer id2;

識別子は、アプリケーション側で生成することもできますが、 Seasar2に自動生成させることもできます。 自動生成させるには、@GeneratedValueを使います。

自動生成のタイプは、@GeneratedValuestrategy属性で指定します。 指定できるタイプのは次の4つです。

  • GenerationType.TABLE
    テーブルを使います。
  • GenerationType.SEQUENCE
    シーケンスを使います。
  • GenerationType.IDENTITY
    データベース固有の識別子自動生成を使います。
  • GenerationType.AUTO(デフォルト)
    データベースに応じてTABLE・SEQUENCE・IDENTITYのどちらかが選択されます。

GenerationType.TABLE

GenerationType.TABLEは次のようにして使います。

@Id
@GeneratedValue(strategy = GenerationType.TABLE)
public Integer id;

上記のように指定した場合、 あらかじめ、次のようなテーブルとデータを用意しておく必要があります。

create table ID_GENERATOR {
  PK varchar(80) not null primary key,
  VALUE integer);
insert into ID_GENERATOR (PK, VALUE) values ('EMPLOYEE_ID', 1);

ID_GENERATORテーブルのPKカラムには、<テーブル名>_<識別子のカラム名>を設定します。

テーブル名やカラム名をカスタマイズするには、次のように@TableGeneratorアノテーションで指定します。

@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "EMPLOYEE_GEN")
@TableGenerator(
    name = "EMPLOYEE_GEN",
    table = "ID_GEN",
    pkColumnName = "GEN_NAME",
    valueColumnName = "GEN_VALUE")
public Integer id;

上記のようなTableGeneratorを定義した場合、 あらかじめ、次のようなテーブルとデータを用意しておく必要があります。

create table ID_GEN {
  GEN_NAME varchar(80) not null primary key,
  GEN_VALUE integer);
insert into ID_GEN (GEN_NAME, GEN_VALUE) values ('EMPLOYEE_GEN', 1);

GenerationType.SEQUENCE

GenerationType.SEQUENCEは次のようにして使います。

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
public Integer id;

上記のように指定した場合、 あらかじめ、次のようなシーケンスを用意しておく必要があります。

create sequence EMPLOYEE_ID
  incremet by 50;

シーケンス名は、<テーブル名>_<識別子のカラム名>となります。

シーケンス名をカスタマイズするには、次のように@SequenceGeneratorアノテーションで指定します。

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "EMPLOYEE_GEN")
@SequenceGenerator(
    name = "EMPLOYEE_GEN",
    sequenceName = "EMPLOYEE_SEQ")
public Integer id;

上記のようなSequenceGeneratorを定義した場合、 あらかじめ、次のようなシーケンスを用意しておく必要があります。

create sequence EMPLOYEE_SEQ
  incremet by 50;

GenerationType.IDENTITY

GenerationType.IDENTITYは次のようにして使います。

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
public Integer id;

GenerationType.IDENTITYを使う場合、 識別子はデータベース固有の方法を使って自動生成されるようにしてください。

create table Employee {
  ID integer not null primary key generated always as identity,
  ...);

バージョン定義

バージョンチェック用であることを指定するには、@Versionを使います。 更新時に、エンティティの値とカラムの値が同一かどうかをチェックし、 同一ならカラムの値がインクリメントされて更新されます。 同一でない場合、javax.persistence.OptimisticLockExceptionが発生します。

例えば、エンティティのバージョンチェック用のプロパティの値が1だったとします。 更新時にカラムの値が1のままならOKで、2に更新されます。 カラムの値が1でない場合は、他で更新されているということなので、 javax.persistence.OptimisticLockExceptionが発生します。

@Version
public Long version = 0L;

注意点

@Versionアノテーションは数値型のフィールドにのみ指定することができます。JPA仕様ではTimestamp型のフィールドもバージョンチェックに使用できることになっていますが、S2JDBCではサポートしていません。更新時刻の保持と排他制御は目的が違うので別のフィールドにしてください。

永続化対象外定義

永続化対象外であることを指定するには、@Transientを使います。 transient修飾子も指定することもできますが、 セッションリプリケーションなどの直列化の対象から外れてしまうので、 @Transientの方をお勧めします。

@Transient
public MyDto myDto;

//こちらはお勧めしない
public transient MyDto myDto2;

ラージオブジェクト定義

ラージオブジェクトであることを指定するには、@Lobを使います。 プロパティの型がbyte[]の場合、自動的にBLOBと判定できるので指定する必要はありませんが、 プロパティの型がStringの場合、カラムの型が通常の文字列なのか、 CLOBなのか判断できないので、CLOBの場合は、@Lobを指定します。

@Lob
public String largeName;

多対一関連定義

多対一関連であることを指定するには、@ManyToOneを使います。 複数のEmployeeに1つのDepartementが関連付けられる場合、 EmployeeからみてDepartmentは多対一関連になります。 関連には、所有者、非所有者という概念があり、外部キーを持っているほうが所有者になります。

上記のケースは、Employeeのテーブルに、 department_id(プロパティ名はdepartmentId)という外部キーがあるので、 Employeeは関連の所有者になります。

@ManyToOneを定義するエンティティは、 必ず関連の所有者になるので、 外部キーに対応するプロパティが必要です。

public class Employee {
    ...
    public Integer departementId;
    
    @ManyToOne
    public Department department;
}

一対多関連定義

一対多関連であることを指定するには、@OneToManyを使います。 1つのDepartmentに複数のEmployeeが関連付けられる場合、 DepartmentからみてEmployeeは一対多関連になります。 関連には、所有者、非所有者という概念があり、外部キーを持っているほうが所有者になります。

上記のケースは、Employeeのテーブルに、 department_id(プロパティ名はdepartmentId)という外部キーがあり、 Departmentの方は、外部キーを持っていないので、 Departmentは関連の非所有者になります。

関連の非所有者の場合、必ずmappedBy属性で逆側の関連のプロパティ名を指定します。 Departmentからみて一対多でEmployeeが関連付けられていて、 そのEmployeeからみるとDepartmentがdepartmentプロパティとして、 多対一で関連付けられています。 お互いが相互の関連であることを示すためにmappedByでdepartmentを指定します。

一対多の関連の型は、List<エンティティ型>にします。 どのエンティティのリストなのかを示すためにGenericsの指定を忘れないようにしてください。

public class Department {
    ...
    @OneToMany(mappedBy = "department")
    public List<Employee> employeeList;
}

一対一関連定義

一対一関連であることを指定するには、@OneToOneを使います。 1つのEmployeeに1つのAddressが関連付けられる場合、 EmployeeからみてAddressは一対一関連になります。 同じように、1つのAddressに1つのEmployeeが関連付けられているので、 AddressからみてEmployeeも一対一関連になります。 関連には、所有者、非所有者という概念があり、外部キーを持っているほうが所有者になります。

上記のケースは、Employeeのテーブルに、 address_id(プロパティ名はaddressId)という外部キーがあるので、 Employeeは関連の所有者になります。 Addressの方は、外部キーを持っていないので、 Addressは関連の非所有者になります。

関連の非所有者の場合、必ずmappedBy属性で逆側の関連のプロパティ名を指定します。 Addressから見ると一対一でEmployeeが関連付けられていて、 そのEmployeeからみるとAddressがaddressプロパティとして、 一対一で関連付けられています。 お互いが相互の関連であることを示すためにmappedByでaddressを指定します。

public class Employee {
    ...
    public Integer addressId;
    
    @OneToOne
    public Address address;
}
public class Address {
    ...
    @OneToOne(mappedBy = "address")
    public Employee employee;
}

結合カラム定義

外部キーを持っている方を関連の所有者といいますが、 関連の所有者側では、@JoinColumnを使って、 結合用のカラムを指定することができます。

name属性で、外部キーを指定します。 name属性を省略した場合、「関連のプロパティ名_関連テーブルの主キー」が 自動的に設定されます。 主キーは、プロパティ名ではなく、カラム名なので注意してください。

public class Employee {
    ...
    public Integer departementId;
    
    @ManyToOne
    public Department department;
}

上記の例では、@JoinColumnが省略されているので、 name属性は、「関連のプロパティ名(department)_関連テーブルの主キー(ID)」、 つまりDEPARTMENT_IDになります。

departmentがDEPARTMENTに変換されているのは、 プロパティ名をカラム名に変換するときに、 キャメル記法は、'_'区切りになり、'_'以外は大文字に変換されるというルールがあるからです。

このルールは、convention.diconで指定されている org.seasar.framework.convention.impl.PersistenceNamingConventionImplの fromPropertyNameToColumnName()の実装を変えることで、カスタマイズすることができます。

referencedColumnName属性で、関連テーブルの主キーを指定します。 referencedColumnName属性を省略した場合、 「関連テーブルの主キー」が自動的に設定されます。 主キーは、プロパティ名ではなく、カラム名なので注意してください。

public class Employee {
    ...
    public Integer departementId;
    
    @ManyToOne
    public Department department;
}

上記の例では、@JoinColumnが省略されているので、 referencedColumnName属性は、「関連テーブルの主キー(ID)」、 つまりIDになります。

JDBCマネージャ

JdbcManagerは、データベースにアクセスするために使われるAPIです。 設定ファイルをセットアップして、 利用したいクラスで次のようにプロパティを定義しておけば、 Seasar2によってJdbcManagerは自動的に設定されます。

public JdbcManager jdbcManager;

Connection, Statementは同一トランザクション中はキャッシュされ、 トランザクション終了時に自動的にクローズされます。 トランザクション外で呼び出されたときは、キャッシュは行なわれず毎回自動的にクローズされます。

トランザクション管理は、S2Txを使ってください。

複数件検索

複数件を検索する場合は、gerResultList()を使います。

List<Employee> results = jdbcManager.from(Employee.class).getResultList();

検索するエンティティは、from()で指定します。

1件検索

1件検索する場合は、getSingleResult()を使います。

Employee result = jdbcManager.from(Employee.class).getSingleResult();

結果が複数件になる場合は、javax.persistence.NonUniqueResultExceptionが発生します。

結合

他のエンティティを結合する場合は、join()を使います。 引数は、結合したいエンティティのプロパティ名です。

List<Employee> results = jdbcManager.from(Employee.class).join("department").getResultList();

デフォルトは、左外部結合で、結合で指定したエンティティは、検索結果に含まれます。 次のように内部結合にすることもできます。

List<Employee> results = jdbcManager.from(Employee.class)
                                    .join("department", JoinType.INNER).getResultList();

検索条件には含めたいけど、検索結果には含めたくない場合、 3番名の引数をfalseにします。

List<Employee> results = jdbcManager.from(Employee.class)
                                    .join("department", JoinType.INNER, false)
                                    .where("department.name = 'RESEARCH'")
                                    .getResultList();

結合は、employee.addressのようにネストすることもできます。 ネストする場合は、必ずベースとなる結合を先に指定します。 employee.addressの場合、employeeがベースとなる結合です。

List<Department> results = jdbcManager.from(Department.class)
                                    .join("employee")
                                    .join("employee.address")
                                    .getResultList();

結合は、多対一関連、一対多関連、一対一関連のどれでも可能で、 いくつでもネストすることが可能です。

検索条件

検索条件を指定する場合は、where()を使います。 where()に書くことのできる条件は、SQLと同じです。 SQLとの違いは、カラム名の変わりにプロパティ名を書くことです。 関連先のプロパティを指定する場合は、join()で指定した名前. プロパティ名になります。

jdbcManager.from(Department.class)
    .join("employeeList")
    .join("employeeList.address")
    .where("employeeList.salary between ? and ? and employeeList.address.street like ?", ...)
    .getResultList();

検索条件の入力画面などでは、ユーザの入力があった部分をandでつないで、 条件を組み立てるということが良く行われます。 このようなケースを簡単に処理するために、 SimpleWhereとMapでも、検索条件を指定できるようにしています。

名前、仕事タイプ、給与の上限下限を条件に検索する画面を考えてみましょう。 べたに検索条件を組み立てるとはこんな感じになるはずです。

public String departmentName;
public String[] jobTypes = new String[0];
public BigDecimal salaryMax;
public BigDecimal salaryMin;
...
StringBuilder sb = new StringBuilder(100);
boolean found = false;
List<Object> values = new ArrayList<Object>();
if (departmentName != null) {
    sb.append("department.name = ?");
    values.add(departmentName);
    found = true;
}
if (jobTypes.length > 0) {
    if (found) {
        sb.append(" and ");
    }
    sb.append("jobType in (");
    for (JobType jt : jobTypes) {
        sb.append("?, ");
        values.add(jt);
    }
    sb.setLength(sb.length() - 2);
    sb.append(")");
    found = true;
}
if (salaryMin != null) {
    if (found) {
        sb.append(" and ");
    }
    sb.append("salary >= ?");
    values.add(salaryMin);
    found = true;
}
if (salaryMax != null) {
    if (found) {
        sb.append(" and ");
    }
    sb.append("salary <= ?");
    values.add(salaryMax);
    found = true;
}
jdbcManager.from(Employee.class).where(sb.toString(), values.toArray()).getResultList();

SimpleWhereを使って組み立てれば、こんなに簡単になります。

public String departmentName;
public String[] jobTypes = new String[0];
public BigDecimal salaryMax;
public BigDecimal salaryMin;
...
jdbcManager.from(Employee.class).where(
    new SimpleWhere().eq("department.name", departmentName).in("jobType", jobTypes)
        .ge("salary", salaryMin).le("salary", salaryMax))
    .getResultList();

SimpleWhereには次のメソッドがあります。

メソッド 説明
eq(String propertyName, Object value) propertyName = ?の条件を追加します。valueがnullの時は追加されません。
ne(String propertyName, Object value) propertyName <> ?の条件を追加します。valueがnullの時は追加されません。
lt(String propertyName, Object value) propertyName < ?の条件を追加します。valueがnullの時は追加されません。
le(String propertyName, Object value) propertyName <= ?の条件を追加します。valueがnullの時は追加されません。
gt(String propertyName, Object value) propertyName > ?の条件を追加します。valueがnullの時は追加されません。
ge(String propertyName, Object value) propertyName >= ?の条件を追加します。valueがnullの時は追加されません。
in(String propertyName, Object... values) propertyName in (?, ...)の条件を追加します。 valuesの配列の長さが0の時は追加されません。
not in(String propertyName, Object... values) propertyName not in (?, ...)の条件を追加します。 valuesの配列の長さが0の時は追加されません。
like(String propertyName, String value) propertyName like ?の条件を追加します。 valueがnullの時は追加されません。
starts(String propertyName, String value) propertyName like ?の条件を追加します。 valueがnullの時は追加されません。 valueの最後に自動的に%が追加されます。
ends(String propertyName, String value) propertyName like ?の条件を追加します。 valueがnullの時は追加されません。 valueの最初に自動的に%が追加されます。
contains(String propertyName, String value) propertyName like ?の条件を追加します。 valueがnullの時は追加されません。 valueの最初と最後に自動的に%が追加されます。
isNull(String propertyName, Boolean value) propertyName is nullの条件を追加します。 valueがnullあるいはBoolean.FALSEの時は追加されません。
isNotNull(String propertyName, Boolean value) propertyName is not nullの条件を追加します。 valueがnullあるいはBoolean.FALSEの時は追加されません。

Mapを使うと次のようになります。

public String departmentName;
public String[] jobTypes = new String[0];
public BigDecimal salaryMax;
public BigDecimal salaryMin;
...
Map<String, Object> w = new HashMap<String, Object>();
w.put("department.name", departmentName);
w.put("jobType_IN", jobTypes);
w.put("salary_LE", salaryMax);
w.put("salary_GE", salaryMin);
jdbcManager.from(Employee.class).where(w).getResultList();

マップのキーにプロパティ名_サフィックスを指定します。 サフィックスがない場合は、_EQが指定されたとみなされます。

次のサフィックスを使うことができます。

サフィックス 説明
_EQ propertyName = ?の条件を追加します。valueがnullの時は追加されません。
_NE propertyName <> ?の条件を追加します。valueがnullの時は追加されません。
_LT propertyName < ?の条件を追加します。valueがnullの時は追加されません。
_LE propertyName <= ?の条件を追加します。valueがnullの時は追加されません。
_GT propertyName > ?の条件を追加します。valueがnullの時は追加されません。
_GE propertyName >= ?の条件を追加します。valueがnullの時は追加されません。
_IN propertyName in (?, ...)の条件を追加します。 valuesの配列の長さが0の時は追加されません。 値は配列のみをサポートしています。
_NOT_IN propertyName not in (?, ...)の条件を追加します。 valuesの配列の長さが0の時は追加されません。 値は配列のみをサポートしています。
_LIKE propertyName like ?の条件を追加します。 valueがnullの時は追加されません。
_STARTS propertyName like ?の条件を追加します。 valueがnullの時は追加されません。 valueの最後に自動的に%が追加されます。
_ENDS propertyName like ?の条件を追加します。 valueがnullの時は追加されません。 valueの最初に自動的に%が追加されます。
_CONTAINS propertyName like ?の条件を追加します。 valueがnullの時は追加されません。 valueの最初と最後に自動的に%が追加されます。
_IS_NULL propertyName is nullの条件を追加します。 valueがnullあるいはBoolean.FALSEの時は追加されません。 値はBooleanのみをサポートしています。
_IS_NOT_NULL propertyName is not nullの条件を追加します。 valueがnullあるいはBoolean.FALSEの時は追加されません。 値はBooleanのみをサポートしています。

マップの組み立てを手動でやる必要は、基本的にありません。 なぜなら、SimpleWhereのほうが便利だからです。 マップを使う場合は、次のようにorg.seasar.framework.beans.util.BeanUtil.createProperties()と組み合わせます。 createProperties()の第二引数には、プロパティ名のプレフィックスを指定します。 プレフィックスを指定した場合、プレフィックスを持つプロパティだけが対象になります。 プロパティ名がマップのキーに変換されるときに、 プレフィックスは削除され、'$'は'.'に変換されます。

public String where_department$name;
public String[] where_jobType_IN = new String[0];
public BigDecimal where_salary_LE;
public BigDecimal where_salary_GE;
...
jdbcManager.from(Employee.class).where(BeanUtil.createProperties(this, "where_")).getResultList();

ソート順

ソート順を指定する場合は、orderBy()を使います。 orderBy()に書くことのできる条件は、SQLと同じです。 SQLとの違いは、カラム名の変わりにプロパティ名を書くことです。 関連先のプロパティを指定する場合は、join()で指定した名前. プロパティ名になります。

jdbcManager.from(Employee.class)
    .join("department")
    .orderBy("name, department.name")
    .getResultList();

ページング

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

jdbcManager.from(Employee.class)
    .orderBy("name")
    .limit(100)
    .offset(10)
    .getResultList();

最大行数

最大行数を指定する場合は、maxRows()を使います。 最大行数を超える行は、通知なしに除外されます。

jdbcManager.from(Employee.class)
    .maxRows(1000)
    .getResultList();

フェッチサイズ

フェッチサイズを指定する場合は、fetchSize()を使います。

jdbcManager.from(Employee.class)
    .fetchSize(100)
    .getResultList();

クエリタイムアウト

クエリタイムアウト(秒)を指定する場合は、queryTimeout()を使います。

jdbcManager.from(Employee.class)
    .queryTimeout(10)
    .getResultList();

挿入

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

int count = jdbcManager.insert(employee).execute();

引数はエンティティで、戻り値は、更新した行数です。 挿入するときに、識別子を自動設定することができます。 詳しくは、識別子定義を参照してください。

バッチ挿入

複数のエンティティをバッチ挿入する場合は、insertBatch()execute()を組み合わせます。

int[] countArray = jdbcManager.insertBatch(employees).execute();

引数はエンティティのリストあるいは配列(可変長引数)で、戻り値は、更新した行数の配列です。 挿入するときに、識別子を自動設定することができます。 詳しくは、識別子定義を参照してください。

nullの項目を更新しない

更新の対象からnullの項目を除外する場合は、excludesNull()を使います。 バッチ系の更新は、すべてのエンティティに同じSQLを適用しなければならないので、 nullを除外してバッチ更新することはできません。 なぜなら、すべてのエンティティのnullの項目が同じだとは限らないからです。

int count = jdbcManager.insert(employee).excludesNull().execute();

指定したプロパティのみを更新対象にする

指定したプロパティのみを更新対象にする場合は、includes()を使います。

int count = jdbcManager.insert(employee).includes("id", "name").execute();

指定したプロパティを更新対象から除外する

指定したプロパティを更新対象から除外する場合は、excludes()を使います。

int count = jdbcManager.insert(employee).includes("version").execute();

更新

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

int count = jdbcManager.update(employee).execute();

引数はエンティティで、戻り値は、更新した行数です。 更新するときに、バージョンによる楽観的排他制御をすることができます。 詳しくは、バージョン定義を参照してください。

バッチ更新

複数のエンティティをバッチ更新する場合は、updateBatch()execute()を組み合わせます。

int[] countArray = jdbcManager.updateBatch(employees).execute();

引数はエンティティのリストあるいは配列(可変長引数)で、戻り値は、更新した行数の配列です。 更新するときに、バージョンによる楽観的排他制御をすることができます。 詳しくは、バージョン定義を参照してください。

バージョンプロパティを通常の更新対象にする

バージョンプロパティを通常の更新対象に含め、バージョンチェックの対象外にする場合は、 includesVersion()を使います。

int count = jdbcManager.update(employee).includesVersion().execute();

変更のあったプロパティのみを更新対象にする

変更のあったプロパティのみを更新対象にする場合は、 changedFrom()を使います。 最初の引数は、比較の対象にする以前のエンティティもしくはマップです。

int count = jdbcManager.update(employee).changedFrom(before).execute();

削除

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

int count = jdbcManager.delete(employee).execute();

引数はエンティティで、戻り値は、更新した行数です。

バッチ削除

複数のエンティティをバッチ削除する場合は、deleteBatch()execute()を組み合わせます。

int[] countArray = jdbcManager.deleteBatch(employees).execute();

引数はエンティティのリストあるいは配列(可変長引数)で、戻り値は、更新した行数の配列です。

その他の機能

SQLのログ

Seasar2で実行したSQLのログは、アプリケーションから参照することが可能です。 SQLのログはorg.seasar.extension.jdbc.SqlRegistryのインスタンスから取得します。

SqlRegistryのインスタンスはDIもしくはルックアップにより取得できます。

DIのための設定は次のようになります。

<component name="sqlLogRegistry" class="org.seasar.extension.jdbc.SqlLogRegistry">
  @org.seasar.extension.jdbc.SqlLogRegistryLocator@getInstance()
</component>

プログラムから直接ルックアップする場合は次のようなコードが必要です。

SqlLogRegistry sqlLogRegistry = SqlLogRegistryLocator.getInstance();
SqlRegistryのインスタンスは複数のログを保持しています(デフォルトでは最新3つのログを保持します)。 最新の(最後に実行された)SQLのログは次のようなコードで取得できます。
SqlLog sqlLog = sqlLogRegistry.getLast();
org.seasar.extension.jdbc.SqlLogのインスタンスは次の情報を保持しています。
  • 未加工のSQL
  • バインド変数を実際の値で置き換えた完全なSQL
  • SQLにバインドされる値の配列
  • SQLにバインドされる値の型の配列