spring.svg

【Spring Boot】Spring JDBC

SpringBoot

パッケージの読み込み

Spring JDBC を使用するために、以下を追加します。

Gradle
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'

また使用する DB に対応したドライバーを追加してください。

接続設定

application.yml(.properties)に、データベースの接続設定をします。 以下は MySQL の設定例です。

YML
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/testdb
    username: hoge
    password: fugafuga
    driver-class-name: com.mysql.cj.jdbc.Driver

JdbcTemplate

宣言

データベース操作用に DAO クラスを作成するとします。

Spring JDBC では、JdbcTemplateクラスを使用してデータベースを操作します。 DAO クラスでは以下のようにJdbcTemplateを宣言します。

@Repository
public class SampleDataDao {
  @Autowired
  private JdbcTemplate template;
}

SQL の実行

まずは SELECT の例を見てみます。

public List<SampleData> findAll() {
  //SQL定義
  String sql = "select * from sample_data";
  //SQL実行
  SqlRowSet rs = template.queryForRowSet(sql);
  //結果反映
  List<SampleData> list = new ArrayList<>();
  while(rs.next()) {
    list.add(new SampleData(rs.getString("id"),
                            rs.getString("first_name"),
                            rs.getString("last_name"),
                            rs.getInt("age"));)
  }
  return list;
}

SQL の実行はqueryForRowSet()によって行い、SqlRowSetとして結果を受け取ります。

SqlRowSetはイテレーターとなっており、next()によって次のレコードを参照します。 次のレコードが参照できない場合、つまり最終レコードの場合に実行すると戻り値としてfalseを返します。

これを利用してループ処理を行い、各レコードの処理を行います。

以下のように決められた 1 データを取得する場合は、queryForObject()を使用します。 intlongには、queryForInt()queryForLong()があります。

public int getCount() {
  //SQL定義
  String sql = "select count(*) from sample_data";
  //SQL実行
  int count = template.queryForObject(sql, Integer.class);
  return count;
}

INSERT, UPDATE, DELETE は、update()を使用します。

public void update() {
  //SQL定義
  String sql = "update sample_data set age = 21 where id = '000001'";
  //SQL実行
  template.update(sql);
}

パラメーターの指定

JDBC では、?を使用することでプレースホルダーとしてパラメーターを設定します。

public SampleData findById(String id) {
  //SQL定義
  String sql = "select * from sample_data where id = ?";
  //SQL実行
  SqlRowSet rs = template.queryForRowSet(sql, id);
  //結果反映
  return new SampleData(rs.getString("id"), rs.getString("first_name"), rs.getString("last_name"), rs.getInt("age"));
}

パラメーターは、queryForRowSet()update()など実行メソッドの引数として設定します。

パラメーターが複数存在する場合は、対応する?の順に引数を増やします。

public void create(SampleData sampleData) {
  //SQL定義
  String sql = "insert into sample_data values (?, ?, ?, ?)";
  //SQL実行
  //プレースホルダーの順に引数を設定する
  template.update(sql, sampleData.getId(), sampleData.getFirstName(), sampleData,getLastName(), sampleData.getAge());
}

NamedParameterJdbcTemplate

宣言

宣言をJdbcTemplateからNamedParameterJdbcTemplateに変更します。

@Repository
public class SampleDataDao {
  @Autowired
  private NamedParameterJdbcTemplate template;
}

SQL 定義

NamedParameterJdbcTemplateでは、SQL 文の中のパラメーターを?ではなく、:paramNameとパラメーター名を使用します。

public void create(SampleData sampleData) {
  //SQL定義
  String sql = "insert into sample_data values (:id, :firstName, :lastName, :age)";
  //...
}

Map によるパラメーター指定

SQL 文中のパラメーターへの紐付けは、キーをパラメーター名とするMapインスタンスを作成して行います。

あとは作成したMapインスタンスを実行用のメソッドの引数として指定するだけです。

public void create(SampleData sampleData) {
  //SQL定義
  String sql = "insert into sample_data values (:id, :firstName, :lastName, :age)";
  //パラメーター定義
  Map<String, Object> params = new HashMap<>();
  params.put("id", sampleData.getString());
  params.put("firstName", sampleData.getString());
  params.put("lastName", sampleData.getLastName());
  params.put("age", sampleData.getAge());
  //SQL実行
  template.update(sql, params);
}

クラスによるパラメーター指定

今回のsampleDataのように、既にパラメーターの情報を持つインスタンスがある場合、Mapを生成する必要はありません。 以下のようにBeanPropertySqlParameterSourceを使用して対応するインスタンスを直接指定することができます。

