0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaもPythonも触ってきた中堅エンジニアが、今さらSpring Bootに入門してみた【Spring Data JPA × PostgreSQL DB連携編】

0
Last updated at Posted at 2026-07-10

はじめに

前回([ローカル環境構築編])で、Mac M1 Proに環境を作って @RestController でHello Worldを返すところまでやりました。

次回は Spring Data JPAとPostgreSQLでDB連携 に進んでみます。

…と書いたので、有言実行です。

正直、ここからが本番という気がしています。
APIがメモリ上の文字列を返すだけなら大したことはないけれど、DBと繋がって初めて「アプリ」っぽくなるわけで。

PythonではSQLAlchemyやSQLModelでDBを触ってきたので、「ORMの概念」自体は分かっているつもり。
今回はその知識を踏まえつつ、Spring Data JPAがどれくらい楽をさせてくれるのかを確かめていきます。

ついでに前回「Lombokは次回以降で試す」と書いたので、今回しっかり使います。


対象読者

  • 前回の[ローカル環境構築編]を読んだ(or 同等の環境がある)
  • Java(Spring含む)は触ったことがある
  • PythonのORM(SQLAlchemy / SQLModelなど)を触ったことがある
  • Spring Data JPAはほぼ未経験
  • Mac M1/M2 Pro使用中

この記事のゴール

Task(タスク)テーブルに対して、**一覧取得・登録・更新・削除(CRUD)**ができるREST APIを作ります。

GET    /tasks       → 一覧取得
POST   /tasks       → 登録
GET    /tasks/{id}  → 1件取得
PUT    /tasks/{id}  → 更新
DELETE /tasks/{id}  → 削除

DBはPostgreSQL。Macに直接入れず、Dockerで立てる方針にします(M1でも素直に動くし、消すのも楽なので)。


環境

項目 バージョン
macOS Sonoma 14.x(M1 Pro)
Java 21.0.7-tem(Temurin)
Spring Boot 4.0.6
ビルドツール Maven
DB PostgreSQL 17(Docker)
ORM Spring Data JPA(Hibernate 7系)

補足:Spring Boot 4.x はJava 17以上が必須です。前回同様、広く使われている安定LTSのJava 21を使っています。バージョンは時期によって上がるので、sdk list java で最新パッチを確認してから入れてください。


1. PostgreSQLをDockerで立てる

まずはDBがないと話が始まりません。プロジェクト直下に docker-compose.yml を置きます。

# docker-compose.yml
services:
  db:
    image: postgres:17
    platform: linux/arm64   # M1の場合はこれを明示しておくと無駄なエミュレーションを避けられる
    container_name: springboot-postgres
    environment:
      POSTGRES_DB: demo
      POSTGRES_USER: demo_user
      POSTGRES_PASSWORD: demo_pass
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

起動します。

docker compose up -d

接続確認(任意)。

docker exec -it springboot-postgres psql -U demo_user -d demo
demo=# \l
demo=# \q

Pythonでいうと
docker compose up -d でDBを使い捨てにする感覚は、Python開発でもよくやるやつですね。ここはほぼ同じです。


2. 依存関係を追加する

前回作ったプロジェクトの pom.xml に、JPAとPostgreSQLドライバを追加します。

<!-- pom.xml の <dependencies> 内に追加 -->
<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>

Pythonでいうと
spring-boot-starter-data-jpapip install sqlalchemy 相当、postgresql ドライバが pip install psycopg2-binary 相当のイメージです。
違うのは、Springの「スターター」はJPA本体・Hibernate・コネクションプール(HikariCP)・トランザクション管理あたりをまとめて引き込んでくれる点。ここはMavenの便利なところです。

追加したら依存を反映します(IDEがやってくれることも多いですが、CLIなら)。

./mvnw clean compile

3. DB接続設定を書く

src/main/resources/application.properties に接続情報を書きます。
(YAML派の人向けに application.yml 版も後述します)

# src/main/resources/application.properties

# --- DB接続 ---
spring.datasource.url=jdbc:postgresql://localhost:5432/demo
spring.datasource.username=demo_user
spring.datasource.password=demo_pass

# --- JPA / Hibernate ---
# 開発中はupdateで自動生成。本番ではvalidate or noneにすること!
spring.jpa.hibernate.ddl-auto=update
# 実行されるSQLをログに出す(学習中は見えた方が理解が早い)
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

YAML派はこちら。

# src/main/resources/application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/demo
    username: demo_user
    password: demo_pass
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true

ddl-auto は要注意ポイント
update はEntityの定義に合わせてテーブルを勝手に作ってくれる便利モードですが、本番でやると事故ります。
学習・開発中は update、本番は validate(or none)+マイグレーションツール(Flyway / Liquibase)が定石、と覚えておけば十分です。
※パスワードを直書きしているのはあくまで学習用です。実際は環境変数や application-local.yml 等に逃がしましょう。


4. Entity(テーブルに対応するクラス)を作る

