Spring Boot Mapperの使い方入門【初心者向け】


Spring Boot Mapperの使い方入門【初心者向け】

Spring Bootを使ってアプリケーション開発を進める上で、データベースとの連携は避けて通れません。ユーザー登録、商品の購入、情報の検索など、アプリケーションが扱うデータのほとんどはデータベースに保存され、必要に応じて読み書きされます。

Spring Bootは、データベース連携を簡単にするための強力な機能やライブラリを多数提供しています。その中でも、「Mapper」やそれに類する役割を果たすコンポーネントは、アプリケーションのビジネスロジック層とデータベースの永続化層の間を取り持つ非常に重要な存在です。

この記事では、Spring Boot開発におけるMapperの基本的な概念から、代表的な実装方法(MyBatis、Spring Data JPA、DTO変換)までを、初心者の方にも分かりやすく、ステップバイステップで解説します。約5000語かけて、それぞれの使い方、メリット、デメリット、そして実践的なコード例を詳しく見ていきましょう。

1. はじめに:なぜMapperが必要なのか?

Spring BootのようなモダンなWebアプリケーションフレームワークでは、通常、レイヤードアーキテクチャを採用します。これは、アプリケーションをいくつかの論理的な層(レイヤー)に分割し、それぞれの層が特定の役割を担うようにする設計手法です。

一般的な層の構成例:

  1. プレゼンテーション層 (Presentation Layer / Web Layer):
    • ユーザーからのリクエストを受け付け、レスポンスを返す層。
    • Spring MVCにおけるコントローラー(@RestController, @Controller)などがこれにあたります。
    • 主にデータフォーマットの変換(JSONなど)や、入力値のバリデーションを行います。
  2. サービス層 (Service Layer / Business Logic Layer):
    • アプリケーションの主要なビジネスロジックを実装する層。
    • 複数の永続化操作や外部サービス呼び出しを組み合わせた一連の処理を実行します。
    • トランザクション管理などもここで行われます。
    • @Serviceアノテーションをつけたクラスなどがこれにあたります。
  3. 永続化層 (Persistence Layer / Data Access Layer):
    • データベースなどの永続化ストアへのアクセスを専門に行う層。
    • データの保存、更新、削除、検索などの操作を担当します。
    • 後述する「Mapper」や「Repository」がこの層に含まれます。

このレイヤードアーキテクチャにおいて、各層は原則として自分より下の層にのみ依存します。つまり、サービス層は永続化層を知っていますが、永続化層はサービス層を知りません。

データベースとの連携とデータの流れ

データベースからデータを読み込む際、永続化層はデータベースのテーブル構造に対応した形式でデータを受け取ります。これを一般的に「エンティティ(Entity)」と呼びます。エンティティクラスは、データベースのテーブルのカラムに対応するフィールドを持つJavaクラスとして定義されます。

永続化層がデータベースからエンティティを取得したら、そのエンティティはサービス層に渡され、ビジネスロジックの処理に使われます。さらに、サービス層で処理されたデータは、プレゼンテーション層に渡され、最終的にユーザーに返されます。

ここで問題になるのが、エンティティをそのままプレゼンテーション層やサービス層で扱うことの是非です。

なぜエンティティを直接使わない方が良い場合があるのか?

エンティティはデータベースの構造と密接に結びついています。そのため、以下のような理由から、エンティティを直接、永続化層の外側(特にプレゼンテーション層)に露出させるのは望ましくない場合があります。

  1. データベース構造の変更への弱さ: データベースのテーブル構造(カラム名、型、テーブル名など)が変更されると、それにマッピングされたエンティティクラスも変更が必要です。エンティティをアプリケーション全体で直接使っていると、プレゼンテーション層やサービス層の広範囲に影響が波及し、変更コストが増大します。
  2. セキュリティ: エンティティは、データベースに保存されている全てのカラムに対応するフィールドを持つことがあります。しかし、ユーザーに返すデータや、ビジネスロジックで必要なデータは、その一部で十分な場合が多いです。不要なフィールド(パスワードのハッシュ値など)を誤ってプレゼンテーション層に渡してしまうリスクがあります。
  3. 過剰なデータのロード (N+1問題など): JPAなどのO/Rマッパーでは、関連するエンティティを自動的にロードする機能があります(Lazy/Eager Loading)。エンティティを不用意に触ることで、意図しない関連データのロードが発生し、パフォーマンス問題(N+1問題など)を引き起こす可能性があります。
  4. ビジネスロジックとの分離: エンティティは永続化の概念です。ビジネスロジックは永続化の詳細から独立しているべきです。エンティティにビジネスロジックを書き始めると、層の分離が曖昧になり、保守性が低下します。
  5. API契約の安定性: 特にRESTful APIの場合、APIのレスポンス形式はクライアントとの契約になります。エンティティ構造がデータベース変更で変わっても、APIのレスポンス形式は変えたくない場合があります。

これらの問題を解決するために登場するのが、「DTO (Data Transfer Object)」と、エンティティとDTO(またはその他の層で使うオブジェクト)の間を変換する役割です。この変換を行う部分を「Mapper」と呼ぶことが多いです。

DTOは、特定の目的(例: APIレスポンス、リクエストボディ、サービス層内部でのデータ受け渡し)のために定義されるシンプルなJavaオブジェクトです。必要なフィールドのみを持ち、ビジネスロジックは含みません。

永続化層はデータベースからエンティティを取得し、そのエンティティをDTOに変換してサービス層に渡します。サービス層はDTOを使ってビジネスロジックを実行し、必要に応じてDTOをエンティティに変換して永続化層に渡すことでデータベースに保存します。プレゼンテーション層はサービス層から受け取ったDTOをそのまま、またはさらに変換してクライアントに返します。

この「エンティティ ⇔ DTO」変換の役割を担うのが、広義の「Mapper」の一つです。

また、狭義の「Mapper」としては、MyBatisのように、データベースのカラムとJavaオブジェクトのフィールドをマッピングし、SQLの実行結果をJavaオブジェクトに変換したり、JavaオブジェクトからSQLのパラメータを生成したりする役割を指すこともあります。

この記事では、これらの両方の側面、すなわち

  1. データベースのデータ ⇔ Javaオブジェクト(エンティティ) のマッピング
  2. 異なるJavaオブジェクト(エンティティ ⇔ DTO) のマッピング

に関わる技術やツールを「Mapper」として紹介し、その使い方を解説していきます。

具体的には、以下の主要な技術・ツールを取り上げます。

  • MyBatis: SQL中心の永続化フレームワーク。JavaオブジェクトとSQLパラメータ/結果セットをマッピングする「Mapper」インターフェースを定義します。
  • Spring Data JPA: JPA (Java Persistence API) を利用したO/Rマッピングフレームワーク。エンティティクラス自体がマッピング定義となり、リポジトリインターフェースを通じてデータベース操作を行います。広義にはエンティティがデータベースへのマッパーとして機能します。
  • ModelMapper / MapStruct: Javaオブジェクト間(特にエンティティとDTO)の変換を自動化するライブラリ。

