はじめに
MybatisとHibernateの実装(基本的なCRUD処理のみ)を比較したかったのでちょっと試しました。
ベースとなるFWにSpring bootを使用しています。
環境
Spring starter projectで必要な依存性を追加しています。
STSのGUIでチェックするだけなのでめっちゃ簡単ですね。
- IDE:STS3.7.3
- ビルド:maven
- Javaバージョン:Java8
- ベースFW:spring-boot2.1.4
- DB:Postgres9.3.3
※ちなみに私の環境だとデフォルトでspring-bootは2.1.6が選択されますが、どうやらmavenとの問題でPOMで「Unknown Error」が発生するので2.1.4にダウングレードしています。
https://bugs.eclipse.org/bugs/show_bug.cgi?id=547340#c9
DB
Postgresを使用しました。
以下、DB作成からテストデータINSERTまで。
postgres=# create database test01; ←test01データベースを作成
postgres=# \l
データベース一覧
名前 | 所有者 | エンコーディング | 照合順序 | Ctype(変換演算子) | アクセス権
-------------+----------+------------------+--------------------+--------------------+-----------------------
postgres | postgres | UTF8 | Japanese_Japan.932 | Japanese_Japan.932 |
test01 | postgres | UTF8 | Japanese_Japan.932 | Japanese_Japan.932 |
(以下省略)
postgres=# \c test01 ←test01データベースを選択
データベース "test01" にユーザ"postgres"として接続しました。
test01=# create table emp (id integer,department character varying(10), name character varying(30));
CREATE TABLE
test01=# \d emp
テーブル "public.emp"
列 | 型 | 修飾語
------------+-----------------------+--------
id | integer |
department | character varying(10) |
name | character varying(30) |
test01=# insert into emp values(101,'人事部','ニコ');
INSERT 0 1
(~省略~)
test01=# select * from emp;
id | department | name
-----+------------+------------
101 | 人事部 | ニコ
102 | 開発部 | ダンテ
103 | 開発部 | ネロ
104 | 総務部 | トリッシュ
105 | 開発部 | バージル
(5 行)
test01=# ALTER TABLE emp ADD PRIMARY KEY(id);
ALTER TABLE
test01=#
【実装】Mybatisの場合
POM
dependenciesのみ抜粋。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
プロパティ
Mybatisの場合は接続情報に関するpropertiesのみでした。
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/test01
spring.datasource.username=postgres
spring.datasource.password=postgres
#SQLログを出す
logging.level.jp.demo.mapper.EmpMapper=DEBUG
SpringBootApplication
package jp.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class MybatisApplication {
public static void main(String[] args) {
try(ConfigurableApplicationContext ctx = SpringApplication.run(MybatisApplication.class, args)){
DemoService app = ctx.getBean(DemoService.class);
app.execute(args);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Service
package jp.demo;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import jp.demo.domain.Emp;
import jp.demo.mapper.EmpMapper;
@Service
public class DemoService {
@Autowired
private EmpMapper mapper;
public void execute(String[] args) {
// select all
System.out.println("### select all ###");
List<Emp> list = mapper.selectAll();
list.forEach(System.out::println);
// insert
System.out.println("### insert ###");
Emp insEmp = new Emp(106, "人事部", "レディ");
mapper.insert(insEmp);
System.out.println(mapper.select(106));
// update
System.out.println("### update ###");
Emp updEmp = new Emp(106, "経理部", null);
mapper.update(updEmp);
System.out.println(mapper.select(106));
// delete
System.out.println("### delete ###");
mapper.delete(106);
System.out.println(mapper.select(106));
}
}
Mapper
後述のマッパーXMLに定義したSQLのidと紐づいています。
ただし、単純なSQLならXMLを用意せずにアノテーションに記入することもできます。
この例ではselectAllの際はSQLをアノテーションで定義しました。
package jp.demo.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import jp.demo.domain.Emp;
@Mapper
public interface EmpMapper {
@Select("select id,department,name from emp")
List<Emp> selectAll();
Emp select(int id);
void insert(Emp emp);
void update(Emp emp);
void delete(int id);
}
Entity
以下のアノテーションはすべてLombokなのでMybatisの場合はEntityクラスにはアノテーションは不要です。
package jp.demo.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {
private int id;
private String department;
private String name;
}
Mapper.xml
マッパーinterfaceのメソッド名とidで紐づいています。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="jp.demo.mapper.EmpMapper">
<resultMap id="empResultMap" type="jp.demo.domain.Emp">
<result property="id" column="id" />
<result property="department" column="department" />
<result property="name" column="name" />
</resultMap>
<select id="select" resultMap="empResultMap">
select id,department,name from emp
where id=#{id};
</select>
<update id="update" parameterType="jp.demo.domain.Emp">
update emp
set department=#{department}
where id=#{id};
</update>
<insert id="insert" parameterType="jp.demo.domain.Emp">
insert into emp
values (#{id},#{department},#{name});
</insert>
<delete id="delete">
delete from emp where id=#{id};
</delete>
</mapper>
実行結果
propertiesでSQLログ出すよう設定したのでSQLとパラメータも併せて出力されています。
### select all ###
2019-08-08 02:22:04.214 INFO 12776 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2019-08-08 02:22:04.539 INFO 12776 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2019-08-08 02:22:04.553 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.selectAll : ==> Preparing: select id,department,name from emp
2019-08-08 02:22:04.585 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.selectAll : ==> Parameters:
2019-08-08 02:22:04.628 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.selectAll : <== Total: 5
Emp(id=101, department=人事部, name=ニコ)
Emp(id=102, department=開発部, name=ダンテ)
Emp(id=103, department=開発部, name=ネロ)
Emp(id=104, department=総務部, name=トリッシュ)
Emp(id=105, department=開発部, name=バージル)
### insert ###
2019-08-08 02:22:04.645 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.insert : ==> Preparing: insert into emp values (?,?,?);
2019-08-08 02:22:04.648 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.insert : ==> Parameters: 106(Integer), 人事部(String), レディ(String)
2019-08-08 02:22:04.686 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.insert : <== Updates: 1
2019-08-08 02:22:04.687 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.select : ==> Preparing: select id,department,name from emp where id=?;
2019-08-08 02:22:04.688 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.select : ==> Parameters: 106(Integer)
2019-08-08 02:22:04.690 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.select : <== Total: 1
Emp(id=106, department=人事部, name=レディ)
### update ###
2019-08-08 02:22:04.691 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.update : ==> Preparing: update emp set department=? where id=?;
2019-08-08 02:22:04.691 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.update : ==> Parameters: 経理部(String), 106(Integer)
2019-08-08 02:22:04.701 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.update : <== Updates: 1
2019-08-08 02:22:04.702 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.select : ==> Preparing: select id,department,name from emp where id=?;
2019-08-08 02:22:04.702 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.select : ==> Parameters: 106(Integer)
2019-08-08 02:22:04.704 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.select : <== Total: 1
Emp(id=106, department=経理部, name=レディ)
### delete ###
2019-08-08 02:22:04.705 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.delete : ==> Preparing: delete from emp where id=?;
2019-08-08 02:22:04.706 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.delete : ==> Parameters: 106(Integer)
2019-08-08 02:22:04.708 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.delete : <== Updates: 1
2019-08-08 02:22:04.708 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.select : ==> Preparing: select id,department,name from emp where id=?;
2019-08-08 02:22:04.709 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.select : ==> Parameters: 106(Integer)
2019-08-08 02:22:04.710 DEBUG 12776 --- [ main] jp.demo.mapper.EmpMapper.select : <== Total: 0
null
【実装】Hibernateの場合
POM
dependenciesのみ抜粋。
上記のMybatisとの違いはdependency「mybatis-spring-boot-starter」のみです。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
プロパティ
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/test01
spring.datasource.username=postgres
spring.datasource.password=postgres
#SQLログを出す
logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=trace
logging.level.org.hibernate.EnumType=trace
Hibernateの場合は接続情報に加えて不要なログを出さないようにするhibernate.propertiesも追加した方がよさそうです。
hibernate.jdbc.lob.non_contextual_creation=true
上記のhibernate.propertiesの設定をしない場合、Spring boot app起動時に以下のエラーがログに出ます。(実行はできます)
java.lang.reflect.InvocationTargetException: null
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_25]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_25]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_25]
at java.lang.reflect.Method.invoke(Method.java:483) ~[na:1.8.0_25]
~~省略~~
at jp.demo.HibernateApplication.main(HibernateApplication.java:11) [classes/:na]
Caused by: java.sql.SQLFeatureNotSupportedException: org.postgresql.jdbc.PgConnection.createClob() メソッドはまだ実装されていません。
at org.postgresql.Driver.notImplemented(Driver.java:688) ~[postgresql-42.2.5.jar:42.2.5]
at org.postgresql.jdbc.PgConnection.createClob(PgConnection.java:1269) ~[postgresql-42.2.5.jar:42.2.5]
... 44 common frames omitted
SpringBootApplication
package jp.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
@SpringBootApplication
public class HibernateApplication {
public static void main(String[] args) {
try(ConfigurableApplicationContext ctx = SpringApplication.run(HibernateApplication.class, args)){
DemoService app = ctx.getBean(DemoService.class);
app.execute(args);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Service
package jp.demo;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import jp.demo.domain.Emp;
import jp.demo.domain.EmpRepository;
@Service
public class DemoService {
@Autowired
EmpRepository repository;
public void execute(String[] args) {
// select all
System.out.println("### select all ###");
List<Emp> list = repository.findAll();
list.forEach(System.out::println);
// insert
System.out.println("### insert ###");
Emp insEmp = new Emp(106, "人事部", "レディ");
repository.save(insEmp);
System.out.println(repository.findById(106).get());
// update
System.out.println("### update ###");
Emp updEmp = repository.findById(106).get();
repository.save(updEmp);
System.out.println(repository.findById(106).get());
// delete
System.out.println("### delete ###");
repository.deleteById(106);
System.out.println(repository.findById(106).isPresent());
}
}
INSERTとUPDATEはCrudRepository.save()にまとめられているようです。
どう切り分けているかというと、更新前に後述のエンティティクラスでIDアノテーションが付与されている属性でまずデータ抽出を行い、存在する場合はUPDATE、存在しない場合はINSERTとなるようです。
なのでCrudRepository.save()を使用するとSQLを自作しなくていい代わりに以下の特典があります。
- 属性すべてでUpdateかけようとするのでいったん現状のレコードをSELECTしてから必要カラムを書き換えたエンティティをsaveメソッドに渡す必要がある。
- INSERTとUPDATEを切り分けるために自動で事前にSELECTが実行されてしまう。
1.はどうしようもないかもしれないけど、2.は設定とかでSELECT発行しないように制御できそう?というかできてほしい。。
Repository
メソッド定義不要。SQLも不要なのですごいですね。ただし、上記Serviceで書いたようにJpaRepositoryを使用するだけだと自由度がかなり下がるっぽいですね。
package jp.demo.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface EmpRepository extends JpaRepository<Emp, Integer> {
}
Entity
Mybatisの場合はアノテーション不要でしたがHibernateはJPAを使うので
JPAアノテーションがいろいろ付与されています。
package jp.demo.domain;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name="emp")
public class Emp {
@Id
@Column(name="id")
private int id;
@Column(name="department")
private String department;
@Column(name="name")
private String name;
}
実行結果
こっちもログ出てますね。
Serviceで書いたように、INSERTの場合はINSERTかUPDATEか判断用の事前SELECT、
UPDATEの場合は判断用SELECTに加えて更新カラム以外のカラム取得のためのSELECTが発行されています。
### select all ###
2019-08-08 01:13:02.862 INFO 10828 --- [ main] o.h.h.i.QueryTranslatorFactoryInitiator : HHH000397: Using ASTQueryTranslatorFactory
2019-08-08 01:13:03.146 DEBUG 10828 --- [ main] org.hibernate.SQL : select emp0_.id as id1_0_, emp0_.department as departme2_0_, emp0_.name as name3_0_ from emp emp0_
Emp(id=101, department=人事部, name=ニコ)
Emp(id=102, department=開発部, name=ダンテ)
Emp(id=103, department=開発部, name=ネロ)
Emp(id=104, department=総務部, name=トリッシュ)
Emp(id=105, department=開発部, name=バージル)
### insert ###
2019-08-08 01:13:03.206 DEBUG 10828 --- [ main] org.hibernate.SQL : select emp0_.id as id1_0_0_, emp0_.department as departme2_0_0_, emp0_.name as name3_0_0_ from emp emp0_ where emp0_.id=?
2019-08-08 01:13:03.225 TRACE 10828 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [INTEGER] - [106]
2019-08-08 01:13:03.285 DEBUG 10828 --- [ main] org.hibernate.SQL : insert into emp (department, name, id) values (?, ?, ?)
2019-08-08 01:13:03.286 TRACE 10828 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [人事部]
2019-08-08 01:13:03.286 TRACE 10828 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [VARCHAR] - [レディ]
2019-08-08 01:13:03.287 TRACE 10828 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [INTEGER] - [106]
2019-08-08 01:13:03.314 DEBUG 10828 --- [ main] org.hibernate.SQL : select emp0_.id as id1_0_0_, emp0_.department as departme2_0_0_, emp0_.name as name3_0_0_ from emp emp0_ where emp0_.id=?
2019-08-08 01:13:03.315 TRACE 10828 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [INTEGER] - [106]
Emp(id=106, department=人事部, name=レディ)
### update ###
2019-08-08 01:13:03.338 DEBUG 10828 --- [ main] org.hibernate.SQL : select emp0_.id as id1_0_0_, emp0_.department as departme2_0_0_, emp0_.name as name3_0_0_ from emp emp0_ where emp0_.id=?
2019-08-08 01:13:03.339 TRACE 10828 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [INTEGER] - [106]
2019-08-08 01:13:03.343 DEBUG 10828 --- [ main] org.hibernate.SQL : select emp0_.id as id1_0_0_, emp0_.department as departme2_0_0_, emp0_.name as name3_0_0_ from emp emp0_ where emp0_.id=?
2019-08-08 01:13:03.344 TRACE 10828 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [INTEGER] - [106]
2019-08-08 01:13:03.348 DEBUG 10828 --- [ main] org.hibernate.SQL : update emp set department=?, name=? where id=?
2019-08-08 01:13:03.348 TRACE 10828 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [経理部]
2019-08-08 01:13:03.349 TRACE 10828 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [VARCHAR] - [レディ]
2019-08-08 01:13:03.349 TRACE 10828 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [INTEGER] - [106]
2019-08-08 01:13:03.354 DEBUG 10828 --- [ main] org.hibernate.SQL : select emp0_.id as id1_0_0_, emp0_.department as departme2_0_0_, emp0_.name as name3_0_0_ from emp emp0_ where emp0_.id=?
2019-08-08 01:13:03.354 TRACE 10828 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [INTEGER] - [106]
Emp(id=106, department=経理部, name=レディ)
### delete ###
2019-08-08 01:13:03.359 DEBUG 10828 --- [ main] org.hibernate.SQL : select emp0_.id as id1_0_0_, emp0_.department as departme2_0_0_, emp0_.name as name3_0_0_ from emp emp0_ where emp0_.id=?
2019-08-08 01:13:03.359 TRACE 10828 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [INTEGER] - [106]
2019-08-08 01:13:03.371 DEBUG 10828 --- [ main] org.hibernate.SQL : delete from emp where id=?
2019-08-08 01:13:03.371 TRACE 10828 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [INTEGER] - [106]
2019-08-08 01:13:03.376 DEBUG 10828 --- [ main] org.hibernate.SQL : select emp0_.id as id1_0_0_, emp0_.department as departme2_0_0_, emp0_.name as name3_0_0_ from emp emp0_ where emp0_.id=?
2019-08-08 01:13:03.376 TRACE 10828 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [INTEGER] - [106]
false
今回のまとめ
ManyToOne結合とか用意するとHibernateのすごさ(おそろしさ)がわかるのですが。。
ということで次回は結合ありのケースで試してみます。
あとHQLも試さないと。。
ちなみに今の時点で思ったことです↓
Mybatis
メリット
- SQLを実装する必要があるので何を抽出・更新しているか明白
- SQLの勉強にもなる
- 余計なクエリが投げられない
デメリット
- SQLを知らない新人さんとかには重労働
- SQLが必要になる分、簡単なCRUD処理でも実装量が多くなる
Hibernate
メリット
- SQLを書く必要がない
- DBを意識せずにオブジェクトの感覚で実装できる
- HQLというテーブルではなくオブジェクトベースで記載する独自のデータアクセス言語がある
デメリット
- 設定が多くて複雑
- SQLの勉強にならない
- 実装量が少ない代わりに更新前にSELECT文が発行される
- Mybatisに比べてかなりブラックボックス化されているので不具合がどこで発生しているのか発見しづらい