ここで前回お預けにしていたLombokが活きます。

src/main/java/com/example/demo/task/Task.java

package com.example.demo.task;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(name = "tasks")
@Getter
@Setter
@NoArgsConstructor
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private boolean done;
}

ポイントを整理します。

アノテーション 役割
@Entity このクラスをDBテーブルに対応させる
@Table(name="tasks") テーブル名を明示(省略するとクラス名から推測)
@Id 主キー
@GeneratedValue(IDENTITY) PostgreSQLの自動採番(連番)に任せる
@Getter / @Setter / @NoArgsConstructor Lombokがgetter/setter/コンストラクタを自動生成

Pythonでいうと
SQLAlchemyの宣言的マッピングそっくりです。

class Task(Base):
    __tablename__ = "tasks"
    id = Column(Integer, primary_key=True)
    title = Column(String)
    done = Column(Boolean, default=False)

@EntityBase 継承、@Idprimary_key=True に相当します。
ただしJavaは型が明示的な分、本来getter/setterで縦に長くなりがち。そこを Lombokが丸ごと潰してくれるので、SQLAlchemyに見劣りしないくらいスッキリします。前回「冗長に見える」と書いた懸念は、Lombokでかなり解消されました。

地味だけど大事な注意
Spring Boot 3以降、importは javax.persistence.* ではなく jakarta.persistence.* です。ネットの古い記事をコピペすると javax で詰まるので注意。


5. Repository(DBアクセス層)を作る

ここがSpring Data JPAの一番おいしいところです。

src/main/java/com/example/demo/task/TaskRepository.java

package com.example.demo.task;

import org.springframework.data.jpa.repository.JpaRepository;

public interface TaskRepository extends JpaRepository<Task, Long> {
    // ここに何も書かなくても、CRUDメソッドが揃っている
}

これだけ。
interface を1つ宣言するだけで、findAll() / findById() / save() / deleteById() などが実装ゼロで使えます。実体のクラスは書きません。Springが起動時に勝手に実装を生成して注入してくれます。

Pythonとの一番大きな差はここ
SQLAlchemyだと、CRUDのロジック(session.add()session.commit()session.query(...).all() …)は基本的に自分で書きますよね。
Spring Data JPAは、よくあるCRUDを「規約」で吸収してくれるので、定型コードがほぼ消えます。最初は「え、本当に動くの?」と疑うレベルで楽でした。

しかも、メソッド名から自動でクエリを組み立てる仕組みもあります。

public interface TaskRepository extends JpaRepository<Task, Long> {

    // SELECT * FROM tasks WHERE done = ? が自動生成される
    List<Task> findByDone(boolean done);

    // タイトル部分一致
    List<Task> findByTitleContaining(String keyword);
}

メソッド名を書くだけでSQLが組まれる、いわゆるクエリメソッドです。慣れると相当強力です。


6. Service(業務ロジック層)を作る

ControllerからRepositoryを直接呼んでもいいのですが、層を分けておくのがSpringの作法です。

src/main/java/com/example/demo/task/TaskService.java

package com.example.demo.task;

import java.util.List;
import org.springframework.stereotype.Service;

@Service
public class TaskService {

    private final TaskRepository repository;

    // コンストラクタインジェクション(Springが自動でrepositoryを渡してくれる)
    public TaskService(TaskRepository repository) {
        this.repository = repository;
    }

    public List<Task> findAll() {
        return repository.findAll();
    }

    public Task findById(Long id) {
        return repository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Task not found: " + id));
    }

    public Task create(Task task) {
        return repository.save(task);
    }

    public Task update(Long id, Task input) {
        Task task = findById(id);
        task.setTitle(input.getTitle());
        task.setDone(input.isDone());
        return repository.save(task); // 既存IDがあればUPDATEになる
    }

    public void delete(Long id) {
        repository.deleteById(id);
    }
}

Pythonでいうと
FastAPIで「routerにロジックを全部書かず、サービス関数に切り出す」あの分け方と同じ発想です。
final フィールド+コンストラクタ注入は、FastAPIの Depends() でDBセッションを受け取るのと役割が近いです(DIの思想は共通)。


7. Controller(REST API層)を作る

前回 @RestController でHello Worldを返した、あのControllerの本格版です。

src/main/java/com/example/demo/task/TaskController.java

package com.example.demo.task;

import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/tasks")
public class TaskController {

    private final TaskService service;

    public TaskController(TaskService service) {
        this.service = service;
    }

    @GetMapping
    public List<Task> list() {
        return service.findAll();
    }

