レガシーシステムと向き合う

この記事で扱うこと

  • 自身が所属しているチームが開発・運用しているレガシーシステムに我慢ができなくなり、作り直しはできずともちょっとだけマシにするためにやったことのメモ
  • 既に対応実施から数年たったが「以前のほうがよかった」という声は一切ないので、取り組みは成功だったと思っている

実施前の状態について

システムについて

開発するための環境

コーディング環境

秘伝のVM運用

  • 会社から支給されたPC上にチームで管理してるVMイメージからVMを作成する

  • DBのインスタンスVM上に構築済みの状態

    • テーブルは作成済みで、データも投入済み(過去に使用したゴミデータも含まれる)
      • DBに更新があった場合は誰かが更新を適用したVMイメージを作成してチーム内に配布

コーディング規約

  • 口伝のルールがあったらしいが、実質ないようなものだった

エディタ

  • 支給されたPC上に好きなエディタを入れてそれを使う人
    • ローカルのディレクトリをVM上にマウントして動かしたりしてた
  • VM上にインストールされたエディタを使う人

コーディング環境の問題点

  • VMの運用がめんどくさい
  • コードを書く人やレビューする人によって書きっぷりが少しずつ異なっていた
  • VMイメージを作った人が .vimrcをめっちゃいじる人だったので死ぬほど使いにくかった

テスト環境

動作確認

  • 画面の表示はVM上で確認
  • VMで確認できない部分を共用の開発環境サーバにデプロイして動かす
    • 外部システムとの連携とか

自動テスト

  • なし

テスト環境の問題点

  • ローカルのVM環境でのテストが信頼できない
    • その人の環境でだけ動くコードなどが発生する
  • 共用開発環境は他の誰かが使っているときは使えない
  • VM上に過去の作業によって生じたゴミデータが永遠に蓄積される
    • DBにゴミを貯めたままVMイメージが配布される
  • 簡単な関数処理の動作確認ですら共用開発環境で動かさないとミスに気付けない
  • 想定していない箇所への影響が検知できない

運用

デプロイ

作業の流れ

  1. 更新対象のファイルを手動でリストアップ
  2. リストアップしたファイルについて、既存ファイルをサーバ上でバックアップ
  3. 更新対象ファイルを置き換え
  4. 切り戻しの際はリストアップしたファイルをバックアップファイルで置き換え

デプロイの問題点

  • リストアップした一覧に漏れが生じる
  • 作業を全て手作業で実施するため、ミスや実施漏れが生じる
  • 前述の共用の開発環境へのデプロイや戻し作業も同様の方法で行うため、戻し作業にも漏れが生じる
    • 後続作業で想定外の事象が起きた時の切り分けが困難
  • ミス防止のため(?)、チームメンバー全員で作業の見守りをしていた
    • 5人で60~90分くらい作業してたので工数的も問題あり
    • それでもミスは頻発していたのでマジで無意味な時間だった

目的意識

やりたかったこと

  • デプロイ作業の簡易化

    • 作業工数の削減
    • ミスを削減したかった
  • 自動テスト

    • 簡単な処理の動作保証
  • コードフォーマットの自動化

  • 静的解析

    • 文法チェック
    • linterの導入

やらないようにしたかったこと

  • VMの配布
    • 自分が配布するのはめんどくさい
    • 他人が配布したものに置き換えるのもめんどくさい
  • 他人がカスタマイズしたvimでの作業
    • 単に使いにくくて不快だった

取り組みについて

前提

  • システム全ての作り直しはしない

    • そんな時間も金もない
    • 最低限として「数年以内に破綻しないための施策」を検討
  • 今までの開発と並行して実施

    • 「内部改善をしてるのでエンハンスできません」はダメ

ゴール

開発のための環境的な話

  • VM配布を廃止し、dockerを利用できるようにする
    • 共用dockerイメージの作成
      • 動作環境としての仕組みだけを持たせて、DBの変更による更新は発生させない
    • 共用dockerイメージを使用した最低限の動作確認方法の確立

運用的な話

  • CIによるチェックで最低限の品質を担保する

  • デプロイスクリプトを作成する

  • デッドコードの削除

    • どこからも呼び出されてない処理の削除

やったこと

dockerイメージの作成

