Spring BootでのDBアクセス方法として、下記の3パターンを試してみました。
- JDBC(spring-boot-starter-jdbc)
- JPA(spring-boot-starter-data-jpa)
- MyBatis(mybatis-spring-boot-starter)
なお、それぞれの全体のコードは、下記に配置してあります。
- https://github.com/onozaty/spring-boot-sandbox/tree/master/spring-boot-rest-jdbc
- https://github.com/onozaty/spring-boot-sandbox/tree/master/spring-boot-rest-jpa
- https://github.com/onozaty/spring-boot-sandbox/tree/master/spring-boot-rest-mybatis
- https://github.com/onozaty/spring-boot-sandbox/tree/master/spring-boot-rest-mybatis-kotlin
テーブル構成
DBはH2を使います。DDLは下記の通りです。
CREATE TABLE customers ( id INT PRIMARY KEY AUTO_INCREMENT, first_name VARCHAR(30) NOT NULL, last_name VARCHAR(30) NOT NULL, address VARCHAR(100) );
実装する処理
基本的なCRUDにプラスして、first_name
カラムを部分一致で検索するものを実装します。
Entityは下記のように定義します。(JPAだけ追加でアノテーションが必要なので後述します)
package com.example.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class Customer { private Integer id; private String firstName; private String lastName; private String address; }
JDBC
NamedParameterJdbcTemplate
を利用します。
SQL自体は書きますが、Entityとのマッピングが簡単になります。
package com.example.repository; import java.util.HashMap; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; import com.example.domain.Customer; @Repository public class CustomerRepository { @Autowired private NamedParameterJdbcTemplate jdbcTemplate; public List<Customer> findAll() { return jdbcTemplate.query( "SELECT * FROM customers ORDER BY id", new BeanPropertyRowMapper<Customer>(Customer.class)); } public Customer findOne(Integer id) { SqlParameterSource param = new MapSqlParameterSource().addValue("id", id); try { return jdbcTemplate.queryForObject( "SELECT * FROM customers WHERE id = :id", param, new BeanPropertyRowMapper<Customer>(Customer.class)); } catch (EmptyResultDataAccessException e) { return null; } } public Customer save(Customer customer) { SqlParameterSource param = new BeanPropertySqlParameterSource(customer); if (customer.getId() == null) { SimpleJdbcInsert insert = new SimpleJdbcInsert((JdbcTemplate) jdbcTemplate.getJdbcOperations()) .withTableName("customers") .usingGeneratedKeyColumns("id"); Number key = insert.executeAndReturnKey(param); customer.setId(key.intValue()); } else { jdbcTemplate.update( "UPDATE customers SET first_name = :firstName, last_name = :lastName, address = :address WHERE id = :id", param); } return customer; } public void delete(Integer id) { SqlParameterSource param = new MapSqlParameterSource().addValue("id", id); jdbcTemplate.update( "DELETE FROM customers WHERE id = :id", param); } public List<Customer> findByFirstName(String firstName) { SqlParameterSource param = new MapSqlParameterSource().addValue("firstName", "%" + firstName + "%"); return jdbcTemplate.query( "SELECT * FROM customers WHERE first_name LIKE :firstName ORDER BY id", param, new BeanPropertyRowMapper<Customer>(Customer.class)); } public void deleteAll() { jdbcTemplate.update("DELETE FROM customers", new HashMap<>()); } }
JPA
EntityにもJPA用のアノテーションが必要となります。
package com.example.domain; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Table; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Entity @Table(name = "customers") @Data @AllArgsConstructor @NoArgsConstructor public class Customer { @Id @GeneratedValue private Integer id; private String firstName; private String lastName; private String address; }
JpaRepository
を継承したinterface
を定義するだけで、基本的なCRUD操作用のメソッド(find
, findOne
, save
, delete
等)が提供されます。
また、メソッド名からクエリを作り出してくれる機能もあり、findByFirstNameContains
とすると、first_nameに対してLIKEで部分一致とするクエリを実行するメソッドとなります。
EntityManagerを使うことも無く、とても簡単に書けます。
package com.example.repository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import com.example.domain.Customer; @Repository public interface CustomerRepository extends JpaRepository<Customer, Integer> { public List<Customer> findByFirstNameContains(String firstName); }
MyBatis
インタフェースを定義し、アノテーションでSQLを指定します。
JDBCの時のようにSQLを書くことになりますが、アノテーションで済ませられるので、より完結に書けます。
package com.example.repository; import java.util.List; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.SelectKey; import org.apache.ibatis.annotations.Update; import org.springframework.stereotype.Repository; import com.example.domain.Customer; @Repository @Mapper public interface CustomerRepository { @Select("SELECT * FROM customers ORDER BY id") public List<Customer> findAll(); @Select("SELECT * FROM customers WHERE id = #{id}") public Customer findOne(@Param("id") Integer id); @Insert("INSERT INTO customers(first_name, last_name, address) VALUES(#{firstName}, #{lastName}, #{address})") @SelectKey(statement = "call identity()", keyProperty = "id", before = false, resultType = int.class) public void insert(Customer customer); @Update("UPDATE customers SET first_name = #{firstName}, last_name = #{lastName}, address = #{address} WHERE id = #{id}") public void update(Customer customer); @Delete("DELETE FROM customers WHERE id = #{id}") public void delete(@Param("id") Integer id); @Select("SELECT * FROM customers WHERE first_name LIKE '%' || #{firstName} || '%' ORDER BY id") public List<Customer> findByFirstName(@Param("firstName") String firstName); @Delete("DELETE FROM customers") public void deleteAll(); }
MyBatis(Kotlin)
Javaだとヒアドキュメントが書けないので、複数行にわたるようなSQLを書くのに不便です。
ということで、ヒアドキュメントが使えるKotlinで書いてみます。
package com.example.repository import com.example.domain.Customer import org.apache.ibatis.annotations.Delete import org.apache.ibatis.annotations.Insert import org.apache.ibatis.annotations.Mapper import org.apache.ibatis.annotations.Param import org.apache.ibatis.annotations.Select import org.apache.ibatis.annotations.SelectKey import org.apache.ibatis.annotations.Update @Mapper interface CustomerRepository { @Select("SELECT * FROM customers ORDER BY id") fun findAll(): List<Customer> @Select("SELECT * FROM customers WHERE id = #{id}") fun findOne(@Param("id") id: Int): Customer @Insert("INSERT INTO customers(first_name, last_name, address) VALUES(#{firstName}, #{lastName}, #{address})") @SelectKey(statement = arrayOf("call identity()"), keyProperty = "id", before = false, resultType = Int::class) fun insert(customer: Customer) @Update("UPDATE customers SET first_name = #{firstName}, last_name = #{lastName}, address = #{address} WHERE id = #{id}") fun update(customer: Customer) @Delete("DELETE FROM customers WHERE id = #{id}") fun delete(@Param("id") id: Int); @Select(""" SELECT * FROM customers WHERE first_name LIKE '%' || #{firstName} || '%' ORDER BY id """) fun findByFirstName(@Param("firstName") firstName: String): List<Customer> @Delete("DELETE FROM customers") fun deleteAll() }
まとめ
シンプルな操作だけならば、JPAがインタフェースを定義するだけなのでとてもらくちんです。 ただ、JPAは多機能で嵌りどころも多いイメージ(理解して使わないと危険)なので、今の自分の知識だとMyBatisがあっているかなと考えています。
MyBatisを使う場合に、ヒアドキュメントを使えるKotlinでMapper部分だけでも書こうと考えていましたが、EclipseのKotlinプラグインだと、importの補完をしてくれないので、結構面倒に感じました(IntelliJ IDEA使えれば、、)。 ここはGroovyも試してみようと考えています。