springframeworkとmybatisでassociationを使用したマッピング

やりたいこと

  • associationを使ってみたい
  • 前回はresultMap / constructor を使ったマッピングをやってみたのでその続き

前提

  • コードは前回の記事で作ったものに変更を加えていく

実行環境

  • 前回と同じ

検証

DBテーブルの追加/更新

  • 部署を管理するための DIVISIONテーブルを追加
  • USERSテーブルにDIVISIONテーブルを参照するカラムを追加
--- a/sql/001_init_tables.sql
+++ b/sql/001_init_tables.sql
@@ -1,9 +1,22 @@
+CREATE TABLE DIVISION (
+  DIVISION_ID CHAR(2) NOT NULL,
+  DIVISION_NAME VARCHAR(32) NOT NULL,
+  PRIMARY KEY (DIVISION_ID)
+);
+
+INSERT INTO DIVISION (DIVISION_ID, DIVISION_NAME) VALUES ('10', '人事部');
+INSERT INTO DIVISION (DIVISION_ID, DIVISION_NAME) VALUES ('20', '営業部');
+INSERT INTO DIVISION (DIVISION_ID, DIVISION_NAME) VALUES ('30', '開発部');
+
 CREATE TABLE USERS (
   USER_ID VARCHAR(32) NOT NULL,
   FIRST_NAME VARCHAR(32) NOT NULL,
   FAMILY_NAME VARCHAR(32) NOT NULL,
-  PRIMARY KEY (USER_ID)
+  DIVISION_ID CHAR(2) NOT NULL,
+  PRIMARY KEY (USER_ID),
+  FOREIGN KEY (DIVISION_ID) REFERENCES DIVISION (DIVISION_ID)
 );
-INSERT INTO USERS (USER_ID, FIRST_NAME, FAMILY_NAME) VALUES ('hogefuga', 'ほげ', 'ふが');
-INSERT INTO USERS (USER_ID, FIRST_NAME, FAMILY_NAME) VALUES ('foobar', 'ふー', 'ばー');
+
+INSERT INTO USERS (USER_ID, FIRST_NAME, FAMILY_NAME, DIVISION_ID) VALUES ('hogefuga', 'ほげ', 'ふが', '10');
+INSERT INTO USERS (USER_ID, FIRST_NAME, FAMILY_NAME, DIVISION_ID) VALUES ('foobar', 'ふー', 'ばー', '20');

Javaクラス

Division / DivisionId クラスの追加

public class DivisionId {
  private final String id;

  public DivisionId(final String id) {
    this.id = id;
  }

  public String toString() {
    return id;
  }
}
public class Division {

  private final DivisionId divisionId;
  private final String divisionName;

  public Division(final DivisionId divisionId, final String divisionName) {
    this.divisionId = divisionId;
    this.divisionName = divisionName;
  }

  public String toString() {
    return String.format("Division {%s:%s}", divisionId, divisionName);
  }
}

UserクラスにDivisionクラスのフィールドを追加

--- a/core/src/main/java/com/github/nkiri/core/domain/model/user/User.java
+++ b/core/src/main/java/com/github/nkiri/core/domain/model/user/User.java
@@ -1,11 +1,15 @@
 package com.github.nkiri.core.domain.model.user;