それでは、それぞれの具体的な使い方を見ていきましょう。

2. MyBatis Mapperの使い方

MyBatisは、SQL文を直接記述することに重点を置いた永続化フレームワークです。Spring Bootとの連携が非常に容易で、Spring Boot Starter MyBatisを利用することで手軽に導入できます。

MyBatisにおける「Mapper」は、データベース操作メソッドを定義するJavaインターフェースを指します。このインターフェースのメソッドと、対応するSQL文(XMLファイルまたはアノテーションで定義)を結びつけることで、データベースアクセスを実装します。

2.1 プロジェクトセットアップ

まず、Spring BootプロジェクトにMyBatisの依存関係を追加します。pom.xmlに以下を追加してください。

“`xml

org.mybatis.spring.boot
mybatis-spring-boot-starter
3.0.2


com.h2database
h2
runtime



“`

mybatis-spring-boot-starterには、MyBatis CoreとSpring連携モジュール、データベース接続プールの依存関係などが含まれています。ここでは、開発・テスト用にインメモリデータベースのH2を追加しています。

2.2 データベース設定

src/main/resources/application.properties または application.yml にデータベース接続設定を記述します。H2データベースを使用する場合の例です。

application.properties:

“`properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true # H2 Consoleを有効にする(開発用)

MyBatisの設定

Mapperインターフェースのあるパッケージを指定

mybatis.mapper-locations=classpath:mapper/*.xml # XMLでSQLを定義する場合
mybatis.type-aliases-package=com.example.myapp.domain.model # エイリアスを定義する場合
“`

mybatis.mapper-locationsは、XMLでSQLを定義する場合に、XMLファイルの場所を指定します。mybatis.type-aliases-packageは、FQCN(完全修飾クラス名)の代わりに短いエイリアス(例: User)でクラスを参照できるようにするための設定です。

2.3 エンティティ(モデル)クラスの作成

データベースのテーブルに対応するJavaクラスを作成します。MyBatisでは、これはPOJO(Plain Old Java Object)であり、特別なアノテーションなどは必須ではありません(JPAとは異なります)。

例: Userテーブルに対応する User クラス

“`java
package com.example.myapp.domain.model;

// getter, setter, constructor などが必要
public class User {
private Long id;
private String name;
private String email;

// コンストラクタ
public User() {}

public User(Long id, String name, String email) {
    this.id = id;
    this.name = name;
    this.email = email;
}

// getter/setter
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }

@Override
public String toString() {
    return "User{" +
           "id=" + id +
           ", name='" + name + '\'' +
           ", email='" + email + '\'' +
           '}';
}

}
“`

2.4 Mapperインターフェースの作成

MyBatisの「Mapper」は、データベース操作メソッドを定義するJavaインターフェースです。このインターフェースに@Mapperアノテーションを付けます。Spring Bootは、このアノテーションが付いたインターフェースをスキャンし、その実装クラスを自動的に生成してBeanとして登録します。

“`java
package com.example.myapp.infrastructure.mapper;

import com.example.myapp.domain.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper // MyBatis Mapperとして認識させるアノテーション
public interface UserMapper {

User findById(@Param("id") Long id); // ユーザーIDで検索

List<User> findAll(); // 全ユーザーを取得

int insert(User user); // ユーザーを登録

int update(User user); // ユーザー情報を更新

int deleteById(@Param("id") Long id); // ユーザーをIDで削除

}
“`

