Dropwizardを普通に使うとトランザクション境界をResourceクラスにすると思います。
しかし、DDDで作成したい場合などはアプリケーション層のサービス(この記事ではファサードクラス)をトランザクション境界にしたくなると思います。
しかし、ファサードクラスを作成して対象のメソッドに@UnitOfWork
を付けて試したところ、以下のエラーが出て動作しませんでした。
org.hibernate.HibernateException: No session currently bound to execution context
どうやらResourceクラスで@UnitOfWork
を使うか、または、自分自身でHibernateのSessionを扱う必要があるようです。
自分自身でHibernateのSessionを扱うとなるとファサードクラスがそれに依存してしまうので宜しくないです。また、依存しないように実装もできると思いますが、そもそも、それが面倒です。
ということで、この辺りはDIコンテナに任したいと思います。
maven設定
Guiceを使います。GuiceにJPAを管理させるので、guice-persist
、hibernate-entitymanager
も依存させます。
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>3.0</version>
</dependency>
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-persist</artifactId>
<version>3.0</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.1.Final</version>
<exclusions>
<exclusion>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
Applicationクラス
まずは、わかり易くするためにDropwizardデフォルトの場合を記述します。
public class BasicSampleApplication extends
Application<BasicSampleConfiguration> {
public static void main(String[] args) throws Exception {
new BasicSampleApplication().run(args);
}
//ここが必要なくなります(がHealthcheck用に残していいかも)
private final HibernateBundle<BasicSampleConfiguration> hibernateBundle = new HibernateBundle<BasicSampleConfiguration>(
Person.class) {
@Override
public DataSourceFactory getDataSourceFactory(
BasicSampleConfiguration configuration) {
return configuration.getDataSourceFactory();
}
};
@Override
public String getName() {
return "hello-ddd";
}
@Override
public void initialize(Bootstrap<BasicSampleConfiguration> bootstrap) {
bootstrap.addBundle(new MigrationsBundle<BasicSampleConfiguration>() {
@Override
public DataSourceFactory getDataSourceFactory(
BasicSampleConfiguration configuration) {
return configuration.getDataSourceFactory();
}
});
//ここがいらなくなります(がHealthcheck用に残していいかも)
bootstrap.addBundle(hibernateBundle);
}
@Override
public void run(BasicSampleConfiguration configuration,
Environment environment) throws ClassNotFoundException {
//ここが変わります
final PersonDao dao = new PersonDao(hibernateBundle.getSessionFactory());
environment.jersey().register(new PeopleResource(dao));
}
}
トランザクション境界を変えるために変更したのが以下になります。
public class SampleDDDApplication extends Application<SampleDDDConfiguration> {
public static void main(String[] args) throws Exception {
new SampleDDDApplication().run(args);
}
@Override
public String getName() {
return "hello-ddd";
}
@Override
public void initialize(Bootstrap<SampleDDDConfiguration> bootstrap) {
bootstrap.addBundle(new MigrationsBundle<SampleDDDConfiguration>() {
@Override
public DataSourceFactory getDataSourceFactory(
SampleDDDConfiguration configuration) {
return configuration.getDataSourceFactory();
}
});
}
@Override
public void run(SampleDDDConfiguration configuration,
Environment environment) throws ClassNotFoundException {
//ここを変更しました
Injector injector = createInjector(configuration);
environment.jersey().register(
injector.getInstance(PeopleResource.class));
//ここを追加しました
environment.lifecycle().manage(
injector.getInstance(JPAInitializer.class));
}
//ここを追加しました
private Injector createInjector(final SampleDDDConfiguration conf) {
return Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(JPAInitializer.class).asEagerSingleton();
bind(SampleDDDConfiguration.class).toInstance(conf);
bind(DataSourceFactory.class).toInstance(
conf.getDataSourceFactory());
}
}, createJpaPersistModule(conf.getDataSourceFactory()));
}
//ここを追加しました
private JpaPersistModule createJpaPersistModule(DataSourceFactory conf) {
Properties props = new Properties();
props.put("javax.persistence.jdbc.url", conf.getUrl());
props.put("javax.persistence.jdbc.user", conf.getUser());
props.put("javax.persistence.jdbc.password", conf.getPassword());
props.put("javax.persistence.jdbc.driver", conf.getDriverClass());
JpaPersistModule jpaModule = new JpaPersistModule("Default");
jpaModule.properties(props);
return jpaModule;
}
}
ポイントは、3つになります。
- jerseyに登録するクラスをGuice管理のインスタンスを使う
- 自作クラスの
JPAInitializer
をconfigure()でbindし、DropwizardのManaged Objectsとして管理している - JpaPersistModuleのPropertiesにDropwizard管理の設定クラスの値を設定する
2番目のJPAInitializerクラスについては、最初はApplication#run()メソッド内でinjectorオブジェクトを使って呼び出してみました。
しかし、Guiceの管理が「卵が先か鶏が先か」的にEntityManagerをbindできなかったので、asEagerSingleton()
で先にインスタンスしています。
以下がJPAInitializerクラスになります。
@Singleton
public class JPAInitializer implements Managed {
private final PersistService service;
@Inject
public JPAInitializer(final PersistService service) {
this.service = service;
}
@Override
public void start() throws Exception {
service.start();
}
@Override
public void stop() throws Exception {
service.stop();
}
}
上記はDropwizardのManaged Objectsという仕組みのクラスになります。
Managed
インターフェイスを実装して、HTTP Serverのライフサイクルに組み込むことができます。
ちなみにstartで例外が出たらアプリは起動しません。stopで例外が出てもシャットダウンはします。
このサンプルでは、サーバー起動・終了とJPAのライフサイクルを合わせています。
ポイントの3番目のJpaPersistModuleは、persistence.xmlを使って作成しています。
但し、通常はこのファイル内でPropertiesの値も記述しますが、それはymlファイルで設定している内容と同じなので、ここでは記述しないで、Applicationクラスのコードで設定しています。
以下がpersistence.xmlです。
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0">
<persistence-unit name="Default" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
</persistence-unit>
</persistence>
Resourceクラス
通常のDropwizardでは、ResourceクラスDaoクラスをインスタンスします。
そこをトランザクション境界を変えたいので、Facadeクラスに変更しています。
また、@UnitOfWork
は、トランザクション境界をFacadeクラスにするので削除しています。
あとは通常と同じです。
@Path("/people")
@Produces(MediaType.APPLICATION_JSON)
public class PeopleResource {
private final PeopleFacade facade;
@Inject
public PeopleResource(PeopleFacade facade) {
this.facade = facade;
}
@POST
public Person createPerson(Person person) {
return facade.create(person);
}
@GET
public List<Person> listPeople() {
return facade.findAll();
}
@GET
@Path("/{personId}")
public Person getPerson(@PathParam("personId") LongParam personId) {
final Optional<Person> person = facade.findById(personId.get());
if (!person.isPresent()) {
throw new NotFoundException("{status:notfound}");
}
return person.get();
}
}
Facadeクラス
トランザクション境界のクラスです。
トランザクション管理をJPA+Guiceでしているので、@Transactional
をメソッドに付与しています。
このメソッドで例外が投げられるとロールバックします。
このサンプルでは、あまり意味がないように思えるかもしれませんが、通常のファサードであれば、一つのメソッドから複数のRepositoryが呼ばれることが多いはずです。
そういうアプリの場合であれば、ここをトランザクション境界にしたくなるはずです。
public class PeopleFacade {
@Inject
private PersonRepository repository;
@Transactional
public Person create(Person person) {
return repository.create(temp);
}
@Transactional
public List<Person> findAll() {
return repository.findAll();
}
@Transactional
public Optional<Person> findById(Long id) {
return repository.findById(id);
}
}
Repositoryクラス
まずは、通常のDropwizardでのDaoクラスを記述します。
このクラスは色々と変更することになります。
public class PersonDao extends AbstractDAO<Person> {
@Inject
public PersonDao(SessionFactory factory) {
super(factory);
}
public Optional<Person> findById(Long id) {
return Optional.fromNullable(get(id));
}
public Person create(Person person) {
return persist(person);
}
public List<Person> findAll() {
return list(namedQuery("com.github.ko2ic.core.Person.findAll"));
}
}
以下が変更したクラスです。
@Singleton
public class PersonRepository {
private final Provider<EntityManager> entityManager;
@Inject
public PersonRepository(Provider<EntityManager> entityManager) {
this.entityManager = entityManager;
}
public Optional<Person> findById(Long id) {
return Optional
.fromNullable(entityManager.get().find(Person.class, id));
}
public Person create(Person person) {
entityManager.get().persist(person);
return person;
}
@SuppressWarnings("unchecked")
public List<Person> findAll() {
return entityManager.get()
.createNamedQuery("com.github.ko2ic.core.Person.findAll")
.getResultList();
}
}
まず、元々Dropwizardで提供されていたAbstractDAOが利用しても意味がないので継承していません。
そして、EntityManagerを使って、DBにアクセスしています。
あと、フィールドは、Provider<EntityManager>にしないとうまく動かないです。
理由は
- PersonRepositoryをsingletonにしているが、EntityManagerは違うため
- 常に同じインスタンスのEntityManagerを使おうとして例外が起きるため(EntityManagerにはライフサイクルがあり、一つのトランザクションが終わるとEntityManagerのライフサイクルは終わる)
です。