+import com.github.nkiri.core.domain.model.division.Division;
+
 public class User {

   private final UserId userId;
   private final String firstName;
   private final String familyName;

+  private Division division;
+
   public User(final UserId userId, final String firstName, final String familyName) {
     this.userId = userId;
     this.firstName = firstName;
@@ -14,6 +18,7 @@ public class User {

   public String toString() {
     return String.format(
-        "User {userId: %s, firstName: %s, familyName: %s}", userId, firstName, familyName);
+        "User {userId: %s, firstName: %s, familyName: %s, division: %s}",
+        userId, firstName, familyName, division);
   }
 }

UserMapper.xmlの修正 (本題)

  • Userクラスのコンストラクタでセットしないフィールドに値をセットするときにassociationを使う
    • 今回でいうと User.divisionがそれ
  • 公式ドキュメントによると has-oneのリレーションシップを扱うものだとのこと
  • 使い方は resultMapと同じような感じ
  • <association>要素の select属性を使うこともできるけど、N+1問題が発生するので巨大な結果を取得するような場合は注意が必要そう
--- a/core/src/main/resources/com/github/nkiri/core/infrastructure/datastore/UserMapper.xml
+++ b/core/src/main/resources/com/github/nkiri/core/infrastructure/datastore/UserMapper.xml
@@ -4,10 +4,14 @@
 <mapper namespace="com.github.nkiri.core.infrastructure.datastore.UserMapper">
   <select id="get" resultMap="userResultMap">
     SELECT
-    USER_ID as userId
-    , FIRST_NAME as firstName
-    , FAMILY_NAME as familyName
-    FROM USERS
+    u.USER_ID as userId
+    , u.FIRST_NAME as firstName
+    , u.FAMILY_NAME as familyName
+    , d.DIVISION_ID as divisionId
+    , d.DIVISION_NAME as divisionName
+    FROM USERS u
+    JOIN DIVISION d
+    ON u.DIVISION_ID = d.DIVISION_ID
     <where>
       USER_ID = #{id}
     </where>
@@ -20,11 +24,23 @@
     </constructor>
   </resultMap>
 
+  <resultMap id="divisionIdMap" type="com.github.nkiri.core.domain.model.division.DivisionId">
+    <constructor>
+      <idArg column="divisionId" javaType="string"/>
+    </constructor>
+  </resultMap>
+
   <resultMap id="userResultMap" type="com.github.nkiri.core.domain.model.user.User">
     <constructor>
       <idArg resultMap="userIdResultMap" column="userId" javaType="com.github.nkiri.core.domain.model.user.UserId"/>
       <arg column="firstName" javaType="string"/>
       <arg column="familyName" javaType="string"/>
     </constructor>
+    <association property="division" javaType="com.github.nkiri.core.domain.model.division.Division">
+      <constructor>
+        <idArg column="divisionId" resultMap="divisionIdMap" javaType="com.github.nkiri.core.domain.model.division.DivisionId"/>
+        <arg column="divisionName" javaType="string"/>
+      </constructor>
+    </association>
   </resultMap>
 </mapper>

springframeworkとmybatisでresultMapを使用したマッピング

やること

  • springframeworkとmybatisを使ったデータ取得
  • resultMapを使用したオブジェクトへのマッピング
    • 値オブジェクトを含むコンストラクタの扱い

やらないこと

  • springframeworkについての話
  • postgresについての話
  • assosiationやcollection(これはまた今度)

前提

  • Repositoryのメソッドを呼び出す処理については言及しない
    • 基本的には取得したデータを System.out.prinlnで出力したものを 実行結果として記載する

実行環境

  • Java8
  • springframework 5.3.23
  • mybatis 3.5.10
  • mybatis-spring 2.0.7
  • postgres14.5 (docker)
  • postgresql (sql driver) 42.5.0

構成

DB

CREATE TABLE USERS (
  USER_ID VARCHAR(32) NOT NULL,
  FIRST_NAME VARCHAR(32) NOT NULL,
  FAMILY_NAME VARCHAR(32) NOT NULL,
  PRIMARY KEY (USER_ID)
);
INSERT INTO USERS (USER_ID, FIRST_NAME, FAMILY_NAME) VALUES ('hogefuga', 'ほげ', 'ふが');
INSERT INTO USERS (USER_ID, FIRST_NAME, FAMILY_NAME) VALUES ('foobar', 'ふー', 'ばー');

Javaクラス

User

public class User {

  private final String userId;
  private final String firstName;
  private final String familyName;

  public User(final String userId, final String firstName, final String familyName) {
    this.userId = userId;
    this.firstName = firstName;
    this.familyName = familyName;
  }

  public String toString() {
    return String.format(
        "User {userId: %s, firstName: %s, familyName: %s}", userId, firstName, familyName);
  }
}

UserRepository

interface

import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository {

  User get(String userId);
}

impl

import com.github.nkiri.core.domain.model.user.User;
import com.github.nkiri.core.domain.model.user.UserRepository;
import org.springframework.stereotype.Repository;

@Repository
public class DefaultUserRepository implements UserRepository {

  private final UserMapper userMapper;

  public DefaultUserRepository(final UserMapper userMapper) {
    this.userMapper = userMapper;
  }

  public User get(String userId) {
    return userMapper.get(userId);
  }
}

UserMapper

  • 後述

applicationContext.xml

  • 省略

検証

文字列パラメータを渡してデータ取得

UserMapper

  • interfaceで定義した引数をxml側で参照する
import com.github.nkiri.core.domain.model.user.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
  User get(String userId);
}
  • resultMapの定義はtype属性にマッピングするクラスを指定するだけ
    • select要素のresultMap属性で定義した resultMapを指定する