@Paramアノテーションは、メソッドの引数に名前を付けて、SQL文中でその名前で参照できるようにするために使用します。引数が複数の場合や、単一の引数でもXMLマッピングで参照する場合は付けておくのが良い習慣です。単一の引数で、アノテーションマッピングの場合は付けなくても参照可能です(例: select * from users where id = #{param1} または #{arg0})。

2.5 SQLマッピングの定義(XML vs アノテーション)

MyBatisでは、Mapperインターフェースのメソッドと実際のSQL文を紐付ける方法として、XMLファイルを使う方法と、インターフェースメソッドに直接アノテーションを付ける方法の2つがあります。

2.5.1 XMLマッピング

SQL文を別途XMLファイルに記述する方法です。複雑なSQLや動的SQLを扱う場合に整理しやすくなります。

  1. XMLファイルの作成: src/main/resources/mapper ディレクトリを作成し、UserMapper.xml というファイルを作成します。ファイル名はMapperインターフェース名に合わせるのが一般的です。

    “`xml
    <?xml version=”1.0″ encoding=”UTF-8″?>
    <!DOCTYPE mapper PUBLIC “-//mybatis.org//DTD Mapper 3.0//EN”
    “http://mybatis.org/dtd/mybatis-3-mapper.dtd”>


    <!-- 結果マッピングの定義 -->
    <!-- type属性でマッピング先のクラスを指定 -->
    <!-- id属性はXML内で参照するための名前 -->
    <resultMap id="userResultMap" type="com.example.myapp.domain.model.User">
        <!-- idタグで主キーカラムとJavaプロパティをマッピング -->
        <id property="id" column="id"/>
        <!-- resultタグでその他のカラムとJavaプロパティをマッピング -->
        <result property="name" column="name"/>
        <result property="email" column="email"/>
    </resultMap>
    
    <!-- SQL定義 -->
    <!-- idはMapperインターフェースのメソッド名と一致させる -->
    <!-- resultMap属性で結果マッピングを指定 -->
    <select id="findById" resultMap="userResultMap">
        SELECT id, name, email FROM users WHERE id = #{id}
    </select>
    
    <select id="findAll" resultMap="userResultMap">
        SELECT id, name, email FROM users
    </select>
    
    <!-- パラメータは #{propertyName} または #{paramName} で参照 -->
    <!-- useGeneratedKeysとkeyPropertyで自動生成された主キーを取得 -->
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO users (name, email) VALUES (#{name}, #{email})
    </insert>
    
    <update id="update">
        UPDATE users SET name = #{name}, email = #{email} WHERE id = #{id}
    </update>
    
    <delete id="deleteById">
        DELETE FROM users WHERE id = #{id}
    </delete>
    

    “`

  2. 設定の確認: application.propertiesmybatis.mapper-locations=classpath:mapper/*.xmlが正しく設定されているか確認します。

XMLマッピングでは、<resultMap>タグを使って、データベースのカラム名とJavaオブジェクトのプロパティ名を詳細にマッピングできます。これは、カラム名とプロパティ名が一致しない場合や、複雑なリレーションをマッピングする場合に役立ちます。<select>, <insert>, <update>, <delete>タグの中にSQL文を記述します。パラメータは#{propertyName}#{paramName}の形式で参照します。

2.5.2 アノテーションマッピング

Mapperインターフェースのメソッドに直接、SQL文を記述したアノテーションを付ける方法です。シンプルなSQLの場合にコードが分散せず見やすくなります。

UserMapper.java:

“`java
package com.example.myapp.infrastructure.mapper;

import com.example.myapp.domain.model.User;
import org.apache.ibatis.annotations.*; // MyBatisのアノテーションをインポート

import java.util.List;

@Mapper
public interface UserMapper {

// @SelectアノテーションでSELECT文を記述
// カラム名とプロパティ名が一致する場合は自動マッピングされる
@Select("SELECT id, name, email FROM users WHERE id = #{id}")
User findById(@Param("id") Long id);

@Select("SELECT id, name, email FROM users")
List<User> findAll();

// @InsertアノテーションでINSERT文を記述
// useGeneratedKeysとkeyPropertyで自動生成された主キーを取得
@Insert("INSERT INTO users (name, email) VALUES (#{name}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);

// @UpdateアノテーションでUPDATE文を記述
@Update("UPDATE users SET name = #{name}, email = #{email} WHERE id = #{id}")
int update(User user);

// @DeleteアノテーションでDELETE文を記述
@Delete("DELETE FROM users WHERE id = #{id}")
int deleteById(@Param("id") Long id);

// カラム名とプロパティ名が一致しない場合や、より詳細なマッピングが必要な場合は@Resultsと@Resultを使用
@Results({
    @Result(property = "id", column = "user_id"),
    @Result(property = "name", column = "user_name"),
    @Result(property = "email", column = "user_email")
})
@Select("SELECT user_id, user_name, user_email FROM users WHERE user_id = #{id}")
User findByIdWithCustomMapping(@Param("id") Long id);

}
“`

アノテーションマッピングでも、@Select, @Insert, @Update, @Deleteなどのアノテーションを使います。パラメータ参照や自動生成キーの取得方法はXMLの場合と同様です。

カラム名とプロパティ名が一致しない場合は、@Results@Resultアノテーションを使って明示的にマッピングを定義できます。

XMLとアノテーションは混在させることも可能ですが、プロジェクト内でどちらかに統一する方が管理しやすいため推奨されます。一般的には、シンプルなクエリはアノテーション、複雑なクエリや動的SQLはXMLという使い分けが多いです。

2.6 DDLの準備 (H2データベースの場合)

H2のようなインメモリデータベースを使う場合、アプリケーション起動時にテーブルを作成するためのSQLファイルを用意することが多いです。

src/main/resources/schema.sql:

sql
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE
);

src/main/resources/data.sql (テストデータ):

sql
INSERT INTO users (name, email) VALUES ('Alice', '[email protected]');
INSERT INTO users (name, email) VALUES ('Bob', '[email protected]');

Spring Bootは、デフォルトでschema.sqldata.sqlをクラスパスのルートから見つけ、起動時に実行します。特別な設定は不要です。

2.7 サービス層からの利用

MyBatis MapperインターフェースはSpringによってBeanとして管理されるため、他のBean(例: サービス層のクラス)にDI(依存性注入)して利用できます。

“`java
package com.example.myapp.application.service;

import com.example.myapp.domain.model.User;
import com.example.myapp.infrastructure.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; // トランザクション管理用

import java.util.List;

@Service // Spring Serviceとして認識させるアノテーション
public class UserService {

private final UserMapper userMapper;

// コンストラクタインジェクション
@Autowired
public UserService(UserMapper userMapper) {
    this.userMapper = userMapper;
}

@Transactional(readOnly = true) // 読み取り専用トランザクション
public User findUserById(Long id) {
    return userMapper.findById(id);
}

@Transactional(readOnly = true)
public List<User> findAllUsers() {
    return userMapper.findAll();
}

@Transactional // 書き込みトランザクション
public void createUser(User user) {
    userMapper.insert(user);
    // ここでuser.getId()を呼び出せば、insert時に生成されたIDを取得できる
    System.out.println("Created user with ID: " + user.getId());
}

@Transactional
public boolean updateUser(User user) {
    int updatedRows = userMapper.update(user);
    return updatedRows > 0; // 更新された行数が1以上ならtrue
}

@Transactional
public boolean deleteUserById(Long id) {
    int deletedRows = userMapper.deleteById(id);
    return deletedRows > 0; // 削除された行数が1以上ならtrue
}

}
“`

サービス層では、MyBatis Mapperのメソッドを呼び出してデータベース操作を行います。@Transactionalアノテーションを使って、ビジネスロジックの単位でトランザクションを管理することが重要です。

2.8 コントローラーからの利用

サービス層のクラスもSpring Beanとして管理されるため、コントローラーにDIして利用できます。

“`java
package com.example.myapp.presentation.controller;

import com.example.myapp.application.service.UserService;
import com.example.myapp.domain.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController // RESTコントローラー
@RequestMapping(“/api/users”) // エンドポイントのベースパス
public class UserController {

private final UserService userService;

@Autowired
public UserController(UserService userService) {
    this.userService = userService;
}

@GetMapping("/{id}") // GET /api/users/{id}
public ResponseEntity<User> getUserById(@PathVariable Long id) {
    User user = userService.findUserById(id);
    if (user != null) {
        return ResponseEntity.ok(user); // 200 OK とUserオブジェクト
    } else {
        return ResponseEntity.notFound().build(); // 404 Not Found
    }
}

@GetMapping // GET /api/users
public List<User> getAllUsers() {
    return userService.findAllUsers(); // 200 OK とユーザーリスト
}

@PostMapping // POST /api/users
@ResponseStatus(HttpStatus.CREATED) // 201 Created を返す
public User createUser(@RequestBody User user) {
    userService.createUser(user);
    // createUserメソッド内でidがセットされている前提
    return user;
}

@PutMapping("/{id}") // PUT /api/users/{id}
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
    user.setId(id); // パスパラメータのIDをUserオブジェクトにセット
    boolean updated = userService.updateUser(user);
    if (updated) {
        // 更新後のユーザー情報を返す場合は再度取得するなどが必要
        return ResponseEntity.ok(user); // 200 OK
    } else {
        return ResponseEntity.notFound().build(); // 404 Not Found
    }
}

@DeleteMapping("/{id}") // DELETE /api/users/{id}
@ResponseStatus(HttpStatus.NO_CONTENT) // 204 No Content を返す
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
    boolean deleted = userService.deleteUserById(id);
    if (deleted) {
         return ResponseEntity.noContent().build(); // 204 No Content
    } else {
        return ResponseEntity.notFound().build(); // 404 Not Found
    }
}

}
“`

この例では、コントローラーがサービス層を呼び出し、サービス層がMyBatis Mapperを使ってデータベースにアクセスしています。コントローラーはプレゼンテーション層の責務として、HTTPリクエスト/レスポンスの処理、パスパラメータやリクエストボディの受け取り、HTTPステータスコードの設定などを行っています。

注意点として、上記の例ではコントローラーがEntityクラス(User)を直接返していますが、前述した理由から、実際にはDTOクラスに変換して返す方がより良い設計です。DTO変換については後述します。

MyBatisのメリット・デメリット

メリット:

  • SQLを直接記述できる: 複雑なクエリやDB固有の機能を活用しやすい。チューニングもSQLレベルで行いやすい。
  • 学習コストが比較的低い: SQLの知識があれば理解しやすい。
  • O/Rマッパーの自動化機能に振り回されない: 意図しないN+1問題などが起きにくい(自分でSQLを書くため)。
  • パフォーマンス: 手書きSQLは、多くの場合、自動生成されるSQLよりも効率が良い可能性がある。
  • 動的SQL: XMLや専用タグを使って、柔軟な動的SQLを生成できる。

デメリット:

  • SQLを書く手間: 全てのCRUD操作に対してSQLを書く必要がある。定型的な操作でも省略できない。
  • データベース変更への対応: テーブル構造が変わると、関連するSQL全てを変更する必要がある。
  • 型安全性の低下: SQL文字列はコンパイル時にはチェックされないため、実行時エラーになりやすい。
  • JavaコードとSQLが分離する(XMLの場合): 関連するコードが別ファイルになり、追跡が難しくなることがある。

MyBatisは、既存のデータベース構造が複雑な場合や、パフォーマンス要件が高くSQLを細かくチューニングしたい場合に強力な選択肢となります。

3. Spring Data JPA (Repository) の使い方

Spring Data JPAは、JPA(Java Persistence API)をベースにしたデータアクセスフレームワークです。MyBatisとは異なり、SQLをほとんど書かずに、Javaのエンティティクラスとリポジトリインターフェースを通じてデータベース操作を行います。これはO/Rマッピング (Object-Relational Mapping)という考え方に基づいています。

JPAにおける「Mapper」的な役割は、主にエンティティクラス自体が担います。エンティティクラスに付与されたアノテーション(@Entity, @Table, @Columnなど)が、Javaオブジェクト(エンティティインスタンス)とデータベースのテーブル/カラムをマッピングする方法を定義します。

データベース操作は、Repositoryインターフェースを定義することで行います。Spring Data JPAは、このインターフェースを解析し、標準的なCRUDメソッドや、特定の命名規則に従ったカスタム検索メソッドの実装を自動的に生成します。

3.1 プロジェクトセットアップ

pom.xmlにSpring Data JPAの依存関係を追加します。特定のJPAプロバイダ(例: Hibernate – Spring Bootのデフォルト)とデータベースドライバも必要です。

“`xml

org.springframework.boot
spring-boot-starter-data-jpa


com.h2database
h2
runtime

“`

3.2 データベース設定

application.properties または application.yml にデータベース接続設定を記述します。MyBatisと同様、H2データベースを使用する場合の例です。

application.properties:

“`properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true # H2 Consoleを有効にする(開発用)

JPA/Hibernate の設定

spring.jpa.database-platform=org.hibernate.dialect.H2Dialect # 使用するDB方言
spring.jpa.hibernate.ddl-auto=create-drop # アプリケーション起動時にスキーマを作成/破棄
spring.jpa.show-sql=true # 生成されるSQLをログに出力
spring.jpa.properties.hibernate.format_sql=true # SQLを整形して出力
“`

spring.jpa.hibernate.ddl-autoは、開発時にはcreate-drop(起動時にスキーマ作成、終了時に破棄)やupdate(既存スキーマを更新)が便利ですが、本番環境ではnoneまたはvalidateを使用し、外部のマイグレーションツール(Flyway, Liquibaseなど)でスキーマ管理を行うのが一般的です。

3.3 エンティティクラスの作成

JPAでは、データベースのテーブルに対応するクラスを@Entityアノテーションを付けて定義します。テーブル名とクラス名が異なる場合は@Table、カラム名とプロパティ名が異なる場合は@Columnを使います。主キーには@Id、自動生成される主キーには@GeneratedValueを付けます。

例: usersテーブルに対応する User エンティティ

“`java
package com.example.myapp.domain.model;

import jakarta.persistence.*; // JPAのアノテーションをインポート

@Entity // このクラスがJPAのエンティティであることを示す
@Table(name = “users”) // マッピングするテーブル名(クラス名と同じなら省略可)
public class User {

@Id // 主キーであることを示す
@GeneratedValue(strategy = GenerationType.IDENTITY) // 主キーがDBで自動生成されることを示す(IDENTITYは自動連番)
private Long id;

@Column(name = "name", nullable = false) // カラム名と制約(プロパティ名と同じなら省略可)
private String name;

@Column(name = "email", nullable = false, unique = true) // unique制約
private String email;

// JPAでは引数なしのデフォルトコンストラクタが必須
protected User() {}

// 通常のコンストラクタ (主キーを含めない場合など)
public User(String name, String email) {
    this.name = name;
    this.email = email;
}

// getter/setter (JPA自体は不要だが、アプリケーションコードからは必要)
public Long getId() { return id; }
// public void setId(Long id) { this.id = id; } // 自動生成キーなのでsetterは不要な場合が多い
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }

@Override
public String toString() {
    return "User{" +
           "id=" + id +
           ", name='" + name + '\'' +
           ", email='" + email + '\'' +
           '}';
}

}
“`

エンティティクラスは、ビジネスロジックから永続化の詳細を隠蔽するために、通常はサービス層までで扱い、プレゼンテーション層にはDTOに変換して渡すことが推奨されます。

3.4 Repositoryインターフェースの作成

Spring Data JPAでは、データベース操作はRepositoryインターフェースを定義することで行います。CRUD操作やページング、ソートなどの基本的な機能は、Spring Data JPAが提供するRepositoryインターフェース(例: CrudRepository, PagingAndSortingRepository, JpaRepository)を継承するだけで利用できます。

特定の条件でデータを検索したい場合は、Spring Data JPAが提供するクエリメソッド (Query Methods)の命名規則に従ってメソッドを定義するだけで、Spring Data JPAが自動的に対応するクエリを生成します。

例: UserRepositoryインターフェース

“`java
package com.example.myapp.infrastructure.repository;

import com.example.myapp.domain.model.User;
import org.springframework.data.jpa.repository.JpaRepository; // JpaRepositoryをインポート
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional; // Optionalを使うのがSpring Data JPAの慣習

// JpaRepository<エンティティクラス, 主キーの型> を継承
@Repository // Spring Repositoryとして認識させるアノテーション (なくても動作するが推奨)
public interface UserRepository extends JpaRepository {

// 標準的なCRUD操作 (findAll(), findById(), save(), deleteById() など) は継承した時点で利用可能

// クエリメソッドの例: メールアドレスでユーザーを検索
// findBy + プロパティ名 (+ 条件) の命名規則
Optional<User> findByEmail(String email);

// 名前で部分一致検索(大文字小文字を区別せず)
List<User> findByNameContainingIgnoreCase(String name);

// 名前とメールアドレスでOR検索
List<User> findByNameOrEmail(String name, String email);

// 年齢が指定値より大きいユーザーを検索(例、Userエンティティにageフィールドがある場合)
// List<User> findByAgeGreaterThan(int age);

// 名前でソートして全件検索
List<User> findAllByOrderByNameAsc();

}
“`

JpaRepositoryは、CrudRepositoryPagingAndSortingRepositoryを継承しており、基本的なCRUD操作に加えて、ページングやソート機能も提供します。Optional<T>を戻り値にすることで、要素が見つからなかった場合にnullを返す代わりにOptional.empty()を返し、null安全なコードを書きやすくなります。

Spring Data JPAは、インターフェースに定義されたメソッド名から、実行すべきJPQL(Java Persistence Query Language)またはSQLを解析して自動生成します。例えば、findByEmail(String email)というメソッドからは、「SELECT u FROM User u WHERE u.email = ?1」のようなJPQLが生成されます。

3.5 カスタムクエリの定義 (@Query)

クエリメソッドの命名規則では表現できない複雑な検索や、JOINを含む検索などを行いたい場合は、@Queryアノテーションを使ってJPQLまたはネイティブSQLを直接記述できます。

“`java
package com.example.myapp.infrastructure.repository;

import com.example.myapp.domain.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; // @Queryをインポート
import org.springframework.data.repository.query.Param; // @Paramをインポート
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository {

Optional<User> findByEmail(String email);

// JPQLを使ったカスタムクエリ
@Query("SELECT u FROM User u WHERE u.name LIKE %:name%")
List<User> findUsersByNameLike(@Param("name") String name);

// ネイティブSQLを使ったカスタムクエリ (value属性でSQL、nativeQuery属性をtrueにする)
// SELECT COUNT(*) FROM users WHERE email = ?
@Query(value = "SELECT COUNT(*) FROM users WHERE email = :email", nativeQuery = true)
int countByEmailNative(@Param("email") String email);

// JPQLを使ったJOINクエリの例(UserがOrderという関連エンティティを持っていると仮定)
// @Query("SELECT u FROM User u JOIN u.orders o WHERE o.amount > :minAmount")
// List<User> findUsersWithOrdersGreaterThan(@Param("minAmount") BigDecimal minAmount);

}
“`

@Queryアノテーションのvalue属性にJPQLまたはSQLを記述します。JPQLはエンティティクラスやプロパティ名を参照しますが、ネイティブSQLはデータベースのテーブル名やカラム名を参照します。パラメータは:paramNameの形式で記述し、メソッド引数に@Param("paramName")を付けて紐付けます。

3.6 サービス層からの利用

Spring Data JPA RepositoryインターフェースもSpringによってBeanとして管理されるため、サービス層にDIして利用できます。

“`java
package com.example.myapp.application.service;

import com.example.myapp.domain.model.User;
import com.example.myapp.infrastructure.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; // トランザクション管理用

import java.util.List;
import java.util.Optional;

@Service
public class UserService {

private final UserRepository userRepository;

@Autowired
public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
}

@Transactional(readOnly = true)
public User findUserById(Long id) {
    // Optional<User>からUserを取り出す。見つからない場合はnullを返す
    return userRepository.findById(id).orElse(null);
}

@Transactional(readOnly = true)
public Optional<User> findUserByEmail(String email) {
    return userRepository.findByEmail(email);
}

@Transactional(readOnly = true)
public List<User> findAllUsers() {
    return userRepository.findAll();
}

@Transactional
public User createUser(User user) {
    // save()メソッドは、新規作成の場合はINSERT、IDがあればUPDATEを行う
    return userRepository.save(user);
}

@Transactional
public User updateUser(User user) {
    // IDがあることを確認してから更新処理
    if (user.getId() == null || !userRepository.existsById(user.getId())) {
        // 存在しない場合はnullを返すなどのエラー処理
        return null;
    }
    return userRepository.save(user); // save()で更新
}

@Transactional
public boolean deleteUserById(Long id) {
    if (userRepository.existsById(id)) { // 存在チェック
        userRepository.deleteById(id); // 削除
        return true;
    }
    return false; // 存在しない場合はfalse
}

}
“`

サービス層では、Repositoryインターフェースのメソッドを呼び出すだけでデータベース操作が実行されます。Spring Data JPAは自動的にクエリを生成し、JDBCなどの下層APIを使ってデータベースと通信します。トランザクション管理はMyBatisの場合と同様、@Transactionalアノテーションで行います。

3.7 コントローラーからの利用

コントローラーからサービス層を呼び出す部分はMyBatisの場合と同様です。ただし、サービス層から受け取るのがJPAエンティティであるため、DTO変換が必要になることを考慮します。

“`java
package com.example.myapp.presentation.controller;

import com.example.myapp.application.service.UserService;
import com.example.myapp.domain.model.User;
import com.example.myapp.presentation.dto.UserDto; // 後述のDTOクラス
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors; // List から List への変換に使用

@RestController
@RequestMapping(“/api/users”)
public class UserController {

private final UserService userService;
// DTO Mapper をDIする (詳細は後述)
// private final UserMapperDto userMapperDto; // 例

@Autowired
public UserController(UserService userService /*, UserMapperDto userMapperDto */) {
    this.userService = userService;
    // this.userMapperDto = userMapperDto;
}