perl用イメージ

  • perlを実行するための環境をもつイメージ
    • plenvで特定のバージョンのperlをインストール
    • 共用開発環境にインストールされているモジュールのインストール

DB用イメージ

docker-compose.yml

  • perlイメージのDBイメージからコンテナを起動し、お互いに疎通できる状態を作る

メリット

  • 全員が同じ環境で動作確認を行うことになるので、実施者依存の変なことが起きない
    • 一度イメージを作成しておけば、よっぽどのことがないと更新作業は発生しない
    • VM配布廃止、不快なvimも使わずに済む

デメリット

  • レガシーシステムをやってる人だとdockerに触れたことがない人が多いかも
    • 「dockerよくわからん」って人でも容易に使えるように手順を固める必要はありそう

CI環境の構築

内容の定義

  1. 変更されたファイルに対して、 perl -cwで文法チェックを実施
    • そもそも実行できないものをここで弾く
  2. 変更されたファイルに対して、 perlcriticでlint
    • 推奨されない記述はここで弾く
  3. 変更されたファイルに対して、 perltidyフォーマットチェック
    • 動かすことはできるけど書き方に問題があるものはここで弾く
  4. リポジトリ全体のテストコードの実行
    • 既存処理に影響を与える変更をした場合はここで検知する

"変更されたファイルに対して" について

  • リポジトリ全体にperl -cwperlcriticの処理を実行すると大量のファイルで問題が検知される
    • 一つ一つ直すとエンハンスが止まるため、前提に反する
    • →何らかの変更を加えた際、その人の書いたコードに問題がなくても既存コードのせいでエラーになることがある
      • 運が悪かったと思ってその人が修正を実施するというルールを定めて運用
  • perltidyだけは最初に全体に適用した
    • 同様に大量のファイルで問題が検知されるが比較的影響は少なくすぐに対応が可能だったため

perltidyについて

  • perltidyは自動的にフォーマットを整えてくれるツールだと思っている
    • a.pl を対象に実行すると元ファイルは a.pl.bakみたいな名前に置き換えられ、 a.plは整形された状態になる
      • a.plが整形された状態であれば、 a.pl.bakは生成されない
    • なのでCI上では *.bakが存在する場合にエラーとしている
    • → CIで整形したものをcommitすればいいのでは?という声もあった
      • 同チーム内で 「perltidyで整形したものが信頼できないから一通り目で確認したい」という声もあった
      • 「信頼できない」と言っているものを自動commitさせる意味がわからなかったため却下とした
        • コードを書いた人間が責任を持って整形した状態をcommitすることとした

テストについて

  • 今までテストコードなんて書いたことがないという人ばかりだったので、実際にサンプルとしてテストコードを作成して動かした
  • 全ての既存コードのテストをいきなり書くことは無理なので、「今日以降変更したところはテストを書きましょう」というルールを定めて運用
    • ただしどうしてもテストコードを書く手間が大きい部分(画面操作のテストとか)は無理してテストコードは書かず、共用環境などで打鍵テストを行うこととした

メリット

  • 共用開発環境を使う前にある程度確認ができるため、共用環境が長時間占有されることがなくなった
  • → 品質担保、共用環境の待ち行列の解消

デメリット

  • 既存コードのせいでエンハンス時に余計な手間がかかる
    • 最初に perl -cw/perlcriticを実行し、エラーを解消してから実装に着手すると比較的マシ

CI環境のDBセットアップについて

  • セットアップ用のスクリプトを作成した
    • DDLが格納されたディレクトリ配下から findコマンドで .sqlなファイルを探して順に実行する
      • 実行順序の制御はファイル名で縛る( 001_hogehoge.sql, 002_fugafuga.sql, ... みたいに)

メリット

  • DB更新があってもdockerイメージ自体の更新は不要

デメリット

  • CIを実行するたびにテーブルから作り直すので若干時間がかかる

デプロイスクリプトの作成

スクリプトの処理内容

  • gitリポジトリの特定のブランチの状態を常に正とし、そのブランチに含まれるコードで全てを置き換える

置き換え処理について

メリット

  • デプロイ/切り戻しの作業工数の削減
  • 共用開発環境に蓄積されたゴミファイルによる影響を排除

