DropwizardでGuiceを利用してトランザクション境界を変える

  • 14
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

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-persisthibernate-entitymanagerも依存させます。

pom.xml
    <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デフォルトの場合を記述します。

BasicSampleApplication.java
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));
    }
}

トランザクション境界を変えるために変更したのが以下になります。

SampleDDDApplication.java
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クラスになります。

JPAInitializer.java
@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です。

src/main/resources/META-INF/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が呼ばれることが多いはずです。
そういうアプリの場合であれば、ここをトランザクション境界にしたくなるはずです。

PeopleFacade.java
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クラスを記述します。
このクラスは色々と変更することになります。

PersonDao.java
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"));
    }
}

以下が変更したクラスです。

PersonRepository.java
@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のライフサイクルは終わる)

です。