@GetMapping("/{id}")
public ResponseEntity<UserDto> getUserById(@PathVariable Long id) {
    User user = userService.findUserById(id);
    if (user != null) {
        // Entity -> DTO 変換
        UserDto userDto = convertToDto(user); // あるいは userMapperDto.toDto(user);
        return ResponseEntity.ok(userDto);
    } else {
        return ResponseEntity.notFound().build();
    }
}

@GetMapping
public List<UserDto> getAllUsers() {
    List<User> users = userService.findAllUsers();
    // List<Entity> -> List<DTO> 変換
    return users.stream()
                .map(this::convertToDto) // あるいは userMapperDto::toDto
                .collect(Collectors.toList());
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserDto createUser(@RequestBody UserDto userDto) {
    // DTO -> Entity 変換
    User user = convertToEntity(userDto); // あるいは userMapperDto.toEntity(userDto);
    User createdUser = userService.createUser(user);
    // Entity -> DTO 変換して返す
    return convertToDto(createdUser); // あるいは userMapperDto.toDto(createdUser);
}

// PUT, DELETEなども同様にDTOとEntityの変換を考慮

// 簡単な変換メソッド (ModelMapperやMapStructを使うとより効率的)
private UserDto convertToDto(User user) {
    if (user == null) return null;
    UserDto userDto = new UserDto();
    userDto.setId(user.getId());
    userDto.setName(user.getName());
    userDto.setEmail(user.getEmail()); // 意図的に含めないフィールドがあっても良い
    return userDto;
}

private User convertToEntity(UserDto userDto) {
    if (userDto == null) return null;
    // 新規作成時はIDはDTOからセットしない(DBで自動生成されるため)
    User user = new User(userDto.getName(), userDto.getEmail());
    // 更新時はDTOからIDもセットする場合がある
    // user.setId(userDto.getId());
    return user;
}

}
“`

上記の例では、convertToDtoconvertToEntityという手書きのメソッドでエンティティとDTOの変換を行っています。フィールドが多いクラスや、複雑な変換が必要な場合は、これを手作業で行うのは煩雑です。そこで、次に紹介するDTOマッパーライブラリが役立ちます。

Spring Data JPAのメリット・デメリット

メリット:

  • 開発効率が高い: 定型的なCRUD操作やシンプルな検索メソッドは、インターフェース定義だけで済む。
  • データベース非依存性が高い: JPQLはエンティティに対してクエリするため、データベースの種類に依存しにくい(ただしネイティブSQLは除く)。
  • 型安全性が高い: クエリメソッドやJPQLはコンパイル時にチェックされる。
  • O/Rマッピング機能: オブジェクト指向的にデータベースを扱える。関連エンティティのマッピングなどが容易。

デメリット:

  • 学習コスト: JPAの概念(エンティティの状態、コンテキスト、関連マッピングなど)を理解する必要がある。
  • 複雑なクエリの扱い: JOINを含む複雑なクエリや特定のパフォーマンスチューニングが必要な場合は、@Queryを使うか、Criteria APIなどを利用する必要があり、学習コストや記述量が大きくなることがある。
  • 自動生成SQLの非効率性: 特定のケースで、自動生成されるSQLが最適な実行計画にならないことがある。
  • N+1問題: 関連データのフェッチ戦略(Lazy/Eager Loading)を適切に設定・考慮しないと、パフォーマンス問題を引き起こしやすい。

Spring Data JPAは、定型的なデータアクセスが多いアプリケーションや、開発速度を重視する場合に非常に有効な選択肢です。

4. DTOマッパーの使い方 (ModelMapper / MapStruct)

前述のように、レイヤードアーキテクチャにおいて、異なる層間でオブジェクト(特にエンティティとDTO)を受け渡す際に、オブジェクトの変換が必要になります。手作業でフィールドを一つずつコピーするのは、クラスのフィールドが増えるほど面倒でエラーも起こりやすくなります。

このようなオブジェクト間の変換を自動化してくれるのが、DTOマッパーライブラリです。ここでは、代表的な2つのライブラリ「ModelMapper」と「MapStruct」を紹介します。

4.1 DTO (Data Transfer Object) の作成

まず、プレゼンテーション層やサービス層でやり取りするためのDTOクラスを作成します。DTOは、必要なフィールドのみを持ち、ビジネスロジックは含みません。

例: UserDtoクラス

“`java
package com.example.myapp.presentation.dto;