デメリット

  • とくになし

デッドコードの削除

不要コード候補の抽出

  • 全ての関数定義と関数呼び出し処理の抽出を行い、愚直に調査を行った。
    • ひたすら grepを駆使
  • どこからも呼び出されてなさそうな処理を不要コード候補としてリストアップ

不要コード候補の監視

  • 不要コード候補にログ出力処理を追加する
    • 「ここの処理は不要コード候補だったけど、○○から呼び出されたぜ」みたいな
  • この状態で一定期間運用を続けて、仕込んだログが出力されなければ不要コードと判断する

不要コードの削除

  • 不要コードが確定したらコードを削除する
    • もしかしたら監視期間中に偶々呼び出されなかっただけで、実際には不要ではないコードを消してしまう可能性もあるので、切り戻し準備はしておく
    • そういうリスクがあることを理解し、過剰に恐れず対応を進めるしかない

繰り返し

  • 不要コードを削除することにより、再度不要コード候補が発生する可能性がある
  • 定期的に削除と抽出を繰り返していく必要がある

まとめ

  • 対応を実施してから3年くらい経つが問題は発生していないし、むしろテストを書いてたおかげでトラブルを事前に防ぐことができたこともある
  • とはいえ「やって!」と言ってやってくれる人なんていないと思うので、最初のうちは自らが行動を起こしていくしかない。具体的には以下のようなことをする。
    • 取り組みを理解してくれる味方をつくる
    • dockerイメージやCIの設定の作成
    • テストコードのサンプル作成
      • 既存コードのめっっっちゃ簡単な処理のテストでOK
    • perl -cwperlcriticでエラー対応方法をまとめる
      • 他の担当者の対応ハードルを下げる
  • 継続してもらうためにはできるだけめんどくさい手順を省くとよいかも
    • それ用のスクリプトを作っておくとか、タスクランナー的なツールを使うとか
    • 我々はMakefileにいろんな処理を定義してそれを使っている
      • make formatとか make testとかで必要な処理を自動化
  • 全てを作り直すことはできなくても最低限守るべきところを守ることで、作り直しができるようになるまでの延命は可能になると思う

参考

metacpan.org

metacpan.org

nextjs + react-chartjs-2 でウォーターフォールチャートを描く

やりたいこと

環境

  • "next": "13.4.9",
  • "chart.js": "^4.3.0",
  • "react-chartjs-2": "^5.2.0",

今回触れないこと

  • react / nextjs の話
  • tailwindcss の話
  • chart.js の話
  • TypeScriptの話(勉強中なので許して…)

実装

WaterfallChartのコンポーネントを作成

方針

  • 普通にBarChartを描くコードを作り、dataをいじるだけ

準備

$ npx create-next-app # nextjsアプリケーションの雛形を作成
$ cd <dir> # プロジェクトのディレクトリに移動

$ npm install --save chart.js react-chartjs-2 # 必要なパッケージのインストール

$ npm run dev # 起動

WaterfallChart コンポーネントの作成

まずは普通の棒グラフ

  • src/app/WaterfallChart.tsxを作成
"use client";
import { Bar } from "react-chartjs-2";
import {
  Chart as ChartJS,
  BarElement,
  CategoryScale,
  LinearScale,
  Title,
  Tooltip,
  Legend,
} from "chart.js";

ChartJS.register(
  CategoryScale,
  LinearScale,
  BarElement,
  Title,
  Tooltip,
  Legend
);

const options = {
  plugins: {
    title: {
      display: false,
    },
  },
  responsive: true,
  interaction: {
    mode: "index" as const,
    intersect: false,
  },
  scales: {
    x: {
      stacked: true,
    },
    y: {
      stacked: true,
    },
  },
};

const data = {
  labels: ["期初", "1Q末", "2Q末", "3Q末", "期末"],
  datasets: [
    {
      label: "在庫数",
      data: [100, 300, 200, 50, 200], // 数値は適当です。「在庫数がそんな遷移するわけねーだろ」などの指摘はしない
    },
  ],
};

export const WaterfallChart = () => {
  return <Bar options={options} data={data} />;
};
  • src/app/page.tsxを変更
    • 自動生成されたもののmain要素内を削除
      • tailwindcss関連は今回は変更していない
    • 作成したWaterfallChartを埋め込む