public void create(SampleData sampleData) {
  //SQL定義
  String sql = "insert into sample_data values (:id, :firstName, :lastName, :age)";
  //パラメーター定義
  SqlParameterSource params = new BeanPropertySqlParameterSource(sampleData);
  //SQL実行
  template.update(sql, params);
}

これにより、パラメーター名と同名のフィールドから値が設定されます。

パラメーターがない場合の指定

NamedParameterJdbcTemplateでは、必ずパラメーター用のインスタンスを設定する必要があります。 パラメーターが必要ない場合は、EmptySqlParameterSource.INSTANCEを設定します。

public List<SampleData> findAll() {
  //SQL定義
  String sql = "select * from sample_data";
  //SQL実行
  SqlRowSet rs = template.queryForRowSet(sql, EmptySqlParameterSource.INSTANCE);
  //結果反映(省略)
}

トランザクション

@Transactinal

Spring JDBC では、メソッドに@Transactionalを付与することで、トランザクションの制御を自動で行ってくれます。

@Service
public class SampleDataService {
  @Autowired
  private SampleDao dao;

  @Transactional
  public void create(SampleData sampleData) {
    //before process
    dao.create(sampleDao);
    //after process
  }
}

メソッドの処理開始時にトランザクションも開始され、終了時にコミットされます。

非検査例外(RuntimeExceptionおよびサブクラス)が発生した場合はロールバックされます。

検査例外が発生した場合はコミットされてしまいます。 検査例外でもロールバックしたい場合は以下のようにrollbackForプロパティを設定します。

@Transactional(rollbackFor = Exception.class)
public void create(SampleData sampleData) {
  //...
}

@Transactionalには以下のプロパティがあります。

プロパティデフォルト説明
propagationPropagation.REQUIREDトランザクションの伝播タイプ(詳細は後述)
isolationIsolation.DEFAULTトランザクションの分離レベル(詳細は後述)
timeoutシステムのデフォルト値トランザクションのタイムアウト(秒)
readOnlyfalseトランザクションが読み取り専用であることを設定
rollbackForロールバックの条件である例外クラスを設定
noRollbackFor例外の中でロールバックを行わないクラスを設定

伝播タイプ

トランザクションの伝搬タイプとは、現在のトランザクションの状態を基に、どのようなトランザクションを使用するかを表すものです。

ServiceA.java
@Transactional(propagation = Propagation.XXXXX)
public void methodA() {
  //...
}
ServiceB.java
@Transactional
public void methodB() {
  //トランザクションありで実行される
  serviceB.methodA();
}

public void methodC() {
  //トランザクションなしで実行される
  serviceB.methodA()
}

プロパティ値として列挙型であるPropagationの値を指定します。

Propagationトランザクションありトランザクションなし補足
REQUIRED引き継ぐ新規生成デフォルト値
SUPPORTS引き継ぐ非トランザクション
MANDATORY引き継ぐ例外スロー
REQUIRES_NEW新規生成新規生成既存のトランザクションは一時停止される
NOT_SUPPORTED非トランザクション非トランザクション既存のトランザクションは一時停止される
NEVER例外スロー非トランザクション
NESTEDネスト新規生成

分離レベル

トランザクションには、ダーディリード(Dirty Read)、ノンリピータブルリード(Non-repeatable Read)、 ファントムリード(Phantom Read)という問題があります。

それぞれの詳細については割愛しますが、どの問題を許容するかを分離レベルとして設定します。

分離レベルは、列挙型のIsolationの値によって設定します。 以下表の「●」は、問題が発生することを表します。

IsolationDirty ReadNon-repeatable ReadPhantom Read
READ_UNCOMMITED
READ_COMMITED-
REPEATABLE_READ--
SERIALIZABLE---

PlatformTransactionManager

@Transactionalを使用せず、自身でトランザクション処理のタイミングを決めたい場合は、PlatformTransactionManagerを使用します。

@Service
public SampleDataService {

  @Autowired
  private PlatformTransactionManager trnManager;

  @Autowired
  private SampleDataDao dao

  public void create(SampleData sampleData) {
    //トランザクション設定
    DefaultTransactionDefinition trnDefinition = new DefaultTransactionDefinition();
    TransactionStatus trnStatus = trnManager.getTransaction(trnDefinition);
    try {
      dao.create();
      //コミット
      trnManager.commit(trnStatus);
    } catch (RuntimeException e) {
      //ロールバック
      trnManager.rollback(trnStatus);
    }
  }
}