// getter, setter, constructor などが必要
public class UserDto {
private Long id;
private String name;
// email は内部情報として公開しない、などの判断が可能
// private String email;

// コンストラクタ
public UserDto() {}

public UserDto(Long id, String name) {
    this.id = id;
    this.name = name;
}

// getter/setter
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
// public String getEmail() { return email; }
// public void setEmail(String email) { this.email = email; }

@Override
public String toString() {
    return "UserDto{" +
           "id=" + id +
           ", name='" + name + '\'' +
           '}';
}

}
“`

この例では、Userエンティティ(ID、名前、メールアドレス)から、メールアドレスを含まないUserDtoへの変換を考えます。

4.2 ModelMapperの使い方

ModelMapperは、実行時にJavaオブジェクトを別のJavaオブジェクトにマッピングするライブラリです。プロパティ名が一致する場合、自動的にマッピングを行います。

4.2.1 依存関係の追加

pom.xmlにModelMapperの依存関係を追加します。

xml
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.1</version> <!-- 最新バージョンを確認してください -->
</dependency>

4.2.2 Spring Beanとしての設定

ModelMapperのインスタンスをSpring Beanとして登録しておくと、DIしてどこからでも利用できます。通常は設定クラスで行います。

“`java
package com.example.myapp.config;

import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApplicationConfig {

@Bean // ModelMapperのインスタンスをSpring Beanとして登録
public ModelMapper modelMapper() {
    ModelMapper modelMapper = new ModelMapper();
    // 必要に応じてマッピング設定を追加
    // 例: Strictモードに設定して、マッピングされないプロパティがあった場合に例外をスロー
    // modelMapper.getConfiguration().setMatchingStrategy(org.modelmapper.convention.MatchingStrategies.STRICT);
    return modelMapper;
}

}
“`

4.2.3 変換処理の実装

ModelMapper Beanをサービス層などにDIし、map()メソッドを使って変換を行います。

“`java
package com.example.myapp.application.service;