import { WaterfallChart } from "./WaterfallChart";

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <WaterfallChart />
    </main>
  );
}
  • 結果表示

棒グラフの表示確認

WaterfallChart形式にする

  • data.datasets[0].data を number[] から (number|number[])[]に変更する
--- a/src/app/WaterfallChart.tsx
+++ b/src/app/WaterfallChart.tsx
@@ -41,11 +41,18 @@ const options = {
 };
 
 const data = {
-  labels: ["期初", "1Q末", "2Q末", "3Q末", "期末"],
+  labels: ["期初", "1Q", "2Q", "3Q", "4Q", "期末"],
   datasets: [
     {
       label: "在庫数",
-      data: [100, 300, 200, 50, 200],
+      data: [
+        100, // number型の要素は通常の棒グラフとして描画
+        [100, 300], // number[]型の要素は0番目の要素~1番目の要素の区間のみをグラフに描画
+        [300, 200],
+        [200, 50],
+        [50, 200],
+        200,
+      ],
     },
   ],
 };
  • 結果表示

ウォーターフォールチャートの表示

増減による色の変更

  • 増加した期間は青、減少した期間は赤でグラフを表示する
diff --git a/src/app/WaterfallChart.tsx b/src/app/WaterfallChart.tsx
index df2f7e1..38f517e 100644
--- a/src/app/WaterfallChart.tsx
+++ b/src/app/WaterfallChart.tsx
@@ -53,10 +53,26 @@ const data = {
         [50, 200],
         200,
       ],
+      backgroundColor: backgroundColor(),
     },
   ],
 };
 
+function backgroundColor() { // datasets.dataの各要素の状態によって色を分岐
+  return (ctx: any) => {
+    if (!ctx.parsed._custom) { // number型要素の場合は黄色で表示
+      return "rgba(255, 255, 102, 0.5)";
+    }
+
+    const start = ctx.parsed._custom.start;
+    const end = ctx.parsed._custom.end;
+
+    return end >= start
+      ? "rgba(102, 178, 255, 0.5)" // 増加
+      : "rgba(255, 102, 178, 0.5)"; // 減少
+  };
+}
+
 export const WaterfallChart = () => {
   return <Bar options={options} data={data} />;
 };
  • 結果表示

色付きウォーターフォールチャート

参考

react-chartjs-2.js.org

www.youtube.com

mybatisのtypehandlerでEnumな値をgetしたりsetしたり

やりたいこと

  • mybatisのtypehandlerを使ってみる

実行環境

  • 前回と同じ
  • コードも前回のものからの差分のみを記載する

前提

そもそもtypehandlerって何

  • Javaで定義されているクラスをJDBCクラスへのマッピング処理を行う
    • DBに値を格納する場合、Javaのクラスの値をDBで扱える型に変換するみたいなイメージ(正確な表現ではないと思う)
    • DBから値を取り出す場合はその逆

どういうケースで使用するか

  • JDBC側でBLOB型として定義している値と、Java側で byte[]型として定義している値の相互変換をするとか
  • Java側でenum型で定義している値の変換

検証

やってみたいこと

  • 独自で定義したenum型のためのtypehandlerを作成し、データの取得や格納で使用する

検証用データ作成

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 STATUS (
  ID INTEGER NOT NULL,
  DESCRIPTION VARCHAR(32) NOT NULL,
  PRIMARY KEY (ID)
);

INSERT INTO STATUS (ID, DESCRIPTION) VALUES (0, 'アクティブ');
INSERT INTO STATUS (ID, DESCRIPTION) VALUES (9, 'ブロック');

CREATE TABLE USERS (
  USER_ID VARCHAR(32) NOT NULL,
  FIRST_NAME VARCHAR(32) NOT NULL,
  FAMILY_NAME VARCHAR(32) NOT NULL,
  DIVISION_ID CHAR(2) NOT NULL,
  STATUS INTEGER NOT NULL,
  PRIMARY KEY (USER_ID),
  FOREIGN KEY (DIVISION_ID) REFERENCES DIVISION (DIVISION_ID),
  FOREIGN KEY (STATUS) REFERENCES STATUS(ID)
);

