見出し画像

Spring入門(JDBC Template編)


はじめに

仕事でSpringを用いたバックエンド開発を行うことになったため、Springについての学習をゼロから開始した。
学んだ内容を概念ごとに分割して記事を作成する。
本記事の対象読者はプログラミングやアプリケーション構築の基礎知識はあるが、Springは未経験の方。

JDBCとは

JDBCとはJavaDataBaseConnectivityの略で、Javaが標準で提供するDB関連の処理を提供する標準API。
DBの種類の違いを吸収してDBへの接続を行ったり、SQLの実行、結果の処理を行うことを可能にする。
JavaからのDBアクセスには様々な仕組みが作成されているが、いずれの場合も内部的にはJava標準のJDBCを使用している。

Spring JDBCとは

Spring JDBCはSpringが提供するJDBCのラッパークラス。
JDBCの冗長な記述をより簡潔にするためのAPIを提供する。
Springでは他ライブラリとの連携の柔軟に行えるため、必ずしもSpring JDBCを活用するわけではない。

DB操作の流れ

JNDI

Java Naming and Directory Interfaceの略。
Javaアプリが外部リソースと名前を紐づけることで、リソース名により外部リソースを利用可能にするためのインターフェース。
リソース情報をハードコードすることなく外部リソースとの接続を可能にする。
リソースはアプリサーバとは別のJNDIサーバに保存され、アプリから名前を受け取って関連づけられたリソースを返却する。
DB等の外部リソースに接続する際に使用される。

DataSource

データ格納元(DBなど)との接続を作成するファクトリをJabaで表現したもの。
DataSourceオブジェクトの作成時にDB名や通信プロトコル、ユーザ、パスワードなどの必要な設定を行うことで、ドライバ情報をハードコードすることなくデータソースにアクセス可能になる。
内部ではJNDIを介してデータソースにアクセスを行っている。

※ドライバ:ハードウェアやソフトウェアを利用するための仲介役。
 システムの詳細を抽象化して接続や操作を簡易化する。
 例:JDBCドライバはJavaアプリとDBMSのデータのやり取りを簡易化する

JDBC Template

DBにSQLを発行するメソッドを提供するSpringフレームワークのクラス。
JDBC Templateオブジェクト作成の引数としてDataSourceオブジェクトが必要であり、DataSourceオブジェクトの数だけJDBC Templateオブジェクトを用意してDIコンテナに格納して使い回す。

DB関連の呼称

テーブル:データを格納する領域全体
レコード:テーブルの一行分にあたる一つのデータセット
カラム:各レコードが持つデータの種類
フィールド:レコードを形成する一つ一つの要素

レコードの取得(SELECT)

取得したいデータの種類により使用するメソッドが異なる。

  1. 1レコードの1カラム:queryForObject

  2. 複数レコードの一カラム:queryForList

  3. Map:queryForMap

  4. 1レコードのエンティティ:queryForObject

  5. 複数レコード(エンティティ):query

いずれの場合も
第一引数:SQL(パラメータ部分はJDBCの規定値である?とする)
第二引数:戻り値の型
第三以降の引数:?の具体値

エンティティ取得の型指定

エンティティを取得する際の方の指定には
new DataClassRowMapper<>(〇〇.class)を用いる 。

public Product selectById(String id) {
    return jdbcTemplate.queryForObject("SELECT * FROM t_product WHERE id=?", new DataClassRowMapper<>(Product.class), id);
}

DataClassRowMapper:レコードの値をコンストラクタで指定したクラスのオブジェクトに自動変換するクラス。
カラムのスネークケース(_での区切り)とフィールドのキャメルケース(大文字での区切り)も自動で対応づけてくれる。

レコードの更新(INSERT,UPDATE,DELETE)

レコードの更新はINSERT,UPDATE,DELETEのすべての場合に共通でJdbcTemplateクラスのupdateメソッドを使用する。
例)insert処理

jdbcTemplate.update("INSERT INTO t_order values (?, ?, ?, ?, ?, ?, ?, ?)",
        order.getId(),
        order.getOrderDateTime(),
        order.getBillingAmount(),
        order.getCustomerName(),
        order.getCustomerAddress(),
        order.getCustomerPhone(),
        order.getCustomerEmailAddress(),
        order.getPaymentMethod().toString());

第一引数:実行したいSQL
第二引数:SQLの?の具体値

戻り値は変更を加えたレコード数のため、登録に失敗して戻り値が0の場合はエラーハンドリング処理を追加する必要がある。

JDBCTemplateのBean化

JdbcTemplateクラスはオブジェクトをBeanメソッドでDIコンテナに格納しておき、同じものを使い回す。
複数のDataSourceを使用する場合は、DataSourceの種類だけJDBCTemplateのBean定義を作成する。

JOINしたデータの取得