<?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">
<mapper namespace="com.github.nkiri.core.infrastructure.datastore.UserMapper">
  <select id="get" resultMap="userResultMap">
    SELECT
    USER_ID as userId
    , FIRST_NAME as firstName
    , FAMILY_NAME as familyName
    FROM USERS
    <where>
      USER_ID = #{userId}
    </where>
    ;
  </select>
  
  <resultMap id="userResultMap" type="com.github.nkiri.core.domain.model.user.User"/>
</mapper>

実行結果

User {userId: hogefuga, firstName: ほげ, familyName: ふが}

User.userIdを値オブジェクトとして定義する

変更点

UserId

public class UserId {
    private final String id;

    public UserId(final String id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return id;
    }
}

User

index e05003d..93c1db8 100644
--- a/core/src/main/java/com/github/nkiri/core/domain/model/user/User.java
+++ b/core/src/main/java/com/github/nkiri/core/domain/model/user/User.java
@@ -2,11 +2,11 @@ package com.github.nkiri.core.domain.model.user;
 
 public class User {
 
-  private final String userId;
+  private final UserId userId;
   private final String firstName;
   private final String familyName;
 
-  public User(final String userId, final String firstName, final String familyName) {
+  public User(final UserId userId, final String firstName, final String familyName) {
     this.userId = userId;
     this.firstName = firstName;
     this.familyName = familyName;

実行結果(エラー)

  • DBから取得したデータをUserのコンストラクタに渡した際に型が不一致でエラーになってる模様
Exception in thread "main" org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: Error instantiating class com.github.nkiri.core.domain.model.user.User with invalid types (UserId,String,String) or values (hogefuga,ほげ,ふが). Cause: java.lang.IllegalArgumentException: argument type mismatch

修正

  • DBのUSER_IDカラムから取得した値を UserIdクラスのコンストラクタに渡し、その結果を用いてUserクラスのコンストラクタを呼び出す
    • constructor要素の子要素には、Javaクラスに用意したコンストラクタの引数の順番に合わせて idArgarg要素を並べる必要がある
      • 今回は (UserId, String, String)の引数を持つコンストラクタが使用される
      • 引数を順不同としたい場合は name属性を指定する
index 7cc80d5..31b127f 100644
--- a/core/src/main/resources/com/github/nkiri/core/infrastructure/datastore/UserMapper.xml
+++ b/core/src/main/resources/com/github/nkiri/core/infrastructure/datastore/UserMapper.xml
@@ -14,5 +14,17 @@
     ;
   </select>
 
-  <resultMap id="userResultMap" type="com.github.nkiri.core.domain.model.user.User"/>
+  <resultMap id="userIdResultMap" type="com.github.nkiri.core.domain.model.user.UserId">
+    <constructor>
+      <idArg column="userId" javaType="string"/>
+    </constructor>
+  </resultMap>
+
+  <resultMap id="userResultMap" type="com.github.nkiri.core.domain.model.user.User">
+    <constructor>
+      <idArg resultMap="userIdResultMap" column="userId" javaType="com.github.nkiri.core.domain.model.user.UserId"/>
+      <arg column="firstName" javaType="string"/>
+      <arg column="familyName" javaType="string"/>
+    </constructor>
+  </resultMap>
 </mapper>
\ No newline at end of file

実行結果

User {userId: hogefuga, firstName: ほげ, familyName: ふが}

値オブジェクトをgetの引数として渡したい

変更点

UserRepository

  • interface
index ca26f43..28f9f39 100644
--- a/core/src/main/java/com/github/nkiri/core/domain/model/user/UserRepository.java
+++ b/core/src/main/java/com/github/nkiri/core/domain/model/user/UserRepository.java
@@ -5,5 +5,5 @@ import org.springframework.stereotype.Repository;
 @Repository
 public interface UserRepository {
 
-  User get(String userId);
+  User get(UserId userId);
 }
  • impl
index e288413..ae792da 100644
--- a/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/DefaultUserRepository.java
+++ b/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/DefaultUserRepository.java
@@ -1,6 +1,7 @@
 package com.github.nkiri.core.infrastructure.datastore;
 
 import com.github.nkiri.core.domain.model.user.User;
+import com.github.nkiri.core.domain.model.user.UserId;
 import com.github.nkiri.core.domain.model.user.UserRepository;
 import org.springframework.stereotype.Repository;
 
@@ -13,7 +14,7 @@ public class DefaultUserRepository implements UserRepository {
     this.userMapper = userMapper;
   }
 
-  public User get(String userId) {
+  public User get(UserId userId) {
     return userMapper.get(userId);
   }
 }

UserMapper

index 1566960..d604e3a 100644
--- a/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/UserMapper.java
+++ b/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/UserMapper.java
@@ -1,9 +1,10 @@
 package com.github.nkiri.core.infrastructure.datastore;
 
 import com.github.nkiri.core.domain.model.user.User;
+import com.github.nkiri.core.domain.model.user.UserId;
 import org.apache.ibatis.annotations.Mapper;
 
 @Mapper
 public interface UserMapper {
-  User get(String userId);
+  User get(UserId userId);
 }

実行結果(エラー)

  • パラメータで渡しているUserIdクラスに、userIdフィールドのgetterメソッドがないと言われている
    • UserMapper.xml内で #{userId}としていると自動でUserId.userIdを参照しようとしてくれる模様
    • しかし今回はUserId.idを参照してほしいのでこれだとダメ
Exception in thread "main" org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'userId' in 'class com.github.nkiri.core.domain.model.user.UserId'

修正

方法1. 参照するフィールド名の変更

  • idフィールドを参照するようにしてあげれば良いだけ
index 31b127f..0964be1 100644
--- a/core/src/main/resources/com/github/nkiri/core/infrastructure/datastore/UserMapper.xml
+++ b/core/src/main/resources/com/github/nkiri/core/infrastructure/datastore/UserMapper.xml
@@ -9,7 +9,7 @@
     , FAMILY_NAME as familyName
     FROM USERS
     <where>
-      USER_ID = #{userId}
+      USER_ID = #{id}
     </where>
     ;
   </select>

方法2. @Paramを使用する(引数が複数の場合に使用する)

  • 引数が複数ある場合は@Paramを使用してxml側で引数の値を参照できるようにする
    • 今回は引数1つなので普通はこの方法は取らないらしいが、一応これでも動く
index d604e3a..1154437 100644
--- a/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/UserMapper.java
+++ b/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/UserMapper.java
@@ -3,8 +3,9 @@ package com.github.nkiri.core.infrastructure.datastore;
 import com.github.nkiri.core.domain.model.user.User;
 import com.github.nkiri.core.domain.model.user.UserId;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
 @Mapper
 public interface UserMapper {
-  User get(UserId userId);
+  User get(@Param("userId") UserId userId);
 }
index 31b127f..6fe3a2d 100644
--- a/core/src/main/resources/com/github/nkiri/core/infrastructure/datastore/UserMapper.xml
+++ b/core/src/main/resources/com/github/nkiri/core/infrastructure/datastore/UserMapper.xml
@@ -9,7 +9,7 @@
     , FAMILY_NAME as familyName
     FROM USERS
     <where>
-      USER_ID = #{userId}
+      USER_ID = #{userId.id}
     </where>
     ;
   </select>

実行結果

User {userId: hogefuga, firstName: ほげ, familyName: ふが}

まとめ

  • resultMapを使うことによって、DB設計がイケてなくてもJavaクラスの設計に影響を与えずにいい感じにマッピングすることができそう
  • associationやcollectionの使い方についてはまた今度調べてみる

OracleDatabase19cをOracleLinux8.5にインストールする

この記事の目的

  • Oracle Databaseを使う仕事をしているのでちゃんと勉強をしようと思った
  • 自宅で環境構築をしようとしたが、そもそもインストーラGUIで起動する時点で詰まった
    • サイレントインストールで構築することもできたが、EM Expressが構成されなかった
      • これの原因はまだ調べられていない
  • Oracle DBAの資格を取る人とかの助けになったら嬉しい

やりたかったこと

  • Oracle Databaseのインストールとセットアップ
    • OUIとかDBCAを使った環境構築
  • Oracle Enterprise Manager Database Express(EM Express)の導入
    • 触ってみたかっただけ

この記事に書かないこと

  • OracleLinuxのセットアップ
  • サイレントインストールの方法
  • 各種パラメータの説明
  • スキーマ作ったりテーブル作ったりメディアリカバリしてみたり(この記事のスコープ外とする)

環境

バージョン情報

  • OS: OracleLinux8.5
  • OracleDatabase: 19.3.0

ローカルアカウント

  • administrator: OSインストール時に作成したアカウント
  • oracle: OracleDatabaseの所持者となるアカウント(作成手順は後述)

準備

前提

  • administratorユーザでログインして実行する前提でコマンドを記載する
  • 当然sudo -u oracleとしているところは、sudo su - oracleとしてユーザを切り替えてから実行しても問題はない

依存パッケージのインストール

sudo yum install -y gcc make libnsl

oracleユーザとかグループの作成

  • administratorユーザでログインし、以下を実行
# oracleユーザを所属させるグループを作成
sudo groupadd -g 54321 oinstall
sudo groupadd -g 54322 dba
sudo groupadd -g 54323 oper
sudo groupadd -g 54324 backupdba
sudo groupadd -g 54325 dgdba
sudo groupadd -g 54326 kmdba
sudo groupadd -g 54327 asmdba
sudo groupadd -g 54328 asmoper
sudo groupadd -g 54329 asmadmin
sudo groupadd -g 54330 racdba

# oracleユーザを作成(↑で作成したグループに紐付け)
sudo useradd -u 54321 -g oinstall -G dba,oper,backupdba,dgdba,kmdba,racdba oracle

oracleユーザの環境変数設定

  • administratorユーザでログインし、以下を実行
sudo -i -u oracle bash -c echo 'export ORACLE_BASE=/u01/app/oracle' >> /home/oracle/.bash_profile
sudo -i -u oracle bash -c echo 'export ORACLE_HOME=/u01/app/oracle/product/19.3.0/dbhome_1' >> /home/oracle/.bash_profile
sudo -i -u oracle bash -c echo 'export ORACLE_SID=orcl' >> /home/oracle/.bash_profile
sudo -i -u oracle bash -c echo 'export NLS_LANG=Japanese_Japan.AL32UTF8' >> /home/oracle/.bash_profile
sudo -i -u oracle bash -c echo 'export CV_ASSUME_DISTID=OEL8.5' >> /home/oracle/.bash_profile
sudo -i -u oracle bash -c echo 'export PATH=$PATH:$ORACLE_HOME/bin' >> /home/oracle/.bash_profile

Oralce Databaseインストール用のディレクトリを準備

  • administratorユーザでログインし、以下を実行
# 公式のドキュメントでもこんな感じのディレクトリ構成にしていたはず
sudo install -o oracle -g oinstall -d /u01
sudo -u oracle mkdir -p /u01/app/oracle/product/19.3.0/dbhome_1 # $ORACLE_HOMEに指定したディレクトリ

公式サイトからダウンロードしたzipファイルを展開

  • administratorユーザでログインし、以下を実行
sudo install -o oracle -g oinstall -m 644 -t /u01/app/oracle/product/19.3.0/dbhome_1 /home/naokiryu/LINUX.X64_193000_db_home.zip

X Windowの設定

X Windowへのアクセス許可

  • デフォルトでは oracleユーザでGUIアプリケーションを実行する権限がないらしい
    • 正確な理解ではないかも
    • ちょっとよくわかってないので詳しい人がいたら教えてほしい
  • ローカルアカウントのユーザに対してGUIアプリケーションの実行を許可するために以下を実行する
sudo xhost +local:user

rootのDISPLAY環境変数を確認

sudo su -
echo $DISPLAY # これの出力結果を↓で使う。ここでは ":0"という結果だったと仮定する

oracleユーザのDISPLAY環境変数の設定

# rootのDISPLAY環境変数と同じ値をセットする
sudo -i -u oracle bash -c echo 'export DISPLAY=:0' >> /home/oracle/.bash_profile

インストールの実行

Oracle関連ソフトウェアのインストール

  • oracleユーザで以下を実行するとGUIインストーラが起動するので、画面をポチポチしてインストールする
cd /u01/app/oracle/product/19.3.0/dbhome_1
./runInstaller

Databaseのセットアップ

  • oracleユーザで以下を実行するとGUIでセットアップツールが起動するので、画面をポチポチしてセットアップする
  • EM Expressについて、ここでは5500ポートでLISTENするように設定することとする
  • リスナーは適当な名前で作成しておく。ここでは LISTENERとして作成することとする。
dbca

EM Expressの起動

  • dbcaで指定したポートでLISTENしているかを確認
# DBに接続
sqlplus / as sysdba

# ポート番号の表示
select dbms_xdb_config.gethttpsport() from dual; # 5500と表示されていればOK

# システムに変更を反映
alter system register;

# 切断
exit
  • リスナーの状態を確認
lsnrctl status | grep 5500 # リスナーが起動していれば接続記述子がgrepに引っかかるはず

firewalldの停止

  • 他のPCからEM Expressのポートに接続するため、firewalldを停止する
  • administratorユーザで以下を実行
sudo systemctl stop firewalld.service

ブラウザからアクセス

  • URL: https://<IPアドレス>:5500/em
  • ログインアカウント情報
    • ユーザ名: sys
    • パスワード: dbcaで指定した SYSのパスワード

まとめ

  • X Windowのところとかめっちゃ探したけど公式ドキュメントからも見つけられず、不親切だなと感じた。
    • 普通に書いてあったらごめんなさい
  • runInstallerdbcaでセットアップしたときのレスポンスファイルを出力して保存できるので、これを使ってサイレントインストール時になぜEM Expressが起動できなかったのかを調べてみようと思う
    • EM ExpressはブラウザのFlashが廃止になってから機能が削られたりしてるって噂なので、今後は使わなくなっていくのかなと思ってる。知らんけど。

令和から始めるJavaServletアプリケーション

この記事について

  • Servletって大学のときに聞いたことあるけど、実際よくわかってない
    • そもそもJavaとか触ったことがない
    • が、突如Javaでアプリケーションを作るチームに入ったのでお勉強
  • とりあえず簡単なものを動かしてみて、裏側の実装とかまで覗けたらいいなという気持ち
  • たぶん今回は動かすだけ

まずは動かしてみる

環境

  • macOS Catalina 10.15.4
  • adoptopenjdk-8
  • maven 3.6.3
  • tomcat 9.0.35 (dockerを使用)

プロジェクトの作成

  • mvn コマンドで一発
$ mvn archetype:generate -DgroupId=com.github.nkiri -DartifactId=servletapp -DarchetypeArtifactId=maven-archetype-webapp
$ # インタラクションは全部デフォルト
  • こんな感じに作られる
$ cd servletapp
$ tree .
.
├── pom.xml
└── src
    └── main
        ├── resources
        └── webapp
            ├── WEB-INF
            │   └── web.xml
            └── index.jsp

Servletクラスの実装

  • javax.servelt.http.HttpServletを継承したクラスを作成する
    • src/main/java/com/github/nkiri/servletapp/HelloServlet.java として保存
    • doGetはHTTPのGETメソッドの処理をするためのメソッドで、他のHTTPメソッドはdoPostとかdoDeleteとかを作るらしい
      • 今回は割愛
    • writerにprintlnで文字列を書き込むとクライアントに返される
      • 今回はシンプルなhtml
package com.github.nkiri.servletapp;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class HelloServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        PrintWriter writer = response.getWriter();
        writer.println("<html><head></head><body>hello<body></html>");
    }
}

web.xmlを作成

  • Webアプリケーションの設定を記載する
  • プロジェクト作成時に作られたものを編集
  • /hi にマッチするリクエストはHelloServletが処理するように設定
@@ -3,5 +3,12 @@
  "http://java.sun.com/dtd/web-app_2_3.dtd" >

 <web-app>
-  <display-name>Archetype Created Web Application</display-name>
-</web-app>
+    <servlet>
+        <servlet-name>Hello</servlet-name>
+        <servlet-class>com.github.nkiri.servletapp.HelloServlet</servlet-class>
+    </servlet>
+    <servlet-mapping>
+        <servlet-name>Hello</servlet-name>
+        <url-pattern>/hi</url-pattern>
+    </servlet-mapping>
+</web-app>

ビルド

  • 今回はwarファイルとして生成する
  • pom.xmlを以下のように編集
@@ -9,6 +9,11 @@
   <url>http://maven.apache.org</url>
   <dependencies>
     <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+      <version>4.0.1</version>
+    </dependency>
+    <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
       <version>3.8.1</version>
@@ -17,5 +22,12 @@
   </dependencies>
   <build>
     <finalName>servletapp</finalName>
+    <plugins>
+      <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-war-plugin</artifactId>
+          <version>3.2.3</version>
+      </plugin>
+    </plugins>
   </build>
 </project>
  • mvnコマンドでビルド
    • target/servletapp.warが作られる
mvn clean package

tomcatにデプロイ

  • dockerでtomcatを起動
    • warファイルは-vコマンドで渡してあげる
docker run -it --name servletapp -v `pwd`/target/servletapp.war:/usr/local/tomcat/webapps/servletapp.war -p 8080:8080 --rm tomcat:9.0.35-jdk8-openjdk

【初心者】SpringBootでTodoアプリのREST APIを書いてみる

追記

  • Controllerのコードの一部が切れていたので修正
  • Controllerのアノテーション周りの話が抜けていたので追記

内容

  • Java初心者が夏休み中にSpringの勉強をする
  • Spring Frameworkはハードルが高そうだったので一先ずSpringBootから
  • お題は初心者がよくやるTodoアプリのREST API

詳しく触れないこと

  • gradle周辺
  • Database周辺
    • JPA
    • H2 Database

本編

  • 開発はIntelliJ IDEAを使用
    • ビルドとかアプリケーションの起動はお任せ

雛形作成

  • https://start.spring.io/ で作成
    • Project: Gradle Project
    • Language: Java
    • Spring Boot: 2.1.8
    • Project Metadaba:
      • Group: com.example
      • Artifact: todoapp
      • Options: そのまま
    • Dependencies:
      • Spring Web
      • Spring Data JPA
      • H2 Database
      • Lombok
  • ダウンロードされたzipファイルをunzipしてIntelliJでopenしたところから開始

パッケージ構成

  • com.example.todoapp直下に以下を並べる
    • controllers
    • repositories
    • models
  • servicesとかもあるべきかもしれないけど今回は割愛

コード

models

package com.example.todoapp.models;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Data
@Entity
public class Task {

    @Id
    @GeneratedValue
    private Long id;
    private String summary;
    private Boolean done = false;

    public Task(){}
}
  • アノテーションについて
    • @Data
      • 各フィールドのsetterとgetterを定義してくれる
      • toString()メソッドやhashCode()メソッドを定義してくれる
    • @Entity
      • エンティティであることを示す。
      • このクラスのフィールドを持つテーブルがDBに作られる
    • @Id
      • エンティティの主キーのフィールドを指定する
    • @GeneratedValue
      • 主キーの値の生成方法を指定する
      • strategygeneratorを指定して挙動を変えられそう(試してない)

repositories

  • TaskRepository.java
package com.example.todoapp.repositories;

import com.example.todoapp.models.Task;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TaskRepository extends JpaRepository<Task, Long> {
}
  • JpaRepositoryを継承したinterfaceを定義
    • JpaRepositoryの祖先にいるCrudRepositoryインターフェースがfindByIdなどのインターフェースを定義している
    • 実装はSimpleJpaRepositoryなのかな?
  • JpaRepositoryは対象のエンティティクラスとその主キーの型を指定する
    • ここで指定した主キーの型は、findByIddeleteByIdの引数の型として使われる
    • 今回作成したTaskエンティティの主キーはLong型なのでここではLongを指定

controllers

  • ApiController.java
package com.example.todoapp.controllers;

import com.example.todoapp.models.Task;
import com.example.todoapp.repositories.TaskRepository;
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.Optional;

@RestController
@RequestMapping(value = "/api")
public class ApiController {

    @Autowired
    TaskRepository taskrepo;

    @GetMapping(value = "/todos")
    public List<Task> tasklist(){
        return taskrepo.findAll();
    }

    @GetMapping(value = "todo/{id}")
    public Optional<Task> retrieve(@PathVariable Long id){
        return taskrepo.findById(id);
    }

    @PostMapping(value = "/todo")
    public ResponseEntity<Task> newTask(@RequestBody Task task){
        Task result = taskrepo.save(task);
        return new ResponseEntity<Task> (result, HttpStatus.CREATED);
    }

    @PutMapping(value = "/todo/{id}")
    public Task update(@PathVariable Long id, @RequestBody Task task){
        task.setId(id);
        return taskrepo.save(task);
    }

    @DeleteMapping(value = "/todo/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id){
        taskrepo.deleteById(id);
    }
}
  • アノテーションについて
    • @RestController
      • 各メソッドの戻り値がそのままレスポンスボディになる
      • @Controllerを使う場合、メソッドの戻り値はStringにしてテンプレートに埋め込む形で使うらしい
    • @RequestMapping
      • このコントローラが受け付けるリクエストのパスやメソッドやクエリパラメータを指定できる
    • @Autowired
      • @Componentアノテーションをつけて登録されているBeanとの紐付けを行う
      • @Repository@Serviceをつけたクラスも、内部では@Componentが付加されている
      • ここではTaskRepositoryインターフェースを満たすクラスが自動的にtaskrepoに格納される
    • @GetMapping, @PostMapping, @PutMapping, @DeleteMapping
      • @RequestMappingのメソッドを固定したもの
      • @GetMappingの場合、@RequestMapping(method=RequestMethod.GET)と同等
    • @ResponseStatus

動作確認

新規登録

$ curl http://localhost:8080/api/todo -XPOST -H 'Content-Type: application/json' -d '{"summary": "my first task"}' | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    75    0    47  100    28    170    101 --:--:-- --:--:-- --:--:--   170
{
  "id": 1,
  "summary": "my first task",
  "done": false
}

リスト取得

$ curl http://localhost:8080/api/todos | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    49    0    49    0     0    285      0 --:--:-- --:--:-- --:--:--   286
[
  {
    "id": 1,
    "summary": "my first task",
    "done": false
  }
]

1件取得

$ curl http://localhost:8080/api/todo/1 | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    47    0    47    0     0   2045      0 --:--:-- --:--:-- --:--:--  2136
{
  "id": 1,
  "summary": "my first task",
  "done": false
}

更新

$ curl http://localhost:8080/api/todo/1 -XPUT -H 'Content-Type: application/json' -d '{"summary": "updated my task", "done": true}' | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    92    0    48  100    44   2632   2412 --:--:-- --:--:-- --:--:--  2666
{
  "id": 1,
  "summary": "updated my task",
  "done": true
}

削除(レスポンスボディがないのでStatusCodeで確認)

$ curl http://localhost:8080/api/todo/1 -XDELETE -v
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> DELETE /api/todo/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 204
< Date: Wed, 25 Sep 2019 15:29:37 GMT
<
* Connection #0 to host localhost left intact

まとめ

  • 動かしたいもの自体はめっちゃ簡単に動かせた
  • アノテーション種類多すぎて何使えばいいのかわからん
    • とりあえず今回使ったものくらいは最低限おさえておきたい
    • あとは何かするときに都度調べられればよさそう

OracleXE 11gの SJIS化

注釈

この記事について

  • OracleDatabase Express Edition(XE) 11g のDockerコンテナで文字コードを変更するためにやったことを書く(N番煎じ)
  • 11gでは文字コードはデフォルトでUTF8となっており、変更するためにはDBを再構築する必要がある
  • 単体テストとかで使いたいので、起動したときに既に文字コードが変更されている状態にしたい
  • 「え、今の時代に11g…?」
    • 仰るとおりだと思いますが、古いシステムだし皆さんのところにもそういうのありますよね?ね?

前提

  • oracleが公開しているDockerfileを使って作成したイメージをベースイメージとする
    • 記事作成時点での最新コミットは 0972d17

手順

追加で修正する箇所

  • CMDコマンドで実行されている$ORACLE_BASE/$RUN_FILEを実行しないようにする必要がある
    • 起動時に/etc/init.d/oracle-xe configureが実行されないようにするため
  • 以下のことを自前で行う
    • tnsnames.oraの書き換え
    • listener.oraの書き換え
    • /etc/init.d/oracle-xe configureに渡すrspファイルの書き換え
      • SYSとSYSTEM向けのパスワードの部分

上記もろもろに対応したDockerfile

確認

  • コンテナを起動
$ docker run --name oracle --shm-size 1g --rm nkiri/oracle/database:11.2.0.2-sjis
  • 別ターミナルでdocker execでコンテナに入る
$ docker exec -it oracle
  • 以下のコマンドを実行
su -p oracle -c "sqlplus -s / as sysdba" <<EOS
> select * from nls_database_parameters where parameter = 'NLS_CHARACTERSET';
> EOS
  • 結果
PARAMETER
------------------------------
VALUE
--------------------------------------------------------------------------------
NLS_CHARACTERSET
JA16SJISTILDE

まとめ

  • まぁできるんだけど、ビルドに1時間くらいかかる…
  • SYSとSYSTEMのパスワードをビルド時に確定することになるので、公式のやつより若干自由度が下がる

django-rest-frameworkのserializerってどう呼ばれてるんだろう

注釈

目的

  • 前に書いたやつ でserializerの中身は読んだ
  • でもこのserializerってどこでどう呼ばれてるんだろう

環境

  • 前のと同じ
  • Serializerの使い方とかは参考記事を参照

結論

  • mixins.pyで呼ばれてた
    • createupdateのそれぞれで
class CreateModelMixin(object):
    """
    Create a model instance.
    """
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
class UpdateModelMixin(object):
    """
    Update a model instance.
    """
    def update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)
  • get_serializerしてis_validしてperform_hogehogeするっていう流れらしい
    • perform_hogehogeではserializer.save()してる

応用

  • POSTとかPUTでAPIを呼ばれた時の挙動を帰るときはviewのcreateメソッドとかupdateメソッドを上書きしてやればよさげ
  • DBにデータ書き込みはしないけどPOSTでAPIを提供したいときは↓みたいな感じ?
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        # ここでserializer.validated_dataを使って何らかの処理をする
        return Response({'result': 'data'}, status=status.HTTP_200_OK)

参考