新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

学習記録:Spring

[学習記録] Spring

内容:第4章 データアクセス層の設計と実装

データアクセス層の役割
  • データアクセス層の主要な役割
    • データアクセスの処理をビジネスロジック層から分離すること
    • データベースへの接続、SQLの発行などがビジネスロジック層にあると可読性が落ちる
    • データアクセスの処理に特化したオブジェクトのことをDAOと呼ぶ(J2EEパターンの1つ、DAOパターン)
  • DAOパターン
    • データアクセスの処理を分離することで、データアクセスの方式が変わってもDAOだけ変更すれば良い
    • データベースのテーブルごとに作られることが多い
    • テーブルの定義情報から自動生成することも
Javaのデータアクセス技術とSpring
  • Javaのデータアクセス技術:JDBC, Hibernate, JPA, MyBatis, JDO...
    • Springはこれらと連携する機能を提供、以降ではJDBCとの連携機能であるSpring JDBCを解説
    • Springの機能を利用することで得られる利点は以下の3つ
  1. データアクセスの処理の記述をシンプルに
  2. Springが提供する汎用的・体系的なデータアクセス例外を利用できる
  3. Springのトランザクション機能が利用できる
  • JDBCを直接利用した場合
    • ConnectionやPreparedStatementの取得とクローズ処理
    • SQLExceptionのエラーコードを取得して値を調べる
    • SQLExceptionのcatch/throwsが必須...などでソースコードの行数が多くなってしまう
Spring JDBC
  • 前述の問題はSpring JDBCを活用することで解決できる
    • Spring JDBCJDBCをラップしたAPIを提供し、冗長な処理を隠蔽してくれる
    • SQLExceptionの原因特定の処理なども不要に
  • Templateクラスを使ってソースコードをシンプルに
    • JdbcTemplateクラス:メソッドの種類が豊富で、直接利用できるJDBCAPIの範囲も広い
    • NamedParameterJdbcTemplateクラス:SQLのパラメータに任意の名前をつけてSQLを発行できる
    • 基本的にはJdbcTemplateだけで機能的には網羅されている
    • ※SimpleJdbcTemplateはSpring3.1からdeprecatedに
  • TemplateクラスのBeanの登録とインジェクション
    • いくつか方法があるがBean定義ファイルに記述することで登録できる
    • Templateクラスのオブジェクトを格納するフィールドを用意して@Autowiredを指定(Bean定義ファイルでインジェクションも可)
    • @Repositoryは@Componentを拡張したアノテーションで、DAOの登録に使用
    • データアクセス時の例外を全てDataAccessExceptionに変換する(そもそもTemplateクラスが変換するのでお作法的な意味が強い)
<bean class="org.springframework.jdbc.core.JdbcTemplate">
    <constructor-arg ref="dataSource" />
</bean>
<bean class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
    <constructor-arg ref="dataSource" />
</bean>
@Repository
public class SpringJdbcHogeDao implements HogeDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private NamedParameterJdbcTemplate npJdbcTemplate;
    ....
}
SQLの発行:SELECT文(ドメインへ変換しない場合)

ドメインへ変換しない場合=レコードの件数を取得したり、1レコードの中の特定カラムなどを取得する場合を指す

  • queryForInt、queryForLong
    • 数値型の値を取得する場合
    • パラメータを指定する場合は?を使って記述
int count = jdbcTemplate.queryForInt("SELECT COUNT(*) FROM pet");

int count = jdbcTemplate.queryForInt(
    "SELECT COUNT(*) FROM pet WHERE owner_name=?", ownerName);
  • queryForObject
    • 取得結果が文字列や日付型の場合に使用
    • 第二引数に指定しているのは、取得結果のオブジェクトのClassオブジェクト
Date birthDate = jdbcTemplate.queryForObject(
    "SELECT birth_date FROM pet WHERE pet_id=?", Date.class, id);
  • queryForMap
    • 1レコード分の値をMap(カラム名をキーとして値が入る)で取得可能
Map<String, Object> pet = jdbcTemplate.queryForMap("SELECT * FROM pet WHERE pet_id=?", id);
  • queryForList
    • 複数レコード分のMapデータを取得する
    • Mapを複数格納したListを取得することが可能
List<Map<String, Object>> petList = jdbcTemplate.queryForList(
    "SELECT * FROM pet WHERE owner_name=?", ownerName);
SQLの発行:SELECT文(ドメインへ変換する場合)
  • queryForObject, query
    • queryForObjectは1レコード分のドメインを取得する際にも使用
    • 第二引数にドメインへの変換処理を実装したクラスのオブジェクトを渡す
    • 匿名クラスを使うか、RowMapperのオブジェクトを渡す
    • queryは複数レコード分のドメインを取得するときに使用
/* 匿名クラスを使う場合 */
Pet pet = jdbcTemplate.queryForObject(
    "SELECT * FROM pet WHERE pet_id=?",
    new RowMapper<Pet>() {
        public Pet mapRow(ResultSet rs, int rowNum) throws SQLException {
            Pet p = new Pet();
            p.setPetId(rs.getInt("pet_id"));
            ...
            return p;
            }}
        , id);