import com.example.myapp.domain.model.User;
import com.example.myapp.infrastructure.repository.UserRepository; // または UserMapper
import com.example.myapp.presentation.dto.UserDto;
import org.modelmapper.ModelMapper; // ModelMapperをインポート
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
public class UserService {

private final UserRepository userRepository; // または UserMapper
private final ModelMapper modelMapper; // ModelMapperをDI

@Autowired
public UserService(UserRepository userRepository, ModelMapper modelMapper) {
    this.userRepository = userRepository;
    this.modelMapper = modelMapper;
}

@Transactional(readOnly = true)
public UserDto findUserDtoById(Long id) {
    Optional<User> userOptional = userRepository.findById(id); // JPAの場合
    // User user = userMapper.findById(id); // MyBatisの場合

    return userOptional.map(user -> modelMapper.map(user, UserDto.class)) // User -> UserDto 変換
                       .orElse(null);
    // または Mybatisの場合: return modelMapper.map(user, UserDto.class);
}

@Transactional(readOnly = true)
public List<UserDto> findAllUserDtos() {
    List<User> users = userRepository.findAll(); // JPAの場合
    // List<User> users = userMapper.findAll(); // MyBatisの場合

    // List<User> -> List<UserDto> 変換
    return users.stream()
                .map(user -> modelMapper.map(user, UserDto.class))
                .collect(Collectors.toList());
}

@Transactional
public UserDto createUserFromDto(UserDto userDto) {
    // UserDto -> User (Entity) 変換
    User user = modelMapper.map(userDto, User.class);
    // DTOに含まれないフィールドは、Entity側でデフォルト値やビジネスロジックで設定
    // 例: user.setCreatedAt(LocalDateTime.now());

    User savedUser = userRepository.save(user); // JPAの場合
    // int affectedRows = userMapper.insert(user); // MyBatisの場合

    // 保存後のEntity (自動生成IDなどがセットされている) を DTO に変換して返す
    return modelMapper.map(savedUser, UserDto.class);
}

// 更新や削除も同様に変換を適用

}
“`

ModelMapperのmap(source, destinationType)メソッドを使うと、sourceオブジェクトのプロパティ値を、destinationTypeで指定したクラスの新しいインスタンスの対応するプロパティにコピーします。プロパティ名が一致しない場合や、独自の変換ルールが必要な場合は、ModelMapperインスタンスに設定を追加することで対応できます。

ModelMapperのメリット:

  • 設定が簡単: プロパティ名が一致すれば設定なしで使える。
  • 実行時処理: 生成されるコードがなく、手軽に導入できる。
  • 柔軟な設定: プロパティ名の不一致、型の不一致、ネストしたオブジェクトなど、様々なケースに対応するための設定オプションが豊富。

ModelMapperのデメリット:

  • パフォーマンス: 実行時にリフレクションを使用するため、MapStructに比べてパフォーマンスが劣る場合がある。
  • コンパイル時のチェックがない: マッピング設定の誤りなどは実行時まで気づきにくい。
  • 設定が複雑になることも: デフォルトのマッピングで対応できない場合、設定コードが複雑になりがち。

4.3 MapStructの使い方

MapStructは、コンパイル時にマッピングコードを生成するライブラリです。アノテーションプロセッサとして機能し、定義したマッパーインターフェースからJavaのクラスを自動生成します。

4.3.1 依存関係の追加

pom.xmlにMapStructの依存関係と、アノテーションプロセッサの設定を追加します。

“`xml