INSERT INTO USERS (USER_ID, FIRST_NAME, FAMILY_NAME, DIVISION_ID, STATUS) VALUES ('hogefuga', 'ほげ', 'ふが', '10', 0);
INSERT INTO USERS (USER_ID, FIRST_NAME, FAMILY_NAME, DIVISION_ID, STATUS) VALUES ('foobar', 'ふー', 'ばー', '20', 9);

Userクラスの変更

  • Statusクラスを作成
public enum Status {
    ACTIVE(0), BLOCKED(9);

    private int id;

    Status(int id) {
        this.id = id;
    }
}
  • Userクラスにstatusフィールドを追加
--- 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
@@ -7,8 +7,8 @@ public class User {
   private final UserId userId;
   private final String firstName;
   private final String familyName;
-
   private Division division;
+  private Status status;
 
   public User(final UserId userId, final String firstName, final String familyName) {
     this.userId = userId;
@@ -18,7 +18,7 @@ public class User {
 
   public String toString() {
     return String.format(
-        "User {userId: %s, firstName: %s, familyName: %s, division: %s}",
-        userId, firstName, familyName, division);
+        "User {userId: %s, firstName: %s, familyName: %s, division: %s, status: %s}",
+        userId, firstName, familyName, division, status);
   }
 }

Statusを扱うtypehandlerを作成

  • BaseTypeHandlerインターフェースを実装する
  • とりあえずIDEが自動生成してくれるメソッドで記載
import com.github.nkiri.core.domain.model.user.Status;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class StatusTypeHandler extends BaseTypeHandler<Status> {
    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, Status status, JdbcType jdbcType) throws SQLException {

    }

    @Override
    public Status getNullableResult(ResultSet resultSet, String s) throws SQLException {
        return null;
    }

    @Override
    public Status getNullableResult(ResultSet resultSet, int i) throws SQLException {
        return null;
    }

    @Override
    public Status getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        return null;
    }
}

typehandlerの登録

  • mybatisがStatusを扱うときに、StatusTypeHandlerを使用するように設定する

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <typeHandlers>
    <typeHandler handler="com.github.nkiri.core.infrastructure.datastore.typehandler.StatusTypeHandler" javaType="com.github.nkiri.core.domain.model.user.Status"/>
  </typeHandlers>
</configuration>

applicationContext.xml

diff --git a/core/src/main/resources/applicationContext.xml b/core/src/main/resources/applicationContext.xml
index dca3030..770d657 100644
--- a/core/src/main/resources/applicationContext.xml
+++ b/core/src/main/resources/applicationContext.xml
@@ -28,6 +28,7 @@
 
   <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
     <property name="dataSource" ref="dataSource"/>
+    <property name="configLocation" value="classpath:mybatis-config.xml"></property>
   </bean>
 
   <mybatis:scan base-package="com.github.nkiri.core.infrastructure.datastore"/>

StatusTypeHandlerの実装

実装するべきメソッドについて

getNullableResult

Statusクラスにメソッドを追加

diff --git a/core/src/main/java/com/github/nkiri/core/domain/model/user/Status.java b/core/src/main/java/com/github/nkiri/core/domain/model/user/Status.java
index 0a24ebb..8866d56 100644
--- a/core/src/main/java/com/github/nkiri/core/domain/model/user/Status.java
+++ b/core/src/main/java/com/github/nkiri/core/domain/model/user/Status.java
@@ -1,5 +1,7 @@
 package com.github.nkiri.core.domain.model.user;
 
+import java.util.Arrays;
+
 public enum Status {
     ACTIVE(0), BLOCKED(9);
 
@@ -8,4 +10,13 @@ public enum Status {
     Status(int id) {
         this.id = id;
     }
+
+    public int getId() {
+        return id;
+    }
+
+    // StatusTypeHandlerのgetNullableResultで使用する
+    public static Status getStatus(int id) {
+        return Arrays.stream(values()).filter(v -> v.getId() == id).findFirst().get();
+    }
 }

getNullableResultを実装

diff --git a/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/typehandler/StatusTypeHandler.java b/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/typehandler/StatusTypeHandler.java
index 03cb667..2b35e2f 100644
--- a/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/typehandler/StatusTypeHandler.java
+++ b/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/typehandler/StatusTypeHandler.java
@@ -17,16 +17,16 @@ public class StatusTypeHandler extends BaseTypeHandler<Status> {
 
     @Override
     public Status getNullableResult(ResultSet resultSet, String s) throws SQLException {
-        return null;
+        return Status.getStatus(resultSet.getInt(s));
     }
 
     @Override
     public Status getNullableResult(ResultSet resultSet, int i) throws SQLException {
-        return null;
+        return Status.getStatus(resultSet.getInt(i));
     }
 
     @Override
     public Status getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
-        return null;
+        return Status.getStatus(callableStatement.getInt(i));
     }
 }

