Spring BootでのDBアクセス方法(JDBC、JPA、MyBatis)について

Spring BootでのDBアクセス方法として、下記の3パターンを試してみました。

  • JDBC(spring-boot-starter-jdbc)
  • JPA(spring-boot-starter-data-jpa)
  • MyBatis(mybatis-spring-boot-starter)

なお、それぞれの全体のコードは、下記に配置してあります。

テーブル構成

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も試してみようと考えています。