🐥

docker-compose 下で Java + Spring Boot + PostgreSQL (Spring Data JDBC編)

2020/12/25に公開

TL;DR

docker-compose.yml を用意する

cd your_project
mkdir server
touch docker-compose.yml
docker-compose.yml
version: '3.6'
services:
  app:
    image: openjdk:15
    container_name: app
    ports:
      - 8080:8080
    tty: true
    volumes:
      - ./server:/srv:cached
    working_dir: /srv
    depends_on:
      - db

  adminer:
    image: adminer:4.7.8
    container_name: adminer
    ports:
      - "9000:8080"
    depends_on:
      - db

  db:
    image: postgres:13.1
    container_name: db
    environment:
      POSTGRES_USER: "root"
      POSTGRES_PASSWORD: "root"
      POSTGRES_DB: "dev"
    ports:
      - "5432:5432"
    volumes:
      - dbvol:/var/lib/postgresql/data
      - ./forDocker/db/initdb:/docker-entrypoint-initdb.d

volumes:
  dbvol:

DB初期投入クエリの準備

postgres の Docker イメージは前述の DockerHub ページに書いてあるように、
/docker-entrypoint-initdb.d に用意したファイルを
サービス開始前に実行してくれます。(MySQL の Docker イメージにも同様の機能がありますね)

このクエリは /var/lib/postgresql/data を生成するときに実行されるため、
再度実行させたい時は
今回の docker-compose.yml であれば docker volume を割り当てているので
dbvoldocker volume rm ... で削除してやれば再度実行させることができます。

ではその前提で、DB初期投入クエリの準備をしていきましょう。

mkdir -p forDocker/db/initdb
touch forDocker/db/initdb/1_create_users.sql
touch forDocker/db/initdb/2_insert_users.sql
forDocker/db/initdb/1_create_users.sql
CREATE TABLE users (
  id SERIAL,
  name VARCHAR(255) NOT NULL,
  PRIMARY KEY (id)
);
forDocker/db/initdb/2_insert_users.sql
INSERT INTO users (name) values ('test');

ファイル名順に実行してくれるのでファイル名にシーケンスを振っています。

Gradleプロジェクト作成

今回も Spring Initializr というサイトで作ってしまいます。

PostgreSQL Driver, Spring Data JDBC などを Dependencies に追加するのを忘れないようにしてください。今回はこの Spring Data JDBC を使用します。

https://github.com/spring-projects/spring-data-jdbc


Spring JDBC と Spring Data JDBC について

その他 Java での DB アクセスパッケージについては以下が詳しいです


入力が終わったら GENERATE してダウンロードします。
ダウンロードしたら展開したものを
your_project/server 配下に配置します。

server/src/main/java/com/example/api/ApiApplication.java はダウンロードしてきたままでOK.

server/src/main/java/com/example/app/AppApplication.java
package com.example.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AppApplication {

	public static void main(String[] args) {
		SpringApplication.run(AppApplication.class, args);
	}

}

server/src/main/resources/application.properties を以下のように編集します。

server/src/main/resources/application.properties
spring.datasource.url=jdbc:postgresql://db:5432/dev
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=none

Spring Data JDBC で必要な Entity と Repository を作成

Spring Data JDBC では色んな方法でデータベースアクセスが定義できるのですが、
今回は最も簡易で Spring Data JPA との違いがわかりやすい
Entity と Repository を利用してアクセスする方法を使いましょう。

# server/src/main/java/com/example/app までは Spring Initializr からDLしたものを展開した時点で存在するはずです
mkdir server/src/main/java/com/example/app/entity
touch server/src/main/java/com/example/app/entity/User.java
mkdir server/src/main/java/com/example/app/repository
touch server/src/main/java/com/example/app/repository/UserRepository.java
server/src/main/java/com/example/app/entity/User.java
package com.example.app.entity;

import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Table("users") 
public class User {

  @Id
  private Long id;
  private String name;
}

今回は Entity の作成に Lombok を利用しています。
Lombok はアノテーションを付けるだけで、 getter, setter, toString, equals などの「何度も繰り返し書くコード」をコンパイル時に自動生成してくれるようになる便利なパッケージです。
詳しくは以下の解説がわかりやすいです。