    @GetMapping("/{id}")
    public Task get(@PathVariable Long id) {
        return service.findById(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Task create(@RequestBody Task task) {
        return service.create(task);
    }

    @PutMapping("/{id}")
    public Task update(@PathVariable Long id, @RequestBody Task task) {
        return service.update(id, task);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        service.delete(id);
    }
}

Pythonでいうと

@app.get("/tasks")
def list_tasks(): ...

@app.post("/tasks", status_code=201)
def create_task(task: TaskIn): ...

@RequestBody が FastAPIの「引数にPydanticモデルを取る」部分、@PathVariable/{id} のパスパラメータに相当します。アノテーションの思想は前回同様ほぼ一致していて、ここはすんなり入れました。


8. 動かして確認する

DB(Docker)が起動している状態で、アプリを起動します。

./mvnw spring-boot:run

起動ログに、Hibernateが tasks テーブルを作るSQL(create table tasks ...)が出ていれば成功です(ddl-auto=update の効果)。

別ターミナルから叩いてみます。

# 登録
curl -X POST http://localhost:8080/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "JPAを学ぶ", "done": false}'
{"id":1,"title":"JPAを学ぶ","done":false}
# 一覧取得
curl http://localhost:8080/tasks
[{"id":1,"title":"JPAを学ぶ","done":false}]
# 更新(完了にする)
curl -X PUT http://localhost:8080/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"title": "JPAを学ぶ", "done": true}'
# 削除
curl -X DELETE http://localhost:8080/tasks/1

コンソールに実際のSQL(insert into tasks ... / select ... from tasks など)が流れているはずです。show-sql=true にしておくと、ORMが裏で何をしているかが見えて勉強になります。


JPAが裏で何をしているか(ざっくり)

repository.save() を呼んだだけでなぜSQLが走るのか」を一度押さえておくと、後でハマりません。

まず、リクエストが通る「層」の全体像です。

jpa_layers_flow.png

自分で書くのは Controller / Service / Repository の3つだけ。
Repository の中身(実装)と、その先の Hibernate ⇔ DB のやり取りは、Springがまるごと面倒を見てくれるのが分かります。

次に、POST /tasks(登録)を1本叩いたときに裏で何が起きているかを時系列で。

jpa_save_sequence.png

ポイントは save() ひとつで INSERTにもUPDATEにもなるところ。idの有無でHibernateが判断しています。

@Entity のオブジェクトと、テーブルの「行」を相互変換しているのがHibernate(JPA実装)です。
SQLAlchemyでいう「セッション」「フラッシュ」「マッピング」を、Springがかなりの部分自動で面倒見てくれている、という理解でだいたい合っています。


M1 Pro特有の注意点

前回も触れましたが、DBが絡むと改めて気にする箇所が出ます。

  • PostgreSQLイメージpostgres の公式イメージはARM64対応ビルドがあるので、M1でもネイティブで動きます。platform: linux/arm64 を明示しておくと、意図しないエミュレーション起動を避けられます。
  • ポート競合:Macに別途PostgreSQLを入れている人は 5432 が衝突しがち。その場合は "15432:5432" のようにホスト側ポートをずらし、接続URLも合わせて変更してください。
  • ボリュームの後始末:データを完全に消してやり直したいときは docker compose down -v-v でボリュームごと削除)。学習中は気軽にリセットできて便利です。

ハマりポイントメモ

実際に詰まった/詰まりそうな箇所をメモしておきます。

症状 原因 / 対処
Cannot resolve jakarta.persistence javax でimportしている。Spring Boot 3以降は jakarta.*
起動時に Connection refused DockerのDBが起動しきっていない。docker compose ps で確認
テーブルが作られない ddl-autonone になっている。学習中は update
getter/setterが見つからないとビルドエラー IDEのLombokプラグイン未導入 or アノテーションプロセッサ未有効
起動はするがSQLが見えない spring.jpa.show-sql=true を入れる

まとめ

やったこと 内容
DB準備 PostgreSQLをDockerで起動(M1向けに linux/arm64 指定)
依存追加 spring-boot-starter-data-jpa + PostgreSQLドライバ
接続設定 application.properties でDB接続&ddl-auto
Entity @Entity + Lombokでテーブル対応クラスを定義
Repository JpaRepository 継承だけでCRUDが揃う
Service / Controller 層を分けてREST APIでCRUDを公開

一番の感想は、**「Repositoryのinterface書くだけでCRUD全部揃うの、ズルくない?」**でした。
SQLAlchemyだと自分で書いていた定型処理が、Spring Data JPAでは規約に吸収されてごっそり消える。ここはPython経験者ほど「楽になった感」が分かりやすいと思います。

一方で、ddl-auto の挙動やトランザクション境界など、「自動でやってくれる部分の中身」を知らないと本番でハマるな、というのも見えてきました。便利さと引き換えに、裏側の理解は別途必要そうです。


次回予告

CRUDはできたものの、いまのコードはバリデーションも例外ハンドリングもガバガバです。
title が空でも登録できるし、存在しないIDを叩くと500が返ります。

次回は Bean Validationでの入力チェック + 例外ハンドリング(@ControllerAdvice を入れて、もう少し「ちゃんとしたAPI」に寄せていきます。


参考


この記事はシリーズの第2回です。
【前回】ローカル環境構築編 / 【次回】Bean Validation × 例外ハンドリング編

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?