いますぐ採用すべきJavaフレームワークDropWizard(その３)


[その1](http://qiita.com/ko2ic/items/cfe5b2f4593b705f9463)と[その2](http://qiita.com/ko2ic/items/069c5f75823fd8964bfa)の続きになります。

今回は以下を記述します。

* hibernateでのDBアクセスの実装
* jdbiでのDBアクセス

# hibernateでのDBアクセスの実装

## maven設定

Dropwizardではhibernateを利用するための仕組みが用意されています。

```pom.xml
    <dependency>
      <groupId>io.dropwizard</groupId>
      <artifactId>dropwizard-hibernate</artifactId>
      <version>${dropwizard.version}</version>
    </dependency>
```

## Entity

まずはよくあるPOJOなentityを作成

```Person.java
@Entity
@Table(name = "people")
@NamedQueries({ @NamedQuery(name = "com.github.ko2ic.core.Person.findAll", query = "SELECT p FROM Person p")})
public class Person {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private long id;

	@Column(name = "fullName", nullable = false)
	private String fullName;

	@Column(name = "jobTitle", nullable = false)
	private String jobTitle;

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getFullName() {
		return fullName;
	}

	public void setFullName(String fullName) {
		this.fullName = fullName;
	}

	public String getJobTitle() {
		return jobTitle;
	}

	public void setJobTitle(String jobTitle) {
		this.jobTitle = jobTitle;
	}
}
```

## Dao

io.dropwizard.hibernate.AbstractDAOを継承して作ります。

```PersonDAO.java
public class PersonDAO extends AbstractDAO<Person> {
	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"));
	}
}
```

## Resourceクラス

@UnitOfWorkを記述するとそこがトランザクション境界になります。

```PeopleResource.java
@Path("/people")
@Produces(MediaType.APPLICATION_JSON)
public class PeopleResource {

	private final PersonDAO peopleDAO;

	public PeopleResource(PersonDAO peopleDAO) {
		this.peopleDAO = peopleDAO;
	}

	@POST
	@UnitOfWork
	public Person createPerson(Person person) {
		return peopleDAO.create(person);
	}

	@GET
	@UnitOfWork
	public List<Person> listPeople() {
		return peopleDAO.findAll();
	}

	@GET
	@UnitOfWork
	@Path("/{personId}")
	public Person getPerson(@PathParam("personId") LongParam personId) {
		final Optional<Person> person = peopleDAO.findById(personId.get());
		if (!person.isPresent()) {
			throw new NotFoundException("{status:notfound}");
		}
		return person.get();
	}
}
```

## Applicationクラス

フィールドでDropwizardのabstractなHibernateBundleクラスをインスタンス化して、登録します。

```HelloWorldApplication.java

	private final HibernateBundle<HelloWorldConfiguration> hibernateBundle = new HibernateBundle<HelloWorldConfiguration>(
			Person.class) {
		@Override
		public DataSourceFactory getDataSourceFactory(
				HelloWorldConfiguration configuration) {
			return configuration.getDataSourceFactory();
		}
	};

	@Override
	public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) {
		・・・
		bootstrap.addBundle(hibernateBundle);
	    ・・・
	}

	@Override
	public void run(HelloWorldConfiguration configuration,
			Environment environment) throws ClassNotFoundException {
       ・・・
		final PersonDAO dao = new PersonDAO(hibernateBundle.getSessionFactory());
		environment.jersey().register(new PeopleResource(dao));
       ・・・
	}
```

環境設定にdatabase関連を追加します。

```example.yml
database:

  # the name of your JDBC driver
  driverClass: org.h2.Driver

  # the username
  user: sa

  # the password
  password: sa

  # the JDBC URL
  url: jdbc:h2:target/example

  # any properties specific to your JDBC driver:
  properties:
    charSet: UTF-8
    hibernate.dialect: org.hibernate.dialect.H2Dialect

  # the maximum amount of time to wait on an empty pool before throwing an exception
  maxWaitForConnection: 1s

  # the SQL query to run when validating a connection's liveness
  validationQuery: "/* MyApplication Health Check */ SELECT 1"

  # the minimum number of connections to keep open
  minSize: 8

  # the maximum number of connections to keep open
  maxSize: 32

  # whether or not idle connections should be validated
  checkConnectionWhileIdle: false
```

## スキーマ作成

まずは、[Dropwizard(Java)でrailsのようにDBマイグレーションをする]()で書いたようにliquibaseで作成します。
liquibaseを利用する方法はそちらを参照してください。

```migration.xml
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
         http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
	<changeSet id="1" author="ko2ic">
		<createTable tableName="people">
			<column name="id" type="bigint" autoIncrement="true">
				<constraints primaryKey="true" nullable="false" />
			</column>
			<column name="fullName" type="varchar(255)">
				<constraints nullable="false" />
			</column>
			<column name="jobTitle" type="varchar(255)" />
		</createTable>
	</changeSet>
</databaseChangeLog>
```

```mvn package```して、スキーマを作ります。

```
$ java -jar target/spike-dropwizard-1.0-SNAPSHOT.jar  db migrate example.yml
```

## 動作確認

起動すると先ほどのリソースが表示されています。

```
・・・
    GET     /people (com.github.ko2ic.resources.PeopleResource)
    GET     /people/{personId} (com.github.ko2ic.resources.PeopleResource)
    POST    /people (com.github.ko2ic.resources.PeopleResource)
・・・

```

まずは、postでデータを2件登録してみます。

```
$ curl -H "Content-Type: application/json" -X POST -d '{"fullName":"Coda Hale", "jobTitle" : "Chief Wizard" }' http://localhost:8080/people
{"id":1,"fullName":"Coda Hale","jobTitle":"Chief Wizard"}

$ curl -H "Content-Type: application/json" -X POST -d '{"fullName":"ko2ic", "jobTitle" : "hoge" }' http://localhost:8080/people
{"id":2,"fullName":"ko2ic","jobTitle":"hoge"}
```

検索してみます。

```
$ curl http://localhost:8080/people
[{"id":1,"fullName":"Coda Hale","jobTitle":"Chief Wizard"},{"id":2,"fullName":"ko2ic","jobTitle":"hoge"}]

$ curl http://localhost:8080/people/2
{"id":2,"fullName":"ko2ic","jobTitle":"hoge"}

$ curl http://localhost:8080/people/3
{status:notfound}
```

ちゃんと想定通りのJSONが返ってきています。

## Health Check

hibernateの場合は、bundleを登録する処理の内部でHealth Checkの登録も行われています。(名前はhibernate)
なので、環境設定で```validationQuery```に記述しておくだけで、Health Checkが行われます。
deadlocksについては、[その1](http://qiita.com/ko2ic/items/cfe5b2f4593b705f9463#1-10)を参照してください。

```
$ curl http://localhost:8081/healthcheck
{"deadlocks":{"healthy":true},"hibernate":{"healthy":true}}
```

# jdbiでのDBアクセス

## maven設定

Dropwizardではjdbiを利用するための仕組みが用意されています。

```pom.xml
    <dependency>
      <groupId>io.dropwizard</groupId>
      <artifactId>dropwizard-jdbi</artifactId>
      <version>${dropwizard.version}</version>
    </dependency>
```

## Entity

hibernateと同じものを使うので省略。(jdbiの場合はJPAのアノテーションはいらない)

## ResultSetMapper

JDBIの場合は、resultsetとエンティティをマッピングするインターフェイスを実装する必要があります。(String型などを返す場合はもちろん必要ありません)

```
public class PersonJdbiMapper implements ResultSetMapper<Person> {

	@Override
	public Person map(int index, ResultSet r, StatementContext ctx)
			throws SQLException {
		Person person = new Person();
		person.setId(r.getInt("id"));
		person.setFullName(r.getString("fullName"));
		person.setJobTitle(r.getString("jobTitle"));
		return person;
	}
}
```

## Dao

JDBIはインターフェイスだけになります。

```PersonJdbiDAO.java
public interface PersonJdbiDAO {

	@SqlQuery("select id, fullName, jobTitle from people where id = :id")
	@Mapper(PersonJdbiMapper.class)
	public Person findById(@Bind("id") Long id);

	@SqlQuery("select id, fullName, jobTitle from people")
	@Mapper(PersonJdbiMapper.class)
	public List<Person> findAll();
}
```

## Resourceクラス

```PeopleJdbiResource.java
@Path("/people/jndi")
@Produces(MediaType.APPLICATION_JSON)
public class PeopleJdbiResource {

	private final PersonJdbiDAO peopleDAO;

	public PeopleJdbiResource(PersonJdbiDAO peopleDAO) {
		this.peopleDAO = peopleDAO;
	}

	@GET
	@UnitOfWork
	public List<Person> listPeople() {
		return peopleDAO.findAll();
	}

	@GET
	@UnitOfWork
	@Path("/{personId}")
	public Person getPerson(@PathParam("personId") LongParam personId) {
		final Person person = peopleDAO.findById(personId.get());
		if (person == null) {
			throw new NotFoundException("{status:notfound}");
		}
		return person;
	}
}
```

## Applicationクラス

DBIFactory#buildの第三引数がhealth check時の名前になります。

```
	@Override
	public void run(HelloWorldConfiguration configuration,
			Environment environment) throws ClassNotFoundException {
		final PersonDAO dao = new PersonDAO(hibernateBundle.getSessionFac
       ・・・
		final DBIFactory factory = new DBIFactory();
		final DBI jdbi = factory.build(environment,
				configuration.getDataSourceFactory(), "jdbi");
		PersonJdbiDAO jdbiDao = jdbi.onDemand(PersonJdbiDAO.class);
		environment.jersey().register(new PeopleJdbiResource(jdbiDao));
       ・・・
    }
```

## 動作確認

hibernateと同じ結果が返ってくることが確認できます。

```
$ curl http://localhost:8080/people/jndi
[{"id":1,"fullName":"Coda Hale","jobTitle":"Chief Wizard"},{"id":2,"fullName":"ko2ic","jobTitle":"hoge"}]

$ curl http://localhost:8080/people/jndi/1
{"id":1,"fullName":"Coda Hale","jobTitle":"Chief Wizard"}
```

## Health Check

jdbiの場合も、bundleを登録する処理の内部でHealth Checkの登録も行われています。(名前はDBIクラスを作成時にユーザが任意の値をつけます)
なので、環境設定で```validationQuery```に記述しておくだけで、Health Checkが行われます。

```
$ curl http://localhost:8081/healthcheck
{"deadlocks":{"healthy":true},"hibernate":{"healthy":true},"jdbi":{"healthy":true}}
```

## トランザクション

以下の記事を参照してください。

* [Dropwizard with jdbiでのトランザクション](http://qiita.com/ko2ic/items/526ae7b99d1781bb1a05)

# まとめ

3回に渡ってDropwizardの基本と思われることを勉強がてら、書いてみました。
Dropwizardについて興味を持たれた方は、僕の記事よりも[公式サイト](https://dropwizard.github.io/dropwizard/index.html)を読んだ方がすぐに理解できると思います。

自分の感触では、簡単なRESTアプリであれば、簡単に作れそうだなと思いました。
逆に大規模なアプリや画面のあるアプリの場合に、どうなるかは、まだ感触すら掴めていません。
（Spring Bootなら、どうなのかも試したいです。）

最後に「いますぐ採用すべき・・・」と書きましたが、自分は実際に仕事で使えているわけではありません。
「いますぐプロジェクトで採用してほしい」し、「もっと有名になって広まれば実際のプロジェクトでも利用できるかも」と思って記述しました。

面白いフレームワークだと思うので、ぜひ試してみてください。