また、今回は Entity と実際のテーブル名とのマッピングに @Table を使っていますが
これは Spring Data JDBC で定義されたアノテーションです。

server/src/main/java/com/example/app/repository/UserRepository.java
package com.example.app.repository;

import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.example.app.entity.User;

@Repository
public interface UserRepository extends CrudRepository<User, Long> {

  @Query(" SELECT" 
       + " COUNT(*)"
       + " FROM users"
       + " WHERE"
       + " id >= 5 ")
  long countOverFive();
}

DI を利用して User にアクセスする Controller を作成

Spring の強力なDIを使って Controller を用意します。

mkdir server/src/main/java/com/example/app/controller
touch server/src/main/java/com/example/app/controller/UserController.java
server/src/main/java/com/example/app/controller/UserController.java
package com.example.app.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.app.entity.User;
import com.example.app.repository.UserRepository;

@RestController
public class UserController {

  private final UserRepository repository;

  @Autowired
  public UserController(UserRepository repository) {
    this.repository = repository;
  }

  @GetMapping("/")
  public Iterable<User> user() {
    // CrudRepository の メソッドが使える
    return repository.findAll();
  }

  @PostMapping("/")
  public User create(@RequestBody User user) {
    // CrudRepository の メソッドが使える
    var savedUser = repository.save(user);
    return savedUser;
  }

  @RequestMapping("/count")
  public String count() {
    // CrudRepository の メソッドが使える
    return String.valueOf(repository.count());
  }

  @RequestMapping("/countoverfive")
  public String countOverFive() {
    // 新しく定義したクエリも呼び出せる
    return String.valueOf(repository.countOverFive());
  }
}

@Autowired をつけることで Spring の DI 機能により new して突っ込んでくれます。
以下などが分かりやすかったです。

@RestController と @RequestMapping("/") は以下記事でも扱いましたね。
本来であれば Repository を扱う Domain Model(Service) を作った方がいいんですが今回は割愛しました。

Docker コンテナを起動してGradleビルド, アプリケーション起動, リクエストを投げてみる

ビルドする前に server/build.gradle を編集してバージョンを抜いてみましょう。
開発中は特に不要と思いますので。

server/build.gradle
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '15'

ここを、こう

server/build.gradle
group = 'com.example'
sourceCompatibility = '15'

ではコンテナを起動してビルド、Spring アプリケーションを起動します。

docker-compose up -d
# DB が立ち上がって初期化されるまでちょっとかかるのでちょっと待つ
docker-compose exec app bash
bash-4.4# sh gradlew build
...
BUILD SUCCESSFUL in 1m 41s
5 actionable tasks: 5 executed
# できてるのを確認
bash-4.4# ls build/libs/
app.jar
bash-4.4# java -jar build/libs/app.jar

起動したらリクエストを投げてみましょう。

$ curl http://localhost:8080 -X GET
[{"id":1,"name":"test"}
$ curl http://localhost:8080 -X POST -H "Content-Type:application/json" -d '{ "name":"foo" }'
{"id":2,"name":"foo"}
$ curl http://localhost:8080 -X POST -H "Content-Type:application/json" -d '{ "name":"bar" }'
{"id":3,"name":"bar"}
$ curl http://localhost:8080 -X POST -H "Content-Type:application/json" -d '{ "name":"baz" }'
{"id":4,"name":"baz"}
$ curl http://localhost:8080 -X POST -H "Content-Type:application/json" -d '{ "name":"hoge" }'
{"id":5,"name":"hoge"}
$ curl http://localhost:8080/count -X GET
5
$ curl http://localhost:8080/countoverfive -X GET
1

OK ですね!
アプリケーションの終了は例によって Ctrl+c です。
コンテナから出るのは exit, コンテナの停止は docker-compose down で。

Spring Data REST を使うよりはもちろん記述量は増えるのですが、
より柔軟な書き方ができるのが伝わったのではないでしょうか。
Spring Data JPA などの ORM は複雑なリレーションシップがある場合に
N+1 問題などとの戦いが発生するので、
SQLを直接扱えるのは個人的には好みな感じです。

今回のリポジトリはこちらです。
https://github.com/JUNKI555/java_spring_boot_practice03

参考

Discussion