レガシーシステムと向き合う
この記事で扱うこと
- 自身が所属しているチームが開発・運用しているレガシーシステムに我慢ができなくなり、作り直しはできずともちょっとだけマシにするためにやったことのメモ
- 既に対応実施から数年たったが「以前のほうがよかった」という声は一切ないので、取り組みは成功だったと思っている
実施前の状態について
システムについて
開発するための環境
コーディング環境
秘伝のVM運用
-
- テーブルは作成済みで、データも投入済み(過去に使用したゴミデータも含まれる)
- DBに更新があった場合は誰かが更新を適用したVMイメージを作成してチーム内に配布
- テーブルは作成済みで、データも投入済み(過去に使用したゴミデータも含まれる)
コーディング規約
- 口伝のルールがあったらしいが、実質ないようなものだった
エディタ
コーディング環境の問題点
テスト環境
動作確認
自動テスト
- なし
テスト環境の問題点
- ローカルのVM環境でのテストが信頼できない
- その人の環境でだけ動くコードなどが発生する
- 共用開発環境は他の誰かが使っているときは使えない
- VM上に過去の作業によって生じたゴミデータが永遠に蓄積される
- DBにゴミを貯めたままVMイメージが配布される
- 簡単な関数処理の動作確認ですら共用開発環境で動かさないとミスに気付けない
- 想定していない箇所への影響が検知できない
運用
デプロイ
作業の流れ
- 更新対象のファイルを手動でリストアップ
- リストアップしたファイルについて、既存ファイルをサーバ上でバックアップ
- 何らかのサフィックスをつけてcpするだけ
- 更新対象ファイルを置き換え
- 切り戻しの際はリストアップしたファイルをバックアップファイルで置き換え
デプロイの問題点
- リストアップした一覧に漏れが生じる
- 作業を全て手作業で実施するため、ミスや実施漏れが生じる
- 前述の共用の開発環境へのデプロイや戻し作業も同様の方法で行うため、戻し作業にも漏れが生じる
- 後続作業で想定外の事象が起きた時の切り分けが困難
- ミス防止のため(?)、チームメンバー全員で作業の見守りをしていた
- 5人で60~90分くらい作業してたので工数的も問題あり
- それでもミスは頻発していたのでマジで無意味な時間だった
目的意識
やりたかったこと
デプロイ作業の簡易化
- 作業工数の削減
- ミスを削減したかった
自動テスト
- 簡単な処理の動作保証
コードフォーマットの自動化
静的解析
- 文法チェック
- インタプリタ言語がメインだったので実行前にエラーを検知
- linterの導入
- 文法チェック
やらないようにしたかったこと
取り組みについて
前提
システム全ての作り直しはしない
- そんな時間も金もない
- 最低限として「数年以内に破綻しないための施策」を検討
今までの開発と並行して実施
- 「内部改善をしてるのでエンハンスできません」はダメ
ゴール
開発のための環境的な話
- VM配布を廃止し、dockerを利用できるようにする
- 共用dockerイメージの作成
- 動作環境としての仕組みだけを持たせて、DBの変更による更新は発生させない
- 共用dockerイメージを使用した最低限の動作確認方法の確立
- 共用dockerイメージの作成
運用的な話
CIによるチェックで最低限の品質を担保する
- 文法チェック
- linter
- 単体テスト
デプロイスクリプトを作成する
- スクリプトを一つでファイル配置を実施
デッドコードの削除
- どこからも呼び出されてない処理の削除
やったこと
dockerイメージの作成
perl用イメージ
DB用イメージ
- DBインスタンスを起動するだけのイメージ
docker-compose.yml
- perlイメージのDBイメージからコンテナを起動し、お互いに疎通できる状態を作る
メリット
デメリット
- レガシーシステムをやってる人だとdockerに触れたことがない人が多いかも
- 「dockerよくわからん」って人でも容易に使えるように手順を固める必要はありそう
CI環境の構築
内容の定義
- 変更されたファイルに対して、
perl -cw
で文法チェックを実施- そもそも実行できないものをここで弾く
- 変更されたファイルに対して、
perlcritic
でlint- 推奨されない記述はここで弾く
- 変更されたファイルに対して、
perltidy
でフォーマットチェック- 動かすことはできるけど書き方に問題があるものはここで弾く
- リポジトリ全体のテストコードの実行
- 既存処理に影響を与える変更をした場合はここで検知する
"変更されたファイルに対して" について
- リポジトリ全体に
perl -cw
やperlcritic
の処理を実行すると大量のファイルで問題が検知される- 一つ一つ直すとエンハンスが止まるため、前提に反する
- →何らかの変更を加えた際、その人の書いたコードに問題がなくても既存コードのせいでエラーになることがある
- 運が悪かったと思ってその人が修正を実施するというルールを定めて運用
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セットアップについて
- セットアップ用のスクリプトを作成した
メリット
- DB更新があってもdockerイメージ自体の更新は不要
デメリット
- CIを実行するたびにテーブルから作り直すので若干時間がかかる
デプロイスクリプトの作成
スクリプトの処理内容
- gitリポジトリの特定のブランチの状態を常に正とし、そのブランチに含まれるコードで全てを置き換える
置き換え処理について
git clone
で取得したリポジトリはリポジトリ名_バージョン
のディレクトリに保存し、実行されるcgiやperlスクリプトはこのcloneしたリポジトリ内のファイルをシンボリックリンクで参照するdockerコンテナや共用開発環境へのデプロイも同じスクリプトで実施
メリット
デメリット
- とくになし
デッドコードの削除
不要コード候補の抽出
- 全ての関数定義と関数呼び出し処理の抽出を行い、愚直に調査を行った。
- ひたすら
grep
を駆使
- ひたすら
- どこからも呼び出されてなさそうな処理を不要コード候補としてリストアップ
不要コード候補の監視
- 不要コード候補にログ出力処理を追加する
- 「ここの処理は不要コード候補だったけど、○○から呼び出されたぜ」みたいな
- この状態で一定期間運用を続けて、仕込んだログが出力されなければ不要コードと判断する
不要コードの削除
- 不要コードが確定したらコードを削除する
- もしかしたら監視期間中に偶々呼び出されなかっただけで、実際には不要ではないコードを消してしまう可能性もあるので、切り戻し準備はしておく
- そういうリスクがあることを理解し、過剰に恐れず対応を進めるしかない
繰り返し
- 不要コードを削除することにより、再度不要コード候補が発生する可能性がある
- 定期的に削除と抽出を繰り返していく必要がある
まとめ
- 対応を実施してから3年くらい経つが問題は発生していないし、むしろテストを書いてたおかげでトラブルを事前に防ぐことができたこともある
- とはいえ「やって!」と言ってやってくれる人なんていないと思うので、最初のうちは自らが行動を起こしていくしかない。具体的には以下のようなことをする。
- 取り組みを理解してくれる味方をつくる
- dockerイメージやCIの設定の作成
- テストコードのサンプル作成
- 既存コードのめっっっちゃ簡単な処理のテストでOK
perl -cw
やperlcritic
でエラー対応方法をまとめる- 他の担当者の対応ハードルを下げる
- 継続してもらうためにはできるだけめんどくさい手順を省くとよいかも
- 全てを作り直すことはできなくても最低限守るべきところを守ることで、作り直しができるようになるまでの延命は可能になると思う
参考
nextjs + react-chartjs-2 でウォーターフォールチャートを描く
やりたいこと
- ウォーターフォールチャートを描きたい
- next.jsで
環境
- "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を埋め込む
- 自動生成されたもののmain要素内を削除
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} />; };
- 結果表示
参考
mybatisのtypehandlerでEnumな値をgetしたりsetしたり
やりたいこと
- mybatisのtypehandlerを使ってみる
実行環境
- 前回と同じ
- コードも前回のものからの差分のみを記載する
前提
そもそもtypehandlerって何
- Javaで定義されているクラスをJDBCクラスへのマッピング処理を行う
- DBに値を格納する場合、Javaのクラスの値をDBで扱える型に変換するみたいなイメージ(正確な表現ではないと思う)
- DBから値を取り出す場合はその逆
どういうケースで使用するか
検証
やってみたいこと
- 独自で定義した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メソッドは前回とか前々回と同じ感じで、
hogefuga
とfoobar
のデータを取得する - 実行結果
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
まとめ
springframeworkとmybatisでassociationを使用したマッピング
やりたいこと
前提
- コードは前回の記事で作ったものに変更を加えていく
実行環境
- 前回と同じ
検証
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についての話
- 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
修正
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
を参照してほしいのでこれだとダメ
- UserMapper.xml内で
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: ふが}
まとめ
OracleDatabase19cをOracleLinux8.5にインストールする
この記事の目的
- Oracle Databaseを使う仕事をしているのでちゃんと勉強をしようと思った
- 自宅で環境構築をしようとしたが、そもそもインストーラをGUIで起動する時点で詰まった
- サイレントインストールで構築することもできたが、EM Expressが構成されなかった
- これの原因はまだ調べられていない
- サイレントインストールで構築することもできたが、EM Expressが構成されなかった
- Oracle DBAの資格を取る人とかの助けになったら嬉しい
やりたかったこと
- Oracle Databaseのインストールとセットアップ
- OUIとかDBCAを使った環境構築
- Oracle Enterprise Manager Database Express(EM Express)の導入
- 触ってみたかっただけ
この記事に書かないこと
環境
バージョン情報
- 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関連ソフトウェアのインストール
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のパスワード
まとめ
令和から始めるJavaServletアプリケーション
この記事について
- Servletって大学のときに聞いたことあるけど、実際よくわかってない
- とりあえず簡単なものを動かしてみて、裏側の実装とかまで覗けたらいいなという気持ち
- たぶん今回は動かすだけ
まずは動かしてみる
環境
プロジェクトの作成
- 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を継承したクラスを作成する
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
- ブラウザから http://localhost:8080/servletapp/hi にアクセスして、
hello
と表示されればOK