JOINとは

複数テーブルを外部キーを基にして結合すること。
以下のようなテーブルがあるとする。

Customers テーブル

| ID         | Name         | City      |
|------------|--------------|-----------|
| 1          | Alice        | Tokyo     |
| 2          | Bob          | Osaka     |
| 3          | Carol        | Aichi     |

Orders テーブル

| ID      | CustomerID | OrderDate  |
|---------|------------|------------|
| 101     | 1          | 2023-07-15 |
| 102     | 2          | 2023-07-16 |
| 103     | 4          | 2023-07-17 |

OrderテーブルはCustomerテーブルの主キーを外部キーとして持っているため、以下のようなSQLで結合することができる。

SQL

SELECT 
  c.id AS c_id, 
  c.name AS c_name,
  c.city AS c_city,
  o.id AS o_id,
  o.orderDate AS o_order
FROM Customers c
  LEFT OUTER JOIN Orders o
  ON c.id = o.customerid;
WHERE
  c.id = '1'

ここで両テーブルではIDというカラム名が被っているため、テーブルの別名としてc,oを付与して、カラム名を「テーブルの別名」.「カラム名」の形で指定している。

※ LEFT OUTER JOINは左外部結合の処理。
 1テーブル目の全レコードが含まれるデータが返却される。
 2テーブル目に一致するカラムが存在しない場合はNULLを設定して
 返却される。
 外部結合には他にRIGHTとFULLが存在する。
 一致するカラムがある場合のみデータを取得したい場合は
 内部結合のINNER JOINを用いる。 

上記のように「テーブルの別名」.「カラム名」で値を取得するとDataClassRowMapperでEntityとのマッピングをしようとするとフィールド名とカラム名が異なるためマッピングできなくなる。

RowMapperインタフェースの実装

DataClassRowMapperはRowMapperというインタフェースの具象クラスであり、自作でRowMapperの具象クラスを作成すれば複数Entityを含むようなマッピングも可能になる。

1対1の変換

以下のようなクラスのRowMapperの実装を考える。

Customer クラス

public class Customer {
  private String id;
  private String orderId;
  private String name;
  private Order order;
  ...Getter,Setterメソッド
}

Order クラス

public class Order {
  private String id;
  private String code;
  private String Name;
  ...Getter,Setterメソッド
}

RowMapperインタフェースの実装は以下のように、RowMapperインタフェースのmapRowメソッドをOverrideする形で実装する。

static class CustomerRowMapper implements RowMapper<Customer> {
  @Override
  public Customer mapRow(ResultSet rs, int rowNum) throws SQLException {
    Customer customer = new Customer();
    customer.setId(rs.getString("c_id"));
    customer.setOrderId(rs.getString("o_order_id"));
    customer.setname(rs.getString("c_name"));
    Order order = new Order();
    order.setId(rs.getString("o_id"));
    order.setCode(rs.getString("o_code"));
    order.setName(rs.getString("o_name"));
    customer.setOrder(order);
    return customer;
  }
}

Repositoryでマッピングを利用するには以下のように記述する。

public Customer selectById(String id) {
  String sql = "..."
  return jdbcTemplate.queryForObject(sql, new CustomerRowMapper(), id);
}

1対多の変換

RowMapperは1レコードに対する処理しか実装できないため、1レコードにEntityの配列を持つようなマッピングには使用できない。
この場合は ResultSetExtractorを使用する。
以下のようなクラスとSQLでの実装を考える。

Customer クラス

public class Customer {
  private String id;
  private String name;
  private List<Order> orders;
  ...Getter,Setterメソッド
}

Order クラス

public class Order {
  private String id;
  private String code;
  private String name;
  private String customerId;
  ...Getter,Setterメソッド
}

SQL

SELECT 
  c.id AS c_id, 
  c.name AS c_name,
  o.id AS o_id,
  o.code AS o_code;
  o.code AS o_name;
  o.customer_id AS o_customer_id
FROM 
  customers c
  LEFT OUTER JOIN orders o
  ON c.id = o.customer_id;
WHERE
  c.id = '1'

ResultSetExtractorの実装は以下のようになる。

static class CustomerResultSetExtractor implements ResultSetExtractor<Customer> {
  @Override
  public Customer extractData(ResultSet rs) throws SQLException, DataAccessException {
    Customer customer = null;
    while(rs.next()) {
      if (customer == null) {
        customer.setId(rs.getString("c_id"));
        customer.setname(rs.getString("c_name"));
        customer.setOrder(new ArrayList<>());
      }
      Order order = new Order();
      order.setId(rs.getString("o_id"));
      order.setCode(rs.getString("o_code"));
      order.setName(rs.getString("o_name"));
      customer.getOrder().add(order)
    }
    
    return customer;
  }
}

この記事が気に入ったらサポートをしてみませんか?