実行してみた結果

  • mainメソッドは前回とか前々回と同じ感じで、hogefugafoobarのデータを取得する
  • 実行結果
User {userId: hogefuga, firstName: ほげ, familyName: ふが, division: Division {10:人事部}, status: ACTIVE}
User {userId: foobar, firstName: ふー, familyName: ばー, division: Division {20:営業部}, status: BLOCKED}

Process finished with exit code 0

setNonNullParameter

準備(ステータス更新処理を作っておく)

  • UserMapperにupdateメソッドを追加する
--- 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,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;
+import org.apache.ibatis.annotations.Param;
 
 @Mapper
 public interface UserMapper {
   User get(UserId userId);
+  int update(@Param("user") User user);
 }
--- 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
@@ -19,6 +19,13 @@
     ;
   </select>

+  <update id="update" parameterType="map">
+    UPDATE USERS SET STATUS = #{user.status}
+    <where>
+      USER_ID = #{user.userId.id}
+    </where>
+  </update>
+
   <resultMap id="userIdResultMap" type="com.github.nkiri.core.domain.model.user.UserId">
     <constructor>
  • UserRepositoryにもメソッドを追加する
--- 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
@@ -6,4 +6,5 @@ import org.springframework.stereotype.Repository;
 public interface UserRepository {
 
   User get(UserId userId);
+  void update(User user);
 }
  • UserクラスにStatusのsetterメソッドを追加
--- 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
@@ -21,4 +21,8 @@ public class User {
         "User {userId: %s, firstName: %s, familyName: %s, division: %s, status: %s}",
         userId, firstName, familyName, division, status);
   }
+
+  public void setStatus(final Status status) {
+    this.status = status;
+  }
 }

setNonNullParameterの実装

--- a/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/typehandler/StatusTypeHandler.java
+++ b/core/src/main/java/com/github/nkiri/core/infrastructure/datastore/typehandler/StatusTypeHandler.java
@@ -12,7 +12,7 @@ import java.sql.SQLException;
 public class StatusTypeHandler extends BaseTypeHandler<Status> {
     @Override
     public void setNonNullParameter(PreparedStatement preparedStatement, int i, Status status, JdbcType jdbcType) throws SQLException {
-
+        preparedStatement.setInt(i, status.getId());
     }

     @Override

テスト用の呼び出し処理

@Service
public class SampleApplicationService {

  private final UserRepository userRepository;

  public SampleApplicationService(final UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  public void sample() {
    // 初期状態のデータを取得して表示
    User user1 = userRepository.get(new UserId("hogefuga"));
    System.out.println(user1);

    // statusを書き換えて保存
    user1.setStatus(Status.BLOCKED);
    userRepository.update(user1);

    // 更新後のデータを取得して表示
    User user1Updated = userRepository.get(new UserId("hogefuga"));
    System.out.println(user1Updated);

  }
}

実行してみた結果

  • ACTIVEだったものがBLOCKEDに書き変わっていることが確認できる
User {userId: hogefuga, firstName: ほげ, familyName: ふが, division: Division {10:人事部}, status: ACTIVE}
User {userId: hogefuga, firstName: ほげ, familyName: ふが, division: Division {10:人事部}, status: BLOCKED}

Process finished with exit code 0

まとめ

  • BaseTypeHandlerを使ってEnum型の変換を試した
  • mybatisにはEnumTypeHandlerというクラスがあるので、本来であればそっちを使うのが望ましい
  • 値をセットするときはPreparedStatement、値を取得するときはResultSetに対して、カラム名またはindexを指定して対象のデータにアクセスしているっぽいことがわかった

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