/* RowMapperを使う場合 */
class MyRowMapper implements RowMapper<Pet> {
    public Pet mapRow(ResultSet rs, int rowNum) throws SQLException {
        Pet p = new Pet();
        p.setPetId(rs.getInt("pet_id"));
        ...
        return p;
    }
}
Pet pet = jdbcTemplate.queryForObject(
            "SELECT * FROM pet WHERE pet_id=?"
            , new MyRowMapper()
            , id);
  • RowMapperで行うドメインへの変換処理も自動でやってくれない?
    • Springが提供するBeanPropertyRowMapperを使用すれば変換も自動化
    • ドメインクラスのプロパティ名とテーブルのカラム名を紐付けて、ドメインを自動生成してくれる
    • プロパティ名をカラム名と同じか、プロパティpetName -> カラム名pet_nameのような形にする
    • ただし、リフレクションを利用しているためパフォーマンスが悪くなる(2倍くらい?)ので注意
Pet pet = jdbcTemplate.queryForObject(
             "SELECT * FROM pet WHERE pet_id=?"
             , new BeanPropertyRowMapper<Pet>(Pet.class)
             , id);
SQLの発行:INSERT/UPDATE/DELETE文
  • updateメソッド
    • INSERT/UPDATE/DELETE文は全てこれでOK
jdbcTemplate.update("DELETE FROM pet WHERE pet_id=?", pet.getPetId());
npJdbcTemplate.update(
     "INSERT INTO pet(pet_id, pet_name, owner_name, price, birth_date)" +
        " VALUE (:PET_ID, :PET_NAME, :OWNER_NAME, :PRICE, :BIRTH_DATE)"
    , new MapSqlParameterSource()
    .addValue("PET_ID", pet.getPetId())
    .addValue...
    ...
);
SQLの発行:バッチアップデート、プロシージャコール
  • batchUpdateメソッド
    • 大量のレコードを一度に更新するためのもの
    • 1レコードずつアップデートするのと比べてパフォーマンスが大きく違う
    • 使用方法はupdateメソッドと同様
List<Object[]> args = new ArrayList<Object[]>();
for (Pet pet : petList) {
    args.add(new Object[]{pet.getOwnerName(), pet.getPetId()});
}
int[] num = jdbcTemplate.batchUpdate(
    "UPDATE pet SET owner_name=? WHERE pet_id=?", args);
汎用データアクセス例外
  • Springではデータアクセス時の例外を体系的に用意
    • JDBC, Hibernateなどのデータアクセス技術独自の例外を汎用データベース例外に変換
    • 例外をハンドリングするクラスでは、データアクセス技術に依存せずに例外をハンドリングできる
例外クラス エラーの原因
CannotAcquireLockException ロックの取得に失敗
ConcurrencyFailureException 同時実行時のエラー
DataAccessResourceFailureException データソースとの接続の失敗
DataIntegrityViolationException 整合性違反エラー
DeadlockLoserDataAccessException デッドロックが発生
EmptyResultDataAccessException 取得しようとしたデータが存在しない
IncorrectResultSizeDataAccessException 取得したレコード数が不正
OptimistLockingFailureException 楽観的ロック失敗
PermissionDeniedDataAccessException 権限エラー
  • 汎用データアクセス例外のハンドリングの方針
    • DAOでcatchすることもできるが、基本的にはスルーするのがよい
    • サービスやコントローラで対処しなくてはならない例外のみcatchして対処
    • 取得されるはずのデータが取得されなかったとき -> サービスでcatchして別のテーブルからデータを取得、など
    • データベースとの接続ができない場合などはビジネスロジックで対処できないので、共通の仕組みで一元処理するのが良い
データソース
  • データソースは、Connectionオブジェクトのファクトリと言える
    • Connectionオブジェクトのライフサイクルはデータソースに任されている
    • 通常の業務アプリケーションではコネクションプールによってConnectionオブジェクトを使いまわす作りになっている
    • データベースのリソース枯渇などを防ぐ
  • Springでのデータソースの使い方
    • データソースをBean定義ファイルで定義: ...
    • 開発者が作成した、もしくはSpringが提供するBeanにインジェクションして利用する
    • インジェクションはアノテーションでもBean定義ファイルでもOK
    • 開発者は適切なDataSourceの実装を選択して設定を行う

※Bean定義ファイルでデータソースを定義する際に指定するドライバクラス名やURLなどは、
変更を容易にするために別途プロパティファイルを用意して記述するほうがよい
(contextスキーマのproperty-placeholderタグを利用すればプロパティファイルに記述した文字列を定義ファイルに取り込める)

  • Springが提供するDataSource
    • SingleConnectionDataSource:1つのConnectionをクローズせずに使いまわす
    • DriverManagerDataSource:取得の要求の度に、Connectionオブジェクトを作成して返す
    • どちらもテストでの用途を想定して作られており、コネクションプールに対応していない
  • サードパーティが提供するDataSource
    • 代表的なものにDBCP, c3p0
    • DBCPではmaxActive(プールするコネクションの数)などの指定ができる
  1. JndiObjectFactoryBeanを使用
  2. jeeスキーマを使用
  3. @Resourceアノテーションを使用