org.mapstruct
mapstruct
1.5.5.Final

org.apache.maven.plugins
maven-compiler-plugin
3.8.1
${java.version}
${java.version}
org.mapstruct
mapstruct-processor
1.5.5.Final
org.mapstruct
mapstruct-spring
1.5.5.Final





-Amapstruct.defaultComponentModel=spring


“`

MapStructのSpring Integration(mapstruct-spring)と、defaultComponentModel=springのコンパイラ引数を設定することで、生成されるマッパー実装クラスがSpringのComponentとして登録されるようになり、DIで利用できます。

4.3.2 マッパーインターフェースの作成

MapStructでは、変換処理を定義するJavaインターフェースを作成します。このインターフェースに@Mapperアノテーションを付けます。

“`java
package com.example.myapp.infrastructure.mapper; // または application.mapper など

import com.example.myapp.domain.model.User;
import com.example.myapp.presentation.dto.UserDto;
import org.mapstruct.Mapper; // MapStructの@Mapperをインポート
import org.mapstruct.Mapping; // @Mappingをインポート
import org.mapstruct.factory.Mappers; // ファクトリクラス

// @Mapperアノテーション。componentModel=”spring” でSpringのComponentとして生成
@Mapper(componentModel = “spring”)
public interface UserMapperDto { // 名前の衝突を避けるためUserMapperとは別の名前に

// User -> UserDto への変換メソッド
// プロパティ名が一致する場合は@Mappingは不要
// UserDtoにはemailフィールドがないため、自動的に無視される
UserDto toDto(User user);

// UserDto -> User への変換メソッド
// DTOにないフィールド(例: id)は、生成されるEntityにはセットされない
User toEntity(UserDto userDto);

// プロパティ名が異なる場合のマッピング例
// @Mapping(source = "email", target = "userEmailAddress")
// UserDtoWithCustomEmail toDtoWithCustomEmail(User user);

// リストの変換も自動で行える
List<UserDto> toDtoList(List<User> users);
List<User> toEntityList(List<UserDto> userDtos);

// 更新系メソッド (既存オブジェクトにマッピングする場合)
// @Mapping(target = "id", ignore = true) // 更新時はIDを無視するなど
// void updateEntityFromDto(UserDto userDto, @MappingTarget User user);

}
“`

@Mapper(componentModel = "spring")アノテーションを付けると、MapStructはコンパイル時にこのインターフェースの実装クラスを生成し、Springの@Componentとして登録します。これにより、このインターフェースを他のクラスにDIできるようになります。

変換メソッドを定義します。メソッド名は任意ですが、toDto, toEntityなどが一般的です。引数と戻り値の型を指定するだけで、MapStructが自動的にマッピングコードを生成します。プロパティ名が一致する場合は@Mappingアノテーションは不要です。変換元にあり変換先にないプロパティは無視され、変換元になく変換先にのみあるプロパティはデフォルト値などがセットされます。

プロパティ名が一致しない場合や、特定のプロパティを無視したい場合は、@Mappingアノテーションを使います。

4.3.3 変換処理の実装

MapStructで生成されたマッパーインターフェースをサービス層などにDIし、定義した変換メソッドを呼び出して利用します。

“`java
package com.example.myapp.application.service;

import com.example.myapp.domain.model.User;
import com.example.myapp.infrastructure.repository.UserRepository; // または UserMapper
import com.example.myapp.infrastructure.mapper.UserMapperDto; // MapStruct Mapperをインポート
import com.example.myapp.presentation.dto.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Service
public class UserService {

private final UserRepository userRepository; // または UserMapper
private final UserMapperDto userMapperDto; // MapStruct MapperをDI

@Autowired
public UserService(UserRepository userRepository, UserMapperDto userMapperDto) {
    this.userRepository = userRepository;
    this.userMapperDto = userMapperDto;
}

@Transactional(readOnly = true)
public UserDto findUserDtoById(Long id) {
    Optional<User> userOptional = userRepository.findById(id); // JPAの場合
    // User user = userMapper.findById(id); // MyBatisの場合

    return userOptional.map(userMapperDto::toDto) // User -> UserDto 変換
                       .orElse(null);
    // または MyBatisの場合: return userMapperDto.toDto(user);
}

@Transactional(readOnly = true)
public List<UserDto> findAllUserDtos() {
    List<User> users = userRepository.findAll(); // JPAの場合
    // List<User> users = userMapper.findAll(); // MyBatisの場合

    // List<User> -> List<UserDto> 変換
    return userMapperDto.toDtoList(users); // リスト変換メソッドを利用
}

@Transactional
public UserDto createUserFromDto(UserDto userDto) {
    // UserDto -> User (Entity) 変換
    User user = userMapperDto.toEntity(userDto);
    // DTOに含まれないフィールドは、Entity側でデフォルト値やビジネスロジックで設定
    // 例: user.setCreatedAt(LocalDateTime.now());

    User savedUser = userRepository.save(user); // JPAの場合
    // int affectedRows = userMapper.insert(user); // MyBatisの場合

    // 保存後のEntity (自動生成IDなどがセットされている) を DTO に変換して返す
    return userMapperDto.toDto(savedUser);
}

// 更新や削除も同様に変換を適用

}
“`

MapStructで生成されたマッパーは、ModelMapperと同様にmap()のようなメソッドを持ちますが、メソッド名は自分で定義できます(例: toDto, toEntity)。MapStructはリストの変換メソッドも自動生成してくれるため、stream().map().collect()のようなコードを書く必要がなく、シンプルに記述できます。

MapStructのメリット:

  • パフォーマンス: コンパイル時にコードを生成するため、実行時のオーバーヘッドが少なく、ModelMapperに比べて高速。
  • コンパイル時のチェック: マッピングの誤りやプロパティ名の不一致などがコンパイル時に検出できるため、実行時エラーを防ぎやすい。
  • ボイラープレートコードの削減: 変換コードを手書きする手間が省ける。
  • DIとの相性: SpringのComponentとして生成されるため、DIが容易。

MapStructのデメリット:

  • ビルド時間: コンパイル時にコード生成が入るため、ビルド時間がわずかに増加する可能性がある。
  • 学習コスト: アノテーションや設定方法を学ぶ必要がある。
  • 生成コードの確認: 生成されたコードをデバッグなどで追う必要がある場合がある。

ModelMapper vs MapStruct

どちらのライブラリを選ぶかは、プロジェクトの要件やチームの好みによります。

  • 手軽さ・導入の早さ: シンプルなケースならModelMapperが少し有利。
  • パフォーマンス・型安全性: MapStructが有利。大規模なアプリケーションやパフォーマンスが重要な場合はMapStructが推奨されることが多い。
  • 複雑なマッピング: どちらも対応可能ですが、設定方法や記述スタイルが異なります。

近年のSpring Boot開発では、パフォーマンスと型安全性の観点からMapStructがより広く採用される傾向があります。

5. 実践的な考慮事項とベストプラクティス

5.1 トランザクション管理

データベース操作は、必ずトランザクション境界内で実行されるようにします。Spring Bootでは、サービス層のメソッドに@Transactionalアノテーションを付けるのが最も一般的な方法です。これにより、メソッド内の複数のデータベース操作(例: ユーザー作成と関連情報登録)が全て成功するか、全て失敗してロールバックされることが保証されます。

  • 読み取り操作: @Transactional(readOnly = true) を付けると、読み取り専用の最適化が行われる場合があります。
  • 書き込み操作: @Transactional (デフォルトは読み書き両用) を付けます。
  • 注意点: @Transactionalはpublicメソッドに付ける必要があります。また、同じクラス内の非publicメソッドからの呼び出しではトランザクションが開始されないため注意が必要です(AOPの仕組みによる制限)。

5.2 エラーハンドリング

データベースアクセス中に発生する可能性のある例外(例: 一意制約違反、データが見つからないなど)を適切に処理する必要があります。

  • Spring Data JPA: findByIdなどがOptionalを返すため、orElse(null)orElseThrow()を使ってデータNotFoundを扱えます。その他の永続化例外はDataAccessExceptionの子クラスとしてラップされます。
  • MyBatis: SQL実行中の例外はMyBatis独自の例外やJDBC例外として発生します。
  • サービス層での処理: 永続化層から発生した例外をサービス層でキャッチし、アプリケーション固有の例外に変換したり、適切なエラーメッセージをログに出力したりします。
  • プレゼンテーション層での処理: RESTful APIの場合、コントローラーや@ControllerAdviceを使って、例外を適切なHTTPステータスコード(例: 404 Not Found, 400 Bad Request, 500 Internal Server Error)とエラーレスポンスボディにマッピングします。

5.3 テストの書き方

MapperやRepositoryのテストは、データベースとの実際の連携を確認するために重要です。

  • MyBatis/JPA Repositoryテスト: Spring Boot Testの@DataJpaTest(JPAの場合)や@MybatisTest(MyBatisの場合)アノテーションを使用します。これらは、インメモリデータベース(H2など)を使ったテスト環境を自動的にセットアップし、関連するSpringコンポーネント(DataSource, EntityManager/SqlSessionなど)のみをロードして、高速にテストを実行できます。
  • DTOマッパーテスト: ModelMapperやMapStruct自体は単体テストしやすいコンポーネントです。MapStructの場合は、生成された実装クラスに対してテストを書くか、インターフェースを直接モック/スパイしてテストすることも可能です。

5.4 DTO設計の考慮事項

  • 用途に応じたDTO: 1つのエンティティに対して、表示用、登録用、更新用など、複数のDTOを定義することがあります。これにより、各画面やAPIが必要とするフィールドのみを持つ、シンプルで安全なDTOを設計できます。
  • バリデーション: 登録・更新用のDTOには、JSR 303 (Bean Validation) などのアノテーション(@NotNull, @Size, @Emailなど)を付けて、入力値のバリデーションを行います。これはプレゼンテーション層(コントローラー)で行うのが一般的です。
  • 関連データ: エンティティ間の関連(OneToMany, ManyToOneなど)をDTOでどう表現するか検討が必要です。単純にIDだけを持たせる、ネストしたDTOを含める、フラットな構造にするなど、API設計に合わせて決定します。

5.5 パフォーマンス考慮

  • N+1問題 (JPA): 関連エンティティをEager Loadingしすぎたり、Lazy Loadingされた関連をトランザクションの外でアクセスしたりすると発生します。必要に応じてJPQLでJOIN FETCHを使う、@EntityGraphを使う、Criteria APIを使うなどで、必要なデータを一度に取得するクエリを書く必要があります。
  • 適切なインデックス: データベーステーブルに適切なインデックスを張ることが、検索パフォーマンスに大きく影響します。
  • キャッシュ: 必要に応じて、Second Level Cache (Ehcache, Caffeineなど) や、クエリキャッシュの利用を検討します。Spring Data JPAやMyBatisはこれらの機能と連携できます。
  • SQLのチューニング (MyBatis): MyBatisを使っている場合は、発行されるSQL文をログ(spring.jpa.show-sql=true や MyBatisのロギング設定)で確認し、必要に応じてSQLを最適化します。

5.6 レイヤードアーキテクチャの徹底

  • 依存関係の方向: 上の層は下の層を知っていますが、下の層は上の層を知らないようにします(例: 永続化層はサービス層を知らない)。
  • 責務の分離: 各層が明確な責務を持つようにします。コントローラーはリクエスト/レスポンス処理、サービスはビジネスロジック、Mapper/Repositoryはデータアクセスに専念します。
  • DTOの活用: エンティティを永続化層の外側に露出させないように、DTOを積極的に活用します。

6. まとめ

この記事では、Spring Bootアプリケーション開発におけるMapperの役割と、その主要な実装方法について詳細に解説しました。

  • アプリケーションを層に分割するレイヤードアーキテクチャにおいて、Mapperはビジネスロジック層と永続化層の間を取り持つ重要なコンポーネントです。
  • 広義には、「データベースのデータ ⇔ Javaオブジェクト(エンティティ) のマッピング」と「異なるJavaオブジェクト(エンティティ ⇔ DTO) のマッピング」の両方を指します。
  • データベースマッピングの代表的な方法として、MyBatisSpring Data JPAを紹介しました。
    • MyBatisはSQL中心で、MapperインターフェースとXML/アノテーションによるSQL定義を使ってデータベースアクセスを行います。SQLを細かく制御したい場合に適しています。
    • Spring Data JPAはO/Rマッピング中心で、エンティティクラスとRepositoryインターフェースを使ってデータベースアクセスを行います。定型的な操作が多く開発効率を重視する場合に適しています。
  • DTOマッピングの自動化ツールとして、ModelMapperMapStructを紹介しました。これらはエンティティとDTO間の変換コードの手書きを削減し、生産性を向上させます。
    • ModelMapperは実行時に変換を行い、手軽に導入できます。
    • MapStructはコンパイル時に変換コードを生成し、パフォーマンスと型安全性に優れています。
  • 実践においては、トランザクション管理エラーハンドリングテストDTO設計パフォーマンス考慮、そしてレイヤードアーキテクチャの徹底が重要であることを説明しました。

これらのMapper関連技術を適切に使い分けることで、保守性が高く、効率的で、堅牢なSpring Bootアプリケーションを開発できるようになります。

まずは小規模なプロジェクトで実際にコードを書いてみて、それぞれのツールの挙動や特徴を体感してみてください。この記事が、あなたのSpring Boot開発におけるMapper理解の一助となれば幸いです。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール