見出し画像

3分間プログラミング講座 第14回:O/RマッピングとDI ~Spring Data JPAでデータベース連携~



登場人物:

  • チャット先生(通称:先生): オブジェクト指向マスター。普段は優しく、たまにユーモアも交えながら解説。

  • ボット助手(通称:ボット君): プログラミングを始めたばかりの若手。素朴な疑問を投げかけるのが得意。


ボット君: 先生!前回のDI、本当にすごかったです!プログラムが賢く連携し合う仕組み、感動しました!これでどんな変更にも対応できそうな気がしてきました!

先生: その調子だ、ボット君!DIはSpring Frameworkの強力な武器の一つで、システムの柔軟性を格段に高めてくれる。これで君は、クラス同士の「連携」の魔法を手に入れたわけだ。しかし、アプリケーションはただロジックを動かすだけじゃない。作ったデータや情報を「保存」して、次回も使えるようにする必要があるよね?

ボット君: はい!ゲームのセーブデータとか、オンラインショップの注文履歴とか、データが残るからこそ便利です!

先生: その通り!そして、そのデータを保存する場所が「データベース」だ。今日のテーマは、このデータベースとJavaのプログラムをスムーズに連携させるための技術「O/Rマッピング」と、それをSpring Frameworkで簡単に実現する「Spring Data JPA」だ!もちろんDIも活用していくぞ!

ボット君: データベース!ついにデータが保存できるようになるんですね!楽しみです!



1. O/Rマッピングとは? ~オブジェクトとデータベースの橋渡し~



先生: まず、「O/Rマッピング」という言葉を解説しよう。これは「Object-Relational Mapping」の略で、Javaのようなオブジェクト指向言語の「オブジェクト」と、リレーショナルデータベースの「テーブル(行と列のデータ)」とを、自動的に対応させる技術のことだ。

ボット君: オブジェクトとテーブルを対応させる…?どういうことですか?

先生: そうだね、JavaではTaskというクラスを作って、そのインスタンスがタスクの情報を保持する。データベースでは、tasksというテーブルを作って、そこにタスクのデータを保存する。

  • Javaのオブジェクトは「タスクのインスタンス」

  • データベースのテーブルの1行は「タスクのデータ」

これらを、手動でSQL文を書いて変換するのは大変だ。

Java

// Javaオブジェクトのイメージ
class Task {
    Long id;
    String title;
    String description;
    boolean completed;
    // ...
}

SQL

-- データベーステーブルのイメージ (PostgreSQL)
CREATE TABLE tasks (
    id BIGSERIAL PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    description TEXT,
    is_completed BOOLEAN DEFAULT FALSE
    -- ...
);

先生: O/Rマッピングは、このJavaのオブジェクトとデータベースのテーブルの間で、データの変換を自動的に行ってくれる仕組みなんだ。

ボット君: じゃあ、僕たちがSQLを直接書かなくても、オブジェクトを操作するだけでデータベースに保存したり、読み込んだりできるようになるんですか?

先生: その通り!それがO/Rマッピングの最大のメリットだ。開発者はオブジェクト指向に集中でき、データベースとの連携が格段に楽になる。



2. Spring Data JPAとは? ~Springが提供する強力なO/Rマッピング~



先生: さて、JavaでO/Rマッピングを実現するための標準的な技術として「JPA (Java Persistence API)」というものがある。そして、Spring Frameworkは、このJPAをさらに使いやすく、シンプルに使えるようにした「Spring Data JPA」というモジュールを提供しているんだ。

ボット君: また「Spring」がついてますね!Springって何でもやってくれるんですね!

先生: そうなんだ!Spring Data JPAを使えば、データベースの操作(データの保存、更新、検索、削除など)を、ほとんどSQLを書かずに実現できる。インターフェースを定義するだけで、Springが自動で必要な実装を提供してくれるんだ。



Spring Data JPA実践例:Task管理アプリケーション



まずは、PostgreSQLデータベースを準備しよう。


A. 依存関係(pom.xml)



Spring BootアプリケーションでPostgreSQLとSpring Data JPAを使うには、以下の依存関係をpom.xmlに追加する。

XML

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>


B. データベース設定 (application.yml)



次に、src/main/resources/application.ymlにPostgreSQLへの接続情報を設定する。

YAML

# src/main/resources/application.yml

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/your_database_name # ① データベースURL (適宜変更)
    username: your_username # ② データベースユーザー名 (適宜変更)
    password: your_password # ③ データベースパスワード (適宜変更)
    driver-class-name: org.postgresql.Driver # PostgreSQLのJDBCドライバー
    # 接続プールの設定 (HikariCP) - 本番環境では重要なチューニングポイント
    hikari:
      maximum-pool-size: 10 # プール内の最大接続数
      minimum-idle: 5       # プール内のアイドル状態の最小接続数

  jpa:
    hibernate:
      ddl-auto: update # ④ アプリケーション起動時にDBスキーマを自動更新
      # 注意:この設定は開発環境でのみ使用すること。本番環境では以下を推奨:
      # - validate: スキーマの検証のみ行い、変更は行わない
      # - - none: 何もしない(手動でのDBマイグレーション推奨)
    show-sql: true # ⑤ 実行されるSQLをログに出力
    properties:
      hibernate:
        format_sql: true # ⑤ 出力されるSQLを整形する
        dialect: org.hibernate.dialect.PostgreSQLDialect # ⑥ 使用するデータベースの方言を明示的に指定
  profiles:
    active: dev # デフォルトで'dev'プロファイルをアクティブにする(後述)
  • your_database_name、your_username、your_passwordは、君が実際に使用するPostgreSQLのデータベース名、ユーザー名、パスワードに置き換えてくれ。

  • ddl-auto: updateは、JPAのエンティティクラス(後述)の変更に合わせて、データベースのテーブルスキーマを自動的に更新してくれる設定だ。開発時には非常に便利だが、本番環境ではvalidate(スキーマがエンティティと一致するか検証するが変更はしない)やnone(何もしない)を使うなど、慎重な運用が必要だよ。

  • hikariの設定は、データベース接続の管理を行う「接続プール」に関するものだ。大量のリクエストを効率的に処理するために重要な設定になる。


C. エンティティクラス(Task.java)



先生: O/Rマッピングの「O(オブジェクト)」にあたる部分が、この「エンティティクラス」だ。これは、データベースのテーブルの構造をJavaのクラスで表現したものになる。Lombokの@Dataアノテーションを使うことで、Getter/Setterなどを自動生成し、記述を大幅に減らせるぞ。

Java

// src/main/java/com/example/demo/Task.java (パッケージ名は適宜変更)
import jakarta.persistence.*; // JPAのアノテーションをインポート
import java.time.LocalDateTime;
import org.hibernate.annotations.CreationTimestamp; // タイムスタンプ自動設定用
import org.hibernate.annotations.UpdateTimestamp;  // タイムスタンプ自動設定用
import lombok.Data;             // Lombokのアノテーション: Getter/Setter/equals/hashCode/toString を自動生成
import lombok.NoArgsConstructor; // Lombokのアノテーション: 引数なしコンストラクタを自動生成(JPAに必須)

@Entity // ① このクラスがJPAのエンティティ(データベーステーブルに対応)であることを示す
@Table(name = "tasks") // ② 対応するテーブル名を指定 (クラス名と異なる場合)
@Data // Lombokにより、Getter, Setter, equals(), hashCode(), toString() が自動生成される
@NoArgsConstructor // Lombokにより、引数なしコンストラクタが自動生成される(JPAに必須)
public class Task {

    @Id // ③ プライマリキーであることを示す
    @GeneratedValue(strategy = GenerationType.IDENTITY) // ④ IDの生成戦略(PostgreSQLではIDENTITYが一般的)
    private Long id;

    @Column(nullable = false) // ⑤ NOT NULL制約を設定
    private String title;

    @Column(columnDefinition = "TEXT") // ⑥ データベースのカラム定義を直接指定
    private String description;

    @Column(name = "is_completed") // ⑦ カラム名とフィールド名が異なる場合
    private boolean completed = false; // デフォルト値を設定

    @CreationTimestamp // ⑧ レコード作成時に自動で現在日時を設定
    private LocalDateTime createdAt;

    @UpdateTimestamp // ⑨ レコード更新時に自動で現在日時を設定
    private LocalDateTime updatedAt;

    // 特定のコンストラクタが必要な場合のみ追加
    public Task(String title, String description) {
        this.title = title;
        this.description = description;
    }

    // Lombokにより上記メソッドは自動生成されるため、明示的に記述する必要はなくなる
}

ボット君: jakarta.persistenceって書いてあります!たくさんアノテーションがありますね!@Entityとか@Idとか、何だかデータベースのカラムと対応してるみたいです!Lombokの@Dataって便利ですね!コードがすごく短くなりました!

先生: その通り!これがJPAのアノテーションとLombokの力だ。

  • @Entity: このJavaクラスがデータベースのテーブルに対応する「エンティティ」であるとJPAに教える。

  • @Table(name = "tasks"): 対応するテーブル名を指定。クラス名と同じなら省略可能。

  • @Id: フィールドがテーブルのプライマリキーであることを示す。

  • @GeneratedValue(strategy = GenerationType.IDENTITY): プライマリキーの自動生成方法を指定。IDENTITYはPostgreSQLなどで、データベース側で自動採番されるSERIAL型などに対応する。

  • @Column: カラムに関する詳細を設定。nullable = falseでNOT NULL制約、columnDefinition = "TEXT"でデータ型を指定できる。

  • @CreationTimestamp, @UpdateTimestamp: Hibernate(JPAの実装の一つ)の機能で、レコードの作成日時と更新日時を自動で設定してくれる。

  • @Data (Lombok): これ一つで、Getter, Setter, equals(), hashCode(), toString()メソッドを自動的に生成してくれる。

  • @NoArgsConstructor (Lombok): 引数なしのコンストラクタを自動生成。JPAはエンティティのインスタンス生成に引数なしコンストラクタを必要とするため、必須のアノテーションだ。


D. リポジトリインターフェース(TaskRepository.java)



先生: 次に、データベース操作を行うための「リポジトリ」だ。Spring Data JPAでは、このリポジトリをインターフェースとして定義するだけで、ほとんどの基本的なデータベース操作が自動で使えるようになる。

Java

// src/main/java/com/example/demo/TaskRepository.java (パッケージ名は適宜変更)
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 java.time.LocalDateTime;
import java.util.List;

@Repository // ① Springにこのインターフェースがリポジトリであることを教える
public interface TaskRepository extends JpaRepository<Task, Long> { // ② JpaRepositoryを継承する

    // --- Spring Data JPAが自動生成してくれるメソッドの例 ---
    // Spring Data JPAはメソッド名からSQLを自動生成する(命名規則に従う)
    List<Task> findByCompletedFalse(); // 完了していないタスクを検索
    List<Task> findByTitleContainingIgnoreCase(String title); // タイトルに特定の文字列を含むタスクを大文字小文字無視で検索
    boolean existsByTitle(String title); // ③ タイトルが存在するかどうかをチェックするカスタムメソッド

    // --- カスタムクエリの例 ---
    // @Queryアノテーションを使って、任意のJPQL(Java Persistence Query Language)やネイティブSQLを書くこともできる
    @Query("SELECT t FROM Task t WHERE t.createdAt >= :startDate ORDER BY t.createdAt DESC")
    List<Task> findTasksCreatedAfter(@Param("startDate") LocalDateTime startDate);

    // 特定のIDのタスクを完了済みに更新する(更新系は@Modifyingと@Transactionalが必要)
    // @Modifying
    // @Transactional
    // @Query("UPDATE Task t SET t.completed = true WHERE t.id = :id")
    // int markTaskAsCompleted(@Param("id") Long id);
}

ボット君: え!これだけでデータベースの操作ができるんですか!?save()とかfindById()とかfindAll()とか、どこにも書いてないのに!existsByTitleなんてメソッド名だけで作れるなんて!

先生: まさにそれがSpring Data JPAの魔法だ!JpaRepositoryを継承するだけで、以下の基本的なCRUD(Create, Read, Update, Delete)操作のメソッドが自動的に提供されるんだ。これらのメソッドは、JpaRepositoryがCrudRepositoryとPagingAndSortingRepositoryを継承しているため利用できる。

  • save(entity): エンティティの保存(新規作成または更新)

  • findById(id): IDでエンティティを検索

  • findAll(): 全てのエンティティを取得

  • deleteById(id): IDでエンティティを削除

  • ...その他多数

さらに、findByCompletedFalse()やexistsByTitle()のように、特定の命名規則に従ったメソッド名をインターフェースに定義するだけで、Spring Data JPAが対応するJPQLクエリを自動で生成してくれるんだ。ただし、複雑な条件の場合は@Queryアノテーションでカスタムクエリを記述することも可能だ。 命名規則には以下のようなキーワードがある:

  • findBy, findAllBy - 検索

  • countBy - 件数取得

  • existsBy - 存在確認

  • deleteBy, removeBy - 削除

ボット君: すごい!SQLを書かなくていいなんて!これは本当に便利です!


E. サービス層での利用(TaskService.java)とDI



先生: 最後に、前回のDIで学んだサービス層から、このTaskRepositoryを利用する例を見てみよう。

Java

// src/main/java/com/example/demo/TaskService.java (パッケージ名は適宜変更)
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; // トランザクション管理用

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional; // 検索結果がnullの場合の安全なハンドリング用

@Service // Springにサービス層のコンポーネントであることを教える
public class TaskService {

    private final TaskRepository taskRepository; // TaskRepositoryへの依存

    // コンストラクタインジェクション:SpringがTaskRepositoryのインスタンスを注入する
    public TaskService(TaskRepository taskRepository) {
        this.taskRepository = taskRepository;
        System.out.println("TaskServiceが初期化され、TaskRepositoryが注入されました。");
    }

    @Transactional // ① メソッド全体をトランザクション管理下に置く
    public Task addTask(Task task) {
        System.out.println("新しいタスクを追加: " + task.getTitle());
        return taskRepository.save(task); // ② saveメソッドでデータベースに保存
    }

    @Transactional(readOnly = true) // ③ 読み取り専用トランザクション
    public List<Task> getAllTasks() {
        System.out.println("全てのタスクを取得します。");
        return taskRepository.findAll(); // ④ 全てのタスクを取得
    }

    @Transactional(readOnly = true)
    public Optional<Task> getTaskById(Long id) {
        if (id == null) {
            throw new IllegalArgumentException("IDがnullです"); // 例外処理の追加
        }
        System.out.println("ID: " + id + "のタスクを検索します。");
        return taskRepository.findById(id); // ⑤ IDでタスクを検索
    }

    @Transactional
    public Task updateTask(Long id, Task updatedTask) {
        System.out.println("ID: " + id + "のタスクを更新します。");
        return taskRepository.findById(id).map(task -> { // ⑥ 既存タスクを取得し、存在すれば更新
            task.setTitle(updatedTask.getTitle());
            task.setDescription(updatedTask.getDescription());
            task.setCompleted(updatedTask.isCompleted());
            return taskRepository.save(task); // ⑦ 更新内容を保存
        }).orElseThrow(() -> new RuntimeException("Task not found with id " + id)); // 見つからなければ例外
    }

    @Transactional
    public void deleteTask(Long id) {
        System.out.println("ID: " + id + "のタスクを削除します。");
        taskRepository.deleteById(id); // ⑧ IDでタスクを削除
    }

    @Transactional(readOnly = true)
    public List<Task> getIncompleteTasks() {
        System.out.println("未完了のタスクを取得します。");
        return taskRepository.findByCompletedFalse(); // ⑨ 自動生成されたメソッドを呼び出し
    }

    @Transactional(readOnly = true)
    public List<Task> getTasksCreatedAfter(LocalDateTime date) {
        System.out.println("指定日時以降に作成されたタスクを取得します。");
        return taskRepository.findTasksCreatedAfter(date); // ⑩ カスタムクエリのメソッドを呼び出し
    }

    @Transactional(readOnly = true)
    public boolean existsByTitle(String title) {
        System.out.println("タイトル '" + title + "' の存在を確認します。");
        return taskRepository.existsByTitle(title); // ⑪ 自動生成された存在チェックメソッドを呼び出し
    }
}

ボット君: なるほど!TaskServiceがTaskRepositoryに依存してて、それが自動で注入されるってことですね!そして、save()とかfindAll()とか使うだけで、データベースにアクセスできるなんて、すごい組み合わせです!トランザクションも自動でやってくれるなんて、本当に便利です!



今日のまとめ!



  • O/Rマッピング: オブジェクト指向言語のオブジェクトと、リレーショナルデータベースのテーブルを自動的に対応させる技術。

  • JPA (Java Persistence API): JavaにおけるO/Rマッピングの標準仕様。

  • Spring Data JPA: JPAをSpring Frameworkでよりシンプルに、強力に利用するためのモジュール。

    • エンティティクラス:@Entity、@IdなどのJPAアノテーションとLombokの@Data、@NoArgsConstructorを使って、Javaオブジェクトをデータベーステーブルにマッピングする。

    • リポジトリインターフェース:JpaRepositoryを継承するだけで、基本的なCRUD操作メソッドが自動で提供される。これらのメソッドは、JpaRepositoryがCrudRepositoryとPagingAndSortingRepositoryを継承しているため利用できる。 メソッド名ルールに従うことで、カスタム検索メソッドも自動生成される。@Queryアノテーションで独自のJPQLやSQLも記述可能。

    • 命名規則: findBy、countBy、existsBy、deleteBy、removeByなどのキーワードを使ってメソッドを定義すると、Spring Data JPAが自動でJPQLクエリを生成する。

    • DIとの連携: サービス層がリポジトリインターフェースに依存することで、具体的なデータベースアクセス実装から切り離され、テスト容易性や保守性が向上する。

    • application.yml: PostgreSQLなどのデータベース接続情報や、HikariCPによる接続プール設定、Hibernateのデータベース方言などを設定。ddl-auto設定は開発環境でのみupdateを推奨し、本番環境ではvalidateまたはnoneを使用する。

    • @Transactional: データベース操作におけるトランザクション管理をSpringが自動で行う。


先生: ボット君、これで君はついに、Javaアプリケーションでデータを永続化する強力な武器を手に入れたことになる!これまでのオブジェクト指向の概念とSpringのDI、そして今日のO/Rマッピングが組み合わさることで、本格的なアプリケーション開発の基礎が完成するぞ。

ボット君: やったー!これで僕の作ったデータが、パソコンの電源を切っても残るようになるんですね!感動です!

先生: その調子だ!次回は、本格的なWebアプリケーション開発の基盤となる「3層分離アーキテクチャ」について学んでいこう。プレゼンテーション層、サービス層、データアクセス層という考え方を理解することで、君のアプリケーションはさらに整理され、大規模な開発にも対応できるようになるぞ!

ボット君: 3層分離!また新しい言葉が出てきました!楽しみです!ありがとうございました!


次回:第15回: 3層分離アーキテクチャ ~大規模アプリ開発の基本構造~

#オブジェクト指向 #プログラミング初心者 #Java #クラスとメソッド #プログラミング学習 #入門


いいなと思ったら応援しよう!

ナオユキ|物語と知識のコンシェルジュ もしこの記事が少しでも役に立ったり、楽しんでいただけたりしたら、チップをいただけるととても嬉しいです! いただいたチップは、今後のより良いコンテンツ制作のための書籍代📚とnote購入🗒️、他の方へのチップ代💰に使わせていただきます。 いつも応援ありがとうございます!