題材は「はじめてのSpring Boot」にある顧客管理システム。
最近よく聞くDomaを試してみたかったのもあり、
JPAじゃなくDoma2、MavenじゃなくGradleで挑戦しました。
ちなみにまだRestAPIができただけです^^;
このエントリでは「はじめてのSpring Boot」に載ってるところは説明省略します。
すごいわかりやすかったので買って読む価値はあると思いますよ。
環境
今回ためした環境は以下のとおりです。
Java8
SpringBoot1.1.9
Doma2.0.1
H2
Gradle2.2.1
構成
ソースはこちら。
https://github.com/nyasba/domaboot.git
一部省略してますが、こんなかんじです。
$ tree
.
├── build.gradle
└── src
└── main
├── java
│ └── com
│ └── example
│ ├── App.java
│ ├── AppConfig.java
│ ├── api
│ │ ├── CustomerRequest.java
│ │ └── CustomerRestController.java
│ ├── datasource
│ │ ├── CustomerRepository.java
│ │ └── DomaRepository.java
│ ├── domain
│ │ └── CustomerEntity.java
│ └── service
│ └── CustomerService.java
└── resources
├── META-INF
│ └── com
│ └── example
│ └── datasource
│ └── CustomerRepository
│ ├── findAllOrderByName.sql
│ └── findById.sql
├── data.sql
├── logback.xml
├── schema.sql
└── templates
└── dummy.html
Domaを使う
DB接続設定
今回はH2database(メモリ)を使ってます。
接続先の定義はDataSourceで書くだけで簡単ですね。
[12/26修正]
Domaの返却するDataSourceはそのままだとSpringの管理外になるため、実行時例外発生時にロールバックされなくなるとのこと。TransactionAwareDataSourceProxyで包むように修正。
@nyasba @ Transactionalをつけたメソッド内でinsertした後Runtime例外投げてもコミットされていると思います。Springの管理するトランザクションがスレッドローカルで結び付いているのでそこに参加させる必要があるはずです
— Toshiaki Maki (@making) 2014, 12月 26
package com.example;
import net.sf.log4jdbc.Log4jdbcProxyDataSource;
import org.seasar.doma.jdbc.Config;
import org.seasar.doma.jdbc.NoCacheSqlFileRepository;
import org.seasar.doma.jdbc.SimpleDataSource;
import org.seasar.doma.jdbc.SqlFileRepository;
import org.seasar.doma.jdbc.dialect.Dialect;
import org.seasar.doma.jdbc.dialect.H2Dialect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration
@EnableTransactionManagement
public class AppConfig {
DataSource realDataSource(){
SimpleDataSource dataSource = new SimpleDataSource();
dataSource.setUrl("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
dataSource.setUser("sa");
return dataSource;
}
// 12/26修正
@Bean
DataSource dataSource(){
return new TransactionAwareDataSourceProxy(
new Log4jdbcProxyDataSource(
this.realDataSource()
)
);
}
@Bean
Dialect dialect(){
return new H2Dialect();
}
@Bean
SqlFileRepository sqlFileRepository(){
return new NoCacheSqlFileRepository();
}
@Bean
Config config(){
return new Config() {
@Override
public DataSource getDataSource() {
return dataSource();
}
@Override
public Dialect getDialect() {
return dialect();
}
@Override
public SqlFileRepository getSqlFileRepository(){
return sqlFileRepository();
}
};
}
}
Dao
DBにアクセスするためのInterfaceを作成します。
これらのクラスには@Daoをつけます。
ここで作ったメソッド1つ1つにSQLが対応します。
package com.example.datasource;
import com.example.domain.CustomerEntity;
import org.seasar.doma.*;
import org.seasar.doma.jdbc.Result;
import java.util.List;
@DomaRepository
@Dao
public interface CustomerRepository {
@Select
public List<CustomerEntity> findAllOrderByName();
@Select
public CustomerEntity findById(Integer id);
@Insert
public Result<CustomerEntity> insert(CustomerEntity customerEntity);
@Update
public Result<CustomerEntity> update(CustomerEntity customerEntity);
@Delete
public Result<CustomerEntity> delete(CustomerEntity customerEntity);
}
@DomaRepositoryはSpringのAutowiredに対応するためのアノテーションです。
CustomerServiceでAutowiredしているのでそのために必要になります。
(SpringBootとDomaを連携するを参考に作りました)
package com.example.datasource;
import org.seasar.doma.AnnotateWith;
import org.seasar.doma.Annotation;
import org.seasar.doma.AnnotationTarget;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@AnnotateWith(annotations = {
@Annotation(target = AnnotationTarget.CLASS, type = Component.class),
@Annotation(target = AnnotationTarget.CONSTRUCTOR, type= Autowired.class)
})
public @interface DomaRepository {
}
検索処理
SQLは、
src/main/resources/META-INF/<package名>/<class名>/<method名>.sql
という形式で作成します。
例えば以下のメソッドの場合は
@Select
public CustomerEntity findById(Integer id);
このようなファイルを作ります。
src/main/resources/META-INF/com/example/datasource/CustomerRepository/findById.sql
中身はこのようになってます。
DomaのSQLは2WaySQLといって、そのまま実行できることが特徴です。
select * from customers where id = /* id */1;
[参考] http://doma.seasar.org/reference/sqlfile.html
SQLファイルは、SQL文を格納したテキストファイルで、Daoのメソッドにマッピングされます。 SQLのブロックコメント(/* */)や行コメント(--)を使用することで、バインド変数や動的なSQLのための条件分岐を表現できます。 SQLのツールでそのままそのSQLを実行できるように、バインド変数にはテスト用のデータを指定します。テスト用のデータは、実行時には使用されません。 たとえば、SQLファイルには次のようなSQL文が格納されます。
select * from employee where employee_id = /* employeeId */99
ここでは、ブロックコメントで囲まれた employeeIdがDaoインタフェースのメソッドのパラメータに対応し、 直後の 99はテスト用の条件になります。
結果を格納するオブジェクト(Entity)
結果はEntityに格納されます。
今回はオブジェクトをimmutableにしてます。
Selectで抽出したカラムとオブジェクトの変数との関係は@Entityのnamingで指定しています。
NamingType.SNAKE_UPPER_CASEを使うと、lastNameという変数にはLAST_NAMEというカラムの値が入ります。
@Tableのテーブル名はSQLファイルに書かれてるので検索に関しては使ってないです。
package com.example.domain;
import lombok.Getter;
import org.seasar.doma.*;
import org.seasar.doma.jdbc.entity.NamingType;
@Entity(naming = NamingType.SNAKE_UPPER_CASE, immutable = true)
@Table(name = "customers")
public class CustomerEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@SequenceGenerator(sequence = "seq_customers")
@Getter
private final Integer id;
@Getter
private final String lastName;
@Getter
private final String firstName;
public CustomerEntity( String lastName, String firstName ){
// DBの自動採番を使うためにnullを設定
this.id = null;
this.lastName = lastName;
this.firstName = firstName;
}
public CustomerEntity( Integer id ,String lastName, String firstName ){
this.id = id;
this.lastName = lastName;
this.firstName = firstName;
}
}
登録/更新処理
登録/更新処理は専用のアノテーションを付けることで実現していますので、別にSQLを作る必要はありません。登録/更新したいEntityを引数で渡して実行するだけです。
@Insert
public Result<CustomerEntity> insert(CustomerEntity customerEntity);
@Update
public Result<CustomerEntity> update(CustomerEntity customerEntity);
ただ、IDの自動採番については少々はまりました。
Entityの中で@GeneratedValueを使って、H2のシーケンステーブルから採番するように記載していましたが、Entityをimmutableにしていたため、何を設定するかで困りました。
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@SequenceGenerator(sequence = "seq_customers")
@Getter
private final Integer id;
適当な値(1とか)ではダメで、nullを設定しておかないとInsert時に自動採番できないようです。
そのためコンストラクタを2つ用意しています。
Lombokの自動生成とも相性が悪く、IntelliJでは@AllArgsConstructor使えませんでした(Eclipseでは使えるらしい)
ビルド
DomaのSQLファイルをコンパイルより前に出力先Dirにコピーするために以下のように依存関係を変える必要があるようです。
processResources.destinationDir = compileJava.destinationDir
compileJava.dependsOn processResources
まとめ
SpringBootでDoma,Gradleを試してみました。
Domaは思った以上に簡単に使えたので気に入りました d(゚∀゚d)
簡単なアプリならこれで十分ですね。