Java, Play2.8 簡易Todoアプリ

本記事はJava, Play Framework(v2.8)で簡単なTodoアプリ作成を通して、Playを学んでいくという記事になります。

環境構築

まず初めに環境構築を行っていきます。

backend

jdkのイメージにsbtをインストールしたdockerファイルを作成します。
sbtはビルドツールになります。

FROM openjdk:8-jdk

ARG SBT_VERSION

# Envs
ENV SBT_VERSION ${SBT_VERSION:-1.3.7}

# Update
RUN apt-get update && \
    apt-get upgrade -y

# Install sbt
RUN curl -L -o sbt-$SBT_VERSION.deb https://dl.bintray.com/sbt/debian/sbt-$SBT_VERSION.deb && \
    dpkg -i sbt-$SBT_VERSION.deb && \
    rm sbt-$SBT_VERSION.deb && \
    apt-get update && \
    apt-get install sbt

COPY ./ /server
WORKDIR /server

RUN sbt about

上のdockerイメージをビルドしていきます。

$ mkdir backend
$ cd backend
$ ls
Dockerfile
$ docker build  -t some-image-name .

ビルドが終わったらコンテナ内で作業していきます。

$ docker run -it -p 9000:9000 -v `pwd`/:/server sweeep-timestamp_play /bin/bash

Playプロジェクトを作成します。 途中でプロジェクト名を聞かれますので、今回はappを入力しています。

$ sbt new playframework/play-java-seed.g8

プロジェクトの作成が終わったら以下のコマンドを入力して、 ブラウザで確認してみます。

$ cd app
$ sbt run

次にホストの設定とDB接続のための設定を行います。

app/conf/applicaiton.confに以下を追加します。 本来ならdb設定を環境変数に、ホスト設定もしっかりやるべきですが、今回は割愛します。

# This is the main configuration file for the application.
# https://www.playframework.com/documentation/latest/ConfigFile

db {
    default.driver = com.mysql.jdbc.Driver
    default.url = "jdbc:mysql://db:3306/java-play-sample"
    default.username = todo_user
    default.password = password
    default.logSql = true
    default.jndiName = DefaultDS
}

jpa.default=defaultPersistenceUnit


play.filters {
  # Allowed hosts filter configuration
  hosts {
    allowed = ["."]
  }
}

基本的にはチュートリアルと同じですが、 今回はdockerに接続するのでurlのhostはlocalhostではなくdocker-composeに記載するサービス名であることに注意が必要です。

default.url = "jdbc:mysql://db:3306/java-play-sample"

依存関係をbuild.sbtに追記していきます。 ormにはHibernate、APIはJPAを使っていきます。

name := """app"""
organization := "com.example"

version := "1.0-SNAPSHOT"

lazy val root = (project in file(".")).enablePlugins(PlayJava)

scalaVersion := "2.13.6"

libraryDependencies ++= Seq(
    guice,
    javaJpa,
    "mysql" % "mysql-connector-java" % "8.0.16",
    "org.hibernate" % "hibernate-core" % "5.4.30.Final"
)

Hibernateを使うので
conf/META-INF/persistence.xmlを作成する必要があります。

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">

    <persistence-unit name="defaultPersistenceUnit" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <non-jta-data-source>DefaultDS</non-jta-data-source>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect"/>
            <property name="hibernate.hbm2ddl.auto" value="update"/>
        </properties>
    </persistence-unit>

</persistence>

db

次にMySQLの環境を作っていきます。

初めにDockerファイルを作ります。

FROM mysql:5.7

次にmysql.confを作ります。

[mysqld]
character-set-server=utf8
default-storage-engine=INNODB
explicit-defaults-for-timestamp=true

[mysqldump]
default-character-set=utf8

[mysql]
default-character-set=utf8

[client]
default-character-set=utf8

次にMySQLの環境変数を書いていきます。

$ pwd
~/FastAPI-example-pro/db
$ touch .env
$ vi .env
MYSQL_DATABASE=fastapi-example
MYSQL_USER=todo_user
MYSQL_ROOT_PASSWORD=password
MYSQL_PASSWORD=password

最後にdocker-composeに作成していきます。

version: '3.7'

services:
  api:
    build:
      context: ./backend/
      dockerfile: ./Dockerfile
    command: >
        bash -c 'cd app &&
        sbt run'
    volumes:
      - ./backend:/server
    ports:
      - 9000:9000
    depends_on:
      - db

  db:
    build:
      context: ./db/
      dockerfile: ./Dockerfile
    env_file:
      - ./db/.env
    ports:
      - 3306:3306
    volumes:
      - ./db/conf.d:/etc/mysql/conf.d

まだbackend側からdbにアクセスする処理は書いていないのですが、 一旦docker-compose up で起動してエラーがないことを確認していきます。

$ docker-compose up 

以下のようなメッセージが最後の方に出たら成功です。

db_1   | 2021-05-22T08:53:02.789762Z 0 [Note] mysqld: ready for connections.
db_1   | Version: '5.7.34'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)

以上で環境構築は終了です。

アプリ作成

次はアプリを作成していきます。 sbt newで生成されたapp配下に追加していきます。

今回作成するのはDO, DAO, Service, Controllerになります。

  • DO:ドメインオブジェクト、DBのテーブルをオブジェクトで扱えるようにしたもの。
  • DAO:DOを操作するためのオブジェクト。
  • Service:DAOやビジネスロジックを記載する。
  • Controller:クライアントからのAPIリクエストとレスポンスを処理する。

本来はDOを直接渡さずに間にDTOを挟むことが多いですが、今回は割愛します。

以下実装していきます。

Domain

app/domain/TodoDO.java


package domain;

import javax.persistence.*;


@Entity
@Table(name = "todo")
public class TodoDO {
    @Id
    @Column
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;

    @Column
    private String title;

    @Column
    private String text;

    public Integer getId() {
        return this.id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitle() {
        return this.title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getText() {
        return this.text;
    }

    public void setText(String text) {
        this.text = text;
    }
}

DAO

app/dao/TodoDao.java

package dao;

import java.util.List;

import com.google.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import play.db.jpa.JPAApi;

import domain.TodoDO;


public class TodoDao {
    private static final String ENTITY_MANAGER_NAME = "default";

    @Inject
    protected JPAApi jpaApi;

    public TodoDO create(TodoDO todoDO) {
        jpaApi.withTransaction(entityManager -> { entityManager.persist(todoDO); });
        return todoDO;
    }

    public TodoDO find(Integer id) {
        return jpaApi.em(ENTITY_MANAGER_NAME).find(TodoDO.class, id);
    }

    public List<TodoDO> findAll(int limit) {
        System.out.println("start findAll");
        System.out.println("initialize EntityManager");
        EntityManager entityManager = jpaApi.em(ENTITY_MANAGER_NAME);
        System.out.println(" EntityManager initialized");

        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<TodoDO> criteriaQuery = criteriaBuilder.createQuery(TodoDO.class);
        Root<TodoDO> root = criteriaQuery.from(TodoDO.class);
        criteriaQuery.select(root);
        return entityManager.createQuery(criteriaQuery).setMaxResults(limit).getResultList();
    }

    public void delete(int id) {
        jpaApi.withTransaction(entityManager -> {
            TodoDO todoDO = entityManager.find(TodoDO.class, id);
            if (todoDO != null) {
                entityManager.remove(todoDO);
            }
        });
    }

    public TodoDO update(TodoDO todoDO) {
        jpaApi.withTransaction(entityManager -> {entityManager.merge(todoDO);});
        return todoDO;
    }
}

Service

app/services/TodoService.java

package services;

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

import com.google.inject.Inject;

import dao.TodoDao;
import domain.TodoDO;


public class TodoService {
    @Inject
    private TodoDao todoDao;

    public List<TodoDO> getAll() {
        List<TodoDO> todoDOList = todoDao.findAll(100);
        return todoDOList;
    }

    public TodoDO getById(int id) {
        TodoDO todoDO = todoDao.find(id);
        if (todoDO == null) {
            return null;
        }

        return todoDO;
    }

    public TodoDO create(TodoDO todoDO) {
        todoDO = todoDao.create(todoDO);
        return todoDO;
    }

    public TodoDO update(TodoDO todoDO, int id) {
        TodoDO fromDb = todoDao.find(id);
        if (fromDb == null) {
            return null;
        }

        fromDb.setTitle(todoDO.getTitle());
        fromDb.setText(todoDO.getText());
        fromDb = todoDao.update(fromDb);

        return fromDb;
    }

    public void delete(int id) {
        todoDao.delete(id);
    }
}

Controller

app/controllers/TodoController.java

package controllers;

import com.fasterxml.jackson.databind.JsonNode;
import com.google.inject.Inject;
import play.libs.Json;
import play.mvc.Controller;
import play.mvc.Http;
import play.mvc.Result;

import domain.TodoDO;
import services.TodoService;


public class TodosController extends Controller {
    @Inject
    private TodoService todoService;

    public Result get(Integer id) {
        TodoDO todo = todoService.getById(id);
        if (todo != null) {
            return ok(Json.toJson(todo));
        }

        return notFound();
    }

    public Result getAll() {
        return ok(Json.toJson(todoService.getAll()));
    }

    public Result create(Http.Request request) {
        JsonNode jsonData = request.body().asJson();
        try {
            TodoDO todoDO = Json.fromJson(jsonData, TodoDO.class);
            todoDO = todoService.create(todoDO);
            return ok(Json.toJson(todoDO));
        } catch (RuntimeException e) {
            // Most probably invalid todo data
            return badRequest(request.body().asJson());
        }
    }

    public Result update(Http.Request request, Integer id) {
        JsonNode jsonData = request.body().asJson();
        try {
            TodoDO todoDO = Json.fromJson(jsonData, TodoDO.class);
            TodoDO todo = todoService.update(todoDO, id);
            if (todo != null) {
                return ok(Json.toJson(todo));
            }
            return notFound();
        } catch (RuntimeException e) {
            return badRequest(request.body().asJson());
        }
    }

    public Result delete(Integer id) {
        todoService.delete(id);
        return ok();
    }
}

DDL

DBのテーブルを作成します。

$ docker-compose up -d
$ mysql -u todo_user -p -h localhost -P 3306 --protocol=tcp
(password)
mysql> use java-play-sample;
mysql> CREATE TABLE IF NOT EXISTS todo(
    -> id INT NOT NULL AUTO_INCREMENT,
    -> title VARCHAR(255) not null,
    -> text TEXT not null,
    -> primary key(id)
    -> );

以上で完了です。

確認して見ましょう。 今回もPythonインタプリタで確認していきます。

import requests

url = 'http://localhost:8000/todos'

# create
params = {'title': 'test title1', 'text': 'test text1'}
res = requests.post(url, json=params) # キーワード引数のjsonに渡すこと

# update
params = {'title': 'test title1', 'text': 'updated1'}
res = requests.put(f'{url}/1', json=params)

# get(all)
res = requests.get(url)

# get(id=1)
res = requests.get(f'{url}/1')

# delete
res = requests.delete(f'{url}/1')

参考文献