出会い
2012年の秋頃にとある炎上PJにてHiberanateに出会った。
MyBatisで開発したあとだったので、SQLが自動発行される仕組みに感心した覚えがある。
その後も2014年頃にHibernateを使ったPJに行ったことがあって、そのPJも炎上していた。
どちらも炎上の原因は、Hibernateではなかったと思うし、たまたま「Spring × Hibernate」の組み合わせがSIの開発界隈で当時の流行りだっただけのように思う。
JPAに基づくO/Rマッパーだと、その後、Spring Data JPAを使ったPJを2015年にやっている。この時は、「SpringBoot × Spring Data JPA」の組み合わせで開発した。
JPAとは
JPA (Java Persistence API)は、O/Rマッピング・アーキテクチャのJava用フレームワークである。Javaオブジェクトをデータベースに格納したり、データベースのデータをJavaオブジェクトに変換する処理を自動化したりするため、JDBCを直接使用するのに比べ、簡素なプログラムコードでデータベース処理を実現できる。
JPA (Java Persistence API)
JavaのクラスにSQLを書くJDBCの時代があって、SQLを外部ファイルに定義するMyBatisの時代があって、もはやSQLは書かなくなった。DBのテーブルとのマッピングは、XMLにゴリゴリ書く必要もなく、アノテーションで済んでしまう。
JPAは仕様にすぎず、機能するには実装が必要で、SpringにおけるHibernate、SpringBootにおけるSpring Data JPAがそれに当たる。
振り返ってみて
(オブジェクト・リレーショナル)インピーダンスミスマッチを解消するための仕組みとしては、根本的な思想としては素晴らしいものだと思う。Javaで操作するオブジェクトの世界とSQLで操作するDBのデータの世界は、確かに乖離がある。
ただ、Hibernateはどのようにして私のキャリアを破滅寸前にしたかの例に漏れず、Hibernateに関わったPJは、パフォーマンス問題に悩まされていた。そして、ネィティブクエリで書き直して、インデックスを張ったりしてチューニングする。
Spring Data JPAでやった案件はそういった現実を知っているメンバーでやったので、発行されるSQLが複雑になりそうな検索とかは、最初からネィティブクエリで書いていった。
メリットとしては、JPAの特徴でもある
Javaオブジェクトとデータベースのテーブル間のマッピング情報を、アノテーションを用いて定義する。これにより、エンティティをPOJO (Plain Old Java Object)で実装できたり、複雑な定義ファイルを作成する必要が無くなるなど、開発容易性を確保することができる。
だと実際、PJで触った上でも思う。
@~のアノテーションでサクサク定義しておけば、DBのテーブルとマッピングされるのは、開発の生産性が高い。簡単なマスターメンテナンス機能だったら、実装らしい実装をしなくてもCRUDの処理が成り立つ。
SpringDataJPAだったら、インタフェースのメソッド名に応じて発行するSQLをコントロールできるので、実装クラスを用意する必要さえなかったりする。圧倒的に記述量は少なくて済む。
デメリットは、使いこなすのに要求されるスキルがJDBCやMyBatisに比べて高いことのように思う。Entityのライフサイクル管理だったり、そのキャッシュ機構への理解や実際にSQLがDBに発行されるタイミングの把握、ネイティブクエリの使い所を適切に判断する能力が要求される。
開発終盤で本番データ相当で処理させてみたら、使うに耐えないパフォーマンスしか出ない。flushのタイミングがおかしくて意図しないデータ更新が起きてしまう。実際にPJ内で遭遇した出来事だけど、それは仕組みの問題というよりは、使いこなせない人間の問題なんだろうと今、振り返って思う。
.NETに似たような仕組みでEntity Frameworkがあって、意図しないデータ更新に悩まされた末、Dapperメインの実装で進められているシステムに出会ったが、どこも似たような道を通るんだなと感じる。
サンプルで実装してみる。
以下の記事を参考にさせていただきました。
- Spring Tool Suite インストールと日本語化 for Windows
- Spring Boot環境でのSpring Data JPA 事始めメモ
- SpringBoot + Spring JPAでデータベースに接続する
- 【Spring Data Jpa のやり方】
Entityを定義して、
package sample.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="worker")
public class Worker {
@Id
@Column(name = "id")
private String id;
@Column(name = "name")
private String name;
@Column(name = "age")
private int age;
@Column(name = "department")
private String department;
public Worker () {
}
public Worker(String id, String name, int age, String department) {
this.id = id;
this.name = name;
this.age = age;
this.department = department;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getDepartment() {
return department;
}
public void setDepartment(String department) {
this.department = department;
}
}
Repositoryを定義する。
package sample.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import sample.model.Worker;
@Repository
public interface SampleJpa extends JpaRepository<Worker, String> {
}
呼び出すServiceを用意、
package sample.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import sample.model.Worker;
import sample.repository.SampleJpa;
@Service
@EnableTransactionManagement
public class SampleService {
@Autowired
SampleJpa sampleJpa;
public void Execute() {
//初期登録
sampleJpa.saveAndFlush(new Worker("0001", "k.jarrett", 73, "music"));
//検索
List<Worker> workerList = sampleJpa.findAll();
//更新
for (Worker worker : workerList){
System.out.println(
String.format(
"id:%s name:%s age:%d department:%s",
worker.getId(), worker.getName(), worker.getAge(), worker.getDepartment()
)
);
worker.setAge(worker.getAge() + 1);
sampleJpa.save(worker);
}
sampleJpa.flush();
//検索
workerList = sampleJpa.findAll();
//削除
for (Worker worker : workerList){
System.out.println(
String.format(
"id:%s name:%s age:%d department:%s",
worker.getId(), worker.getName(), worker.getAge(), worker.getDepartment()
)
);
sampleJpa.delete(worker);
}
sampleJpa.flush();
//検索
workerList = sampleJpa.findAll();
if(workerList.size() == 0) {
System.out.println("count = 0");
}
}
}
起動時のRunnerは、こんな感じです。
package sample;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import sample.service.SampleService;
@SpringBootApplication
@EnableJpaRepositories("sample.repository")
@EntityScan("sample.model")
@ComponentScan(basePackages = {"sample.service"})
public class JpaSampleApplication {
public static void main(String[] args) {
SpringApplication.run(JpaSampleApplication.class, args);
}
@Bean
public CommandLineRunner demo(SampleService service) {
return (args) -> {
service.Execute();
};
}
}
サンプルプロジェクトごとGitHubにプッシュしています。
https://github.com/TsJazz27Sumin/jpaSample/tree/master/src/main/java/sample
サンプルで実装してみて感想。
主にDI周りで関係ある起動時のComponentScanやEntityScanのことをさっぱり忘れていて、Can't Autowire @Repository annotated interface in Spring Bootに助けられました。
あとは、Mavenの設定とか初期設定も「そういえば必要だった!」という感じで、設定周りで1-2時間ぐらいかかりました。でも、設定終わるとアノテーションでサクサク設定して、インターフェース用意するだけでSQLも書いてないし、シンプルな処理だとあっと言う間ですね。
経験上、実際のPJだと、ネイティブクエリ書く実装クラスを必要に応じて用意すると思いますが、それも検索条件が多かったり、JOINが多くてチューニングが必要な検索処理ぐらいです。
検索クエリもシンプルなものならメソッド名に応じて用意されるし、ここらへんSpring Data JPAの使いこなし方次第で、かなり生産性高くシステム開発できそうです。
あとは、親子関係のEntityがあるとついて回るfetch問題は、業務システム作る上では避けて通れない課題かなと思っています。ここらへんは、個々のプロジェクトでどの時点でどのエンティティが必要なのかによって、fetch typeを変えたり、ネイティブクエリで実装したり、整理が必要だなと思っています。