メインコンテンツにスキップ

なぜDockerなのか?

· 9分の読み時間
Haril Song
Owner, Software Engineer at 42dot
情報

この記事は社内情報共有のために書かれており、Java開発環境を基に説明しています。

Dockerとは?

情報

Linuxコンテナを作成・使用するためのコンテナ技術であり、この技術をサポートする最大の企業の名前でもあり、オープンソースプロジェクトの名前でもあります。

deploy-history 誰もが一度はDockerを検索したときに見たことがある画像

2013年に導入されたDockerは、インフラの世界をコンテナ中心のものに変革しました。多くのアプリケーションがコンテナを使用してデプロイされ、Dockerfileを作成してイメージをビルドし、コンテナをデプロイすることが一般的な開発プロセスとなりました。2019年のDockerConプレゼンテーションでは、1052億回ものコンテナイメージのプルが報告されました。

Dockerを使用することで、非常に軽量なモジュール型の仮想マシンのようにコンテナを扱うことができます。さらに、コンテナは柔軟にビルド、デプロイ、コピー、移動が可能で、クラウド向けのアプリケーション最適化をサポートします。

Dockerコンテナの利点

どこでも一貫した動作

コンテナランタイムがインストールされている限り、Dockerコンテナはどこでも同じ動作を保証します。例えば、チームメンバーAがWindows OSを使用し、チームメンバーBがMacOSを使用している場合でも、Dockerfileを通じてイメージを共有することで、OSに関係なく同じ結果を確認できます。デプロイの場合も同様です。コンテナが正常に動作することが確認されていれば、追加の設定なしでどこでも正常に動作します。

モジュール性

Dockerのコンテナ化アプローチは、アプリケーションの一部を分解、更新、または回復する能力に焦点を当てています。サービス指向アーキテクチャ(SOA)のように、複数のアプリケーション間でプロセスを共有するマイクロサービスベースのアプローチを採用できます。

レイヤリングとイメージバージョン管理

各Dockerイメージファイルは一連のレイヤーで構成されており、これらが一つのイメージに結合されます。

Dockerは新しいコンテナをビルドする際にこれらのレイヤーを再利用するため、ビルドプロセスが非常に速くなります。中間の変更はイメージ間で共有され、速度、スケーラビリティ、効率が向上します。

高速なデプロイ

Dockerベースのコンテナはデプロイ時間を数秒に短縮できます。OSを起動してコンテナを追加または移動する必要がないため、デプロイ時間が大幅に短縮されます。さらに、高速なデプロイ速度により、コンテナによって生成されたデータの作成と削除がコスト効率よく簡単に行え、ユーザーはそれが正しく行われたかどうかを心配する必要がありません。

要するに、Docker技術は効率性を強調し、より細かく制御可能なマイクロサービスベースのアプローチを提供します

ロールバック

Dockerを使用してデプロイする際、イメージはタグ付きで使用されます。例えば、バージョン1.2のイメージを使用してデプロイし、リポジトリにバージョン1.1のイメージがまだある場合、jarファイルを再準備することなくコマンドを実行するだけで済みます。

docker run --name app image:1.2
docker stop app

## バージョン1.1を実行
docker run --name app image:1.1

Docker使用前後の比較

Dockerコンテナを使用することで、従来の方法に比べてはるかに迅速かつ柔軟なデプロイが可能になります。

Dockerコンテナを使用しないデプロイ

  1. ローカルマシンでデプロイするjarファイルをパッケージ化。
  2. scpなどのファイル転送プロトコルを使用してjarファイルを本番サーバーに転送。
  3. ステータス管理のためにsystemctlを使用してサービスファイルを作成。
  4. systemctl start appでアプリケーションを実行。

複数のアプリが1つのサーバーで実行されている場合、停止したアプリを見つけるのは非常に複雑になります。複数のサーバーで複数のアプリを実行する場合も同様で、各サーバーでコマンドを実行する必要があり、非常に疲れるプロセスです。

Dockerコンテナを使用したデプロイ

  1. Dockerfileを使用してアプリケーションのイメージを作成。→ ビルド ⚒️
  2. DockerhubやGitlabレジストリなどのリポジトリにイメージをプッシュ。→ シッピング🚢
  3. 本番サーバーでdocker run imageを使用してアプリケーションを実行。

複雑なパス設定やファイル転送プロセスに時間を浪費する必要はありません。Dockerはどの環境でも動作し、どこでも実行され、リソースを効率的に使用します。

Dockerは単一のコンテナを効果的に管理するように設計されています。しかし、数百のコンテナやコンテナ化されたアプリを使用し始めると、管理とオーケストレーションが非常に困難になります。すべてのコンテナにネットワーキング、セキュリティ、テレメトリなどのサービスを提供するためには、一歩引いてそれらをグループ化する必要があります。ここでKubernetes1が登場します。

いつ使用すべきか?

開発者はほぼすべての状況でDockerを非常に有用と感じるでしょう。実際、Dockerは開発、デプロイ、運用において従来の方法よりも優れていることが多いため、Dockerコンテナは常に最優先で検討すべきです。

  1. ローカルマシンでPostgreSQLのような開発データベースが必要なとき。
  2. 新しい技術をテストまたは迅速に採用したいとき。
  3. ローカルマシンに直接インストールまたはアンインストールが難しいソフトウェアがあるとき(例:WindowsでJavaを再インストールするのは悪夢です)。
  4. フロントエンドチームなど、他のチームから最新のデプロイバージョンをローカルマシンで実行したいとき。
  5. 本番サーバーをNCPからAWSに切り替える必要があるとき。

シンプルなAPIサーバー:

docker run --name rest-server -p 80:8080 songkg7/rest-server
# curlを使用
curl http://localhost/ping

# httpieを使用
http localhost/ping

ポート80がコンテナのポート8080にマッピングされているため、コンテナとの通信がうまくいくことが確認できます。

よく使われるDocker Runオプション

--name : コンテナに名前を付ける

-p : コンテナのポートをホストに公開する

--rm : コンテナが終了したときに自動的に削除する

-i : インタラクティブモード、アタッチされていなくてもSTDINを開いたままにする

-t : 擬似TTYを割り当て、ターミナルに似た環境を作成する

-v : ボリュームをバインドマウントする

結論

Dockerコンテナを使用することで、従来のデプロイ方法で発生する問題を解決しながら、便利な操作が可能になります。次回は、アプリケーションのイメージを作成するDockerfileについて見ていきます。

参考文献


Footnotes

  1. Kubernetes

Kubernetesを探る

· 6分の読み時間
Haril Song
Owner, Software Engineer at 42dot

Kubernetesとは?

Kubernetesは以下の機能を提供します:

  • サービスディスカバリとロードバランシング
  • ストレージオーケストレーション
  • 自動ロールアウトとロールバック
  • 自動ビンパッキング
  • 自動スケーリング
  • シークレットと設定管理

詳細については公式ドキュメントを参照してください。

Kubernetesを実行する方法はいくつかありますが、公式サイトではデモンストレーションにminikubeを使用しています。この記事では、Docker Desktopを使ったKubernetesの利用に焦点を当てます。minikubeの使い方を学びたい場合は、公式サイトを参照してください。

では、minikubeについて簡単に触れてみましょう。

Minikube

インストール

brew install minikube

使用方法

コマンドは直感的でシンプルなので、説明はほとんど不要です。

minikube start
minikube dashboard
minikube stop
# 使用後のリソースをクリーンアップ
minikube delete --all

利点

minikubeは、シークレットの設定などの詳細な設定が不要なため、開発目的に適しています。

欠点

一つの大きな欠点は、ダッシュボードを表示するコマンドがハングアップすることがある点です。この問題が主な理由で、この記事を書く際にはminikubeを使用していません。

Docker Desktop

インストール

Docker DesktopのメニューからKubernetesを有効にするだけです。

enable

ダッシュボード

Kubernetesダッシュボードはデフォルトでは有効になっていません。以下のコマンドで有効にできます:

kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.5.0/aio/deploy/recommended.yaml

ダッシュボードの起動

kubectl proxy

このリンクからダッシュボードにアクセスできます。

dashboard

ログインするにはトークンが必要です。トークンの作成方法を見てみましょう。

シークレット

まず、関連ファイルを別々に保存するためにkubernetesフォルダを作成します。

mkdir kubernetes && cd kubernetes
警告

ダッシュボードアカウントに管理者権限を付与することはセキュリティリスクを伴うため、実際の運用で使用する際には注意が必要です。

dashboard-adminuser.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
name: admin-user
namespace: kubernetes-dashboard
kubectl apply -f dashboard-adminuser.yaml

cluster-role-binding.yml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: admin-user
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: admin-user
namespace: kubernetes-dashboard
kubectl apply -f cluster-role-binding.yaml

トークンの作成

kubectl -n kubernetes-dashboard create token admin-user
eyJhbGciOiJSUzI1NiIsImtpZCI6IjVjQjhWQVdpeWdLTlJYeXVKSUpxZndQUkoxdzU3eXFvM2dtMHJQZGY4TUkifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjox7jU4NTA3NTY1LCJpYXQiOjE2NTg1MDM5NjUsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW4lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsInNlcnZpY2VhY2NvdW55Ijp7Im5hbWUiOiJhZG1pbi11c2VyIiwidWlkIjoiZTRkODM5NjQtZWE2MC00ZWI0LTk1NDgtZjFjNWQ3YWM4ZGQ3In19LCJuYmYiOjE2NTg1MDM5NjUsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlcm5ldGVzLWRhc2hib2FyZDphZG1pbi11c2VyIn1.RjoUaQnhTVKvzpAx_rToItI8HTZsr-6brMHWL63ca1_D4QIMCxU-zz7HFK04tCvOwyOTWw603XPDCv-ovjs1lM6A3tdgncqs8z1oTRamM4E-Sum8oi7cKnmVFSLjfLKqQxapBvZF5x-SxJ8Myla-izQxYkCtbWIlc6JfShxCSBJvfwSGW8c6kKdYdJv1QQdU1BfPY1sVz__cLNPA70_OpoosHevfVV86hsMvxCwVkNQHIpGlBX-NPog4nLY4gfuCMxKqjdVh8wLT7yS-E3sUJiXCcPJ2-BFSen4y-RIDbg18qbCtE3hQBr033Mfuly1Wc12UkU4bQeiF5SerODDn-g

生成されたトークンを使用してログインします。

welcome-view アクセス成功!

デプロイメントの作成

イメージを使用してデプロイメントを作成します。この記事では、事前に準備されたgolangを使用したウェブサーバーを使用します。

kubectl create deployment rest-server --image=songkg7/rest-server

コマンドが正常に実行されると、ダッシュボードでの変更を簡単に監視できます。

create-deployment デプロイメント作成後、ダッシュボードが即座に更新されます。

しかし、CLIを使用してこれを確認する方法も学びましょう(根本的な方法です...!)。

ステータスの確認

kubectl get deployments

get-deployment

デプロイメントが作成されると、ポッドも同時に生成されます。

kubectl get pods -o wide

get-pods

すべてが正常に動作していることを確認したら、ウェブサーバーにリクエストを送信してみましょう。curlの代わりにhttpie1を使用します。curlに慣れている場合は、それを使用しても構いません。

http localhost:8080/ping

error

すべてが正常に動作しているように見えるのに、なぜ応答を受け取れないのでしょうか? 🤔

これは、サービスがまだ外部に公開されていないためです。デフォルトでは、Kubernetesのポッドは内部でのみ通信できます。サービスを外部に公開しましょう。

サービスの公開

kubectl expose deployment rest-server --type=LoadBalancer --port=8080

サービスがポート8080を使用しているため、このポートを開きます。異なるポートを使用すると接続に問題が生じる可能性があります。

では、再度リクエストを送信してみましょう。

http localhost:8080/ping

200

成功した応答を受け取ることができます。

参考資料


Footnotes

  1. Elegant httpie

[Java] コレクションをよりコレクションらしくする - Iterable

· 4分の読み時間
Haril Song
Owner, Software Engineer at 42dot

概要

// Iterableを実装するJavaのコレクション。
public interface Collection<E> extends Iterable<E>

ファーストクラスコレクションはオブジェクトを扱う上で非常に便利な方法です。しかし、「ファーストクラスコレクション」という名前にもかかわらず、実際にはCollectionをフィールドとして保持しているだけで、実際にはCollectionではないため、Collectionが提供するさまざまなメソッドを使用することはできません。この記事では、Iterableを使用してファーストクラスコレクションをより実際のCollectionに近づける方法を紹介します。

簡単な例を見てみましょう。

@Value
public class LottoNumber {
int value;

public static LottoNumber create(int value) {
return new LottoNumber(value);
}
}
public class LottoNumbers {

private final List<LottoNumber> lottoNumbers;

private LottoNumbers(List<LottoNumber> lottoNumbers) {
this.lottoNumbers = lottoNumbers;
}

public static LottoNumbers create(LottoNumber... numbers) {
return new LottoNumbers(List.of(numbers));
}

// Listのメソッドを使用するためにisEmpty()メソッドを委譲します。
public boolean isEmpty() {
return lottoNumbers.isEmpty();
}
}

LottoNumbersLottoNumberをリストとして保持するファーストクラスコレクションです。リストが空かどうかを確認するために、isEmpty()を実装しています。

isEmpty()の簡単なテストを書いてみましょう。

@Test
void isEmpty() {
LottoNumber lottoNumber = LottoNumber.create(7);
LottoNumbers lottoNumbers = LottoNumbers.create(lottoNumber);

assertThat(lottoNumbers.isEmpty()).isFalse();
}

悪くはありませんが、AssertJはコレクションをテストするためのさまざまなメソッドを提供しています。

  • has..
  • contains...
  • isEmpty()

ファーストクラスコレクションはCollectionではないため、これらの便利なアサートメソッドを使用することはできません。

より正確には、iterator()がないと要素を反復処理できないため、これらを使用することができません。iterator()を使用するには、Iterableを実装するだけです。

実装は非常に簡単です。

public class LottoNumbers implements Iterable<LottoNumber> {

//...

@Override
public Iterator<LottoNumber> iterator() {
return lottoNumbers.iterator();
}
}

ファーストクラスコレクションはすでにCollectionを持っているので、isEmpty()を委譲したのと同じように、単にそれを返すだけです。

@Test
void isEmpty_iterable() {
LottoNumber lottoNumber = LottoNumber.create(7);
LottoNumbers lottoNumbers = LottoNumbers.create(lottoNumber);

assertThat(lottoNumbers).containsExactly(lottoNumber);
assertThat(lottoNumbers).isNotEmpty();
assertThat(lottoNumbers).hasSize(1);
}

これでさまざまなテストメソッドを使用できるようになりました。

テストだけでなく、機能の実装においても便利に使用できます。

for (LottoNumber lottoNumber : lottoNumbers) {
System.out.println("lottoNumber: " + lottoNumber);
}

これはforループがiterator()を使用するため可能です。

結論

Iterableを実装することで、より豊富な機能を使用することができます。実装は難しくなく、機能拡張に近いので、ファーストクラスコレクションを持っている場合は積極的にIterableを活用しましょう。

エレガントなHTTP CLI、HTTPie

· 2分の読み時間
Haril Song
Owner, Software Engineer at 42dot

概要

curlコマンドを置き換えることができるCLIツール

Linuxを頻繁に使用する開発者であれば、curlコマンドをよく使うでしょう。サーバーから外部APIリクエストを送信するための必須コマンドですが、出力の可読性が低いという欠点があります。HTTPieはこの欠点を解消できる興味深いツールなので、紹介しましょう。

インストール

Macユーザーの場合、brewを使って簡単にインストールできます。

brew install httpie

CentOSの場合、yumを使ってインストールできます。

yum install epel-release
yum install httpie

使用方法

まず、curlを使ってGETリクエストを送信する方法です。

curl https://httpie.io/hello

curl-get

次に、HTTPieを使って比較してみましょう。

https httpie.io/hello

get

コマンドのあらゆる面で可読性が大幅に向上しています。レスポンスとヘッダーの値がデフォルトで含まれているため、別のコマンドを使用せずに一目でさまざまな情報を得ることができます。

コマンドではhttpshttpが区別されることに注意してください。

http localhost:8080

公式サイトに記載されているように、POSTリクエストを送信することもできます。

http -a USERNAME POST https://api.github.com/repos/httpie/httpie/issues/83/comments body='HTTPie is awesome! :heart:'

その他のさまざまな機能についてはGitHubで説明されているので、うまく活用すれば生産性を大幅に向上させることができます。

参考

ゲッターとセッターに関する真実と誤解

· 6分の読み時間
Haril Song
Owner, Software Engineer at 42dot

Googleで「getter/setter」を検索すると、数多くの記事が見つかります。その多くは、カプセル化や情報隠蔽といったキーワードに焦点を当てて、getter/setterを使用する理由を説明しています。

一般的な説明では、フィールド変数をprivateとして宣言し、外部からのアクセスを防ぎ、getter/setterを通じてのみ公開することでカプセル化が達成されるとされています。

しかし、getter/setterを使用することで本当にデータをカプセル化できるのでしょうか?

実際には、getter/setterではカプセル化を全く達成できません。 カプセル化を達成するためには、ゲッターとセッターの使用を避けるべきです。これを理解するためには、カプセル化の明確な理解が必要です。

カプセル化とは?

オブジェクト指向プログラミングにおけるカプセル化には、オブジェクトの属性(データフィールド)と動作(メソッド)を一緒にまとめることと、オブジェクトの実装の詳細を内部に隠すことの二つの側面があります。 - Wikipedia

カプセル化とは、外部のエンティティがオブジェクトの内部属性を完全に知ることができないようにすることを意味します。

なぜゲッターとセッターはカプセル化を達成できないのか

学んだように、カプセル化は外部のエンティティがオブジェクトの内部属性を知ることができないようにすることを指します。しかし、getter/setterは特定のフィールドが存在することを外部に露呈しています。例を見てみましょう。

public class Student {

private String name;
private int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String introduce() {
return String.format("私の名前は%sで、年齢は%d歳です。", name, age);
}
}
class StudentTest {

@Test
void student() {
Student student = new Student();
student.setName("ジョン");
student.setAge(20);
String introduce = student.introduce();

assertThat(student.getName()).isEqualTo("ジョン");
assertThat(student.getAge()).isEqualTo(20);
assertThat(introduce).isEqualTo("私の名前はジョンで、年齢は20歳です。");
}
}

Studentクラスの外部から、そのクラスにはnameageという属性があることが明らかです。この状態をカプセル化されていると考えられるでしょうか?

もしStudentからage属性を削除した場合、getter/setterを使用しているすべての場所で変更が必要になります。これにより強い結合が生じます。

真のカプセル化とは、オブジェクトの内部構造の変更が外部のエンティティに影響を与えないことを意味します。公開インターフェースを除いて。

内部実装を隠してみましょう。

public class Student {

private String name;
private int age;

public Student(String name, int age) {
this.name = name;
this.age = age;
}

public String introduce() {
return String.format("私の名前は%sで、年齢は%d歳です。", name, age);
}
}
class StudentTest {

@Test
void student() {
Student student = new Student("ジョン", 20);
String introduce = student.introduce();

assertThat(introduce).isEqualTo("私の名前はジョンで、年齢は20歳です。");
}
}

このように、オブジェクトは公開インターフェースを通じて内部実装を露呈しません。どのようなデータを保持しているかを知ることができず、変更も防ぎ、メッセージを通じてのみ通信します。

結論

カプセル化はオブジェクト指向設計において重要なトピックであり、外部要因に依存しない設計を強調します。カプセル化のレベルについては意見が分かれ、gettersetterの両方を使用しないことを推奨する人もいれば、getterの使用は許容されるとする人もいます。

個人的には、可能な限りgetterの使用を避けるべきだと考えていますが、特にテストにおいては、ゲッターやセッターがあるとテストコードの記述が容易になる場合があります。カプセル化のレベルを決定するには、現在の状況や開発中のコードの目的に依存します。

良い設計は常にトレードオフの過程を経て生まれます。

情報

すべてのサンプルコードはGitHubで確認できます。

「sitemap.xmlが見つかりません」問題の解決方法

· 1分の読み時間
Haril Song
Owner, Software Engineer at 42dot

ブログのインデックスをGoogleに登録するためにsitemap.xmlを登録していましたが、「sitemapが見つかりません」というエラーメッセージばかりが表示されていました。最終的に解決方法を見つけたので、ここで共有します。

この方法がすべてのケースで解決するわけではありませんが、試してみる価値はあると思います。

以下のコマンドを実行するだけです:

curl https://www.google.com/ping\?sitemap\={あなたのsitemapのパス}

そして、再度サーチコンソールを確認すると...!

sitemap-success ほぼ1ヶ月かかってやっと解決しました...😢

ついにsitemapが認識されました。

お役に立てれば幸いです!

参考

[Spring Batch] KafkaItemReader

· 3分の読み時間
Haril Song
Owner, Software Engineer at 42dot
情報

この記事を書く前にDockerを使ってKafkaをインストールしましたが、その内容はここでは扱いません。

KafkaItemReaderとは..?

Spring Batchでは、Kafkaトピックからデータを処理するためにKafkaItemReaderが提供されています。

簡単なバッチジョブを作成してみましょう。

まず、必要な依存関係を追加します。

dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-batch'
implementation 'org.springframework.kafka:spring-kafka'
...
}

application.ymlにKafkaの設定を行います。

spring:
kafka:
bootstrap-servers:
- localhost:9092
consumer:
group-id: batch
@Slf4j
@Configuration
@RequiredArgsConstructor
public class KafkaSubscribeJobConfig {

private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
private final KafkaProperties kafkaProperties;

@Bean
Job kafkaJob() {
return jobBuilderFactory.get("kafkaJob")
.incrementer(new RunIdIncrementer())
.start(step1())
.build();
}

@Bean
Step step1() {
return stepBuilderFactory.get("step1")
.<String, String>chunk(5)
.reader(kafkaItemReader())
.writer(items -> log.info("items: {}", items))
.build();
}

@Bean
KafkaItemReader<String, String> kafkaItemReader() {
Properties properties = new Properties();
properties.putAll(kafkaProperties.buildConsumerProperties());

return new KafkaItemReaderBuilder<String, String>()
.name("kafkaItemReader")
.topic("test") // 1.
.partitions(0) // 2.
.partitionOffsets(new HashMap<>()) // 3.
.consumerProperties(properties) // 4.
.build();
}
}
  1. データを読み取るトピックを指定します。
  2. トピックのパーティションを指定します。複数のパーティションを指定することも可能です。
  3. KafkaItemReaderでオフセットを指定しない場合、オフセット0から読み取ります。空のマップを提供すると、最後のオフセットから読み取ります。
  4. 実行に必要なプロパティを設定します。
ヒント

KafkaPropertiesは、SpringでKafkaを便利に使用するためのさまざまな公開インターフェースを提供します。

試してみる

さて、バッチジョブを実行すると、application.ymlの情報に基づいてconsumer groupsが自動的に作成され、ジョブがトピックの購読を開始します。

kafka console producerを使って、testトピックに1から10までのデータを追加してみましょう。

kafka-console-producer.sh --bootstrap-server localhost:9092 --topic test

produce-topic

バッチジョブがトピックを正常に購読していることがわかります。

subscribe-batch

chunkSizeを5に設定したので、データは5件ずつバッチ処理されます。

ここまで、Spring BatchでのKafkaItemReaderの基本的な使い方を見てきました。次に、テストコードの書き方を見てみましょう。

Qodanaで簡単に静的コード解析を行う方法

· 3分の読み時間
Haril Song
Owner, Software Engineer at 42dot

Qodanaとは?

Qodanaは、JetBrainsが提供するコード品質向上ツールです。非常に使いやすいので、簡単に紹介したいと思います。

まず、Dockerがインストールされた環境が必要です。

docker run --rm -it -p 8080:8080 \
-v <source-directory>/:/data/project/ \
-v <output-directory>/:/data/results/ \
jetbrains/qodana-jvm --show-report

私はJavaアプリケーションを解析しているので、jvmイメージを使用しました。別の言語を使用している場合は、Qodanaのウェブサイトで適切なイメージを見つけることができます。

  • <source-directory>には解析したいプロジェクトのパスを置き換えてください。
  • <output-directory>には解析結果を保存するパスを入力してください。これについては後ほど説明します。

解析結果を保存するために、ルートディレクトリにqodanaというフォルダを作成しました。

mkdir ~/qodana
# そして<output-directory>を~/qodanaに置き換えます。

次に、上記のdocker run ~コマンドを実行し、しばらく待つと以下のような結果が表示されます。

私はテスト用にシンプルなJavaアプリケーションを使用しました。

image

今、http://localhost:8080 にアクセスすると、コード解析の結果を見ることができます。

image1

Dockerがインストールされていれば、現在のプロジェクトのコード解析結果を簡単に取得できます。

このような解析ツールはコードレビューの一形態として機能し、レビュアーの疲労を軽減し、より詳細なレビューに集中できるようにします。このようなコード品質管理ツールを積極的に活用することで、非常に便利な開発体験を得ることができます。

[Spring Batch] カスタム制約ライターの実装

· 6分の読み時間
Haril Song
Owner, Software Engineer at 42dot

状況 🧐

最近、特定のロジックのために PostgreSQLUpsert を使用するバッチプロセスを設計しました。実装中に、ビジネス要件の変更により、複合一意条件に特定のカラムを追加する必要がありました。

問題は、複合一意カラムの一意制約が、特定のカラムに null 値が含まれている場合に重複を防止しないことから発生しました。

問題の状況を例で見てみましょう。

create table student
(
id integer not null
constraint student_pk
primary key,
name varchar,
major varchar,
constraint student_unique
unique (name, major)
);
idnamemajor
1songkorean
2kimenglish
3parkmath
4kimNULL
5kimNULL

null の重複を避けるために、ダミーデータを挿入するというアイデアが自然に浮かびましたが、データベースに意味のないデータを保存するのは気が進みませんでした。特に、null が発生するカラムが UUID のような複雑なデータを保存する場合、他の値の中に埋もれた意味のない値を識別するのは非常に困難です。

少し面倒ではありますが、unique partial index を使用することで、ダミーデータを挿入せずに null 値を許可しないようにすることができます。私は、たとえ挑戦的であっても、最も理想的な解決策を追求することにしました。

解決策

部分インデックス

CREATE UNIQUE INDEX stu_2col_uni_idx ON student (name, major)
WHERE major IS NOT NULL;

CREATE UNIQUE INDEX stu_1col_uni_idx ON student (name)
WHERE major IS NULL;

PostgreSQL は部分インデックスの機能を提供しています。

部分インデックス : 特定の条件が満たされた場合にのみインデックスを作成する機能。インデックスの範囲を絞ることで、効率的なインデックス作成とメンテナンスが可能になります。

name のみの値が挿入される場合、stu_1col_uni_idxmajornull の同じ name を持つ行を1行のみ許可します。2つの補完的なインデックスを作成することで、特定のカラムに null 値が含まれる重複を巧妙に防ぐことができます。

duplicate error major がない値を保存しようとするとエラーが発生します

しかし、このように2つの一意制約がある場合、Upsert 実行中に1つの制約チェックしか許可されないため、バッチは意図した通りに実行されませんでした。

多くの検討の末、SQLを実行する前に特定の値が欠落しているかどうかを確認し、条件を満たすSQLを実行することにしました。

SelectConstraintWriter の実装

public class SelectConstraintWriter extends JdbcBatchItemWriter<Student> {

@Setter
private String anotherSql;

@Override
public void write(List<? extends Student> items) {
if (items.isEmpty()) {
return;
}

List<? extends Student> existMajorStudents = items.stream()
.filter(student -> student.getMajor() != null)
.collect(toList());

List<? extends Student> nullMajorStudents = items.stream()
.filter(student -> student.getMajor() == null)
.collect(toList());

executeSql(existMajorStudents, sql);
executeSql(nullMajorStudents, anotherSql);
}

private void executeSql(List<? extends student> students, String sql) {
if (logger.isDebugEnabled()) {
logger.debug("Executing batch with " + students.size() + " items.");
}

int[] updateCounts;

if (usingNamedParameters) {
if (this.itemSqlParameterSourceProvider == null) {
updateCounts = namedParameterJdbcTemplate.batchUpdate(sql, students.toArray(new Map[students.size()]));
} else {
SqlParameterSource[] batchArgs = new SqlParameterSource[students.size()];
int i = 0;
for (student item : students) {
batchArgs[i++] = itemSqlParameterSourceProvider.createSqlParameterSource(item);
}
updateCounts = namedParameterJdbcTemplate.batchUpdate(sql, batchArgs);
}
} else {
updateCounts = namedParameterJdbcTemplate.getJdbcOperations().execute(sql,
(PreparedStatementCallback<int[]>) ps -> {
for (student item : students) {
itemPreparedStatementSetter.setValues(item, ps);
ps.addBatch();
}
return ps.executeBatch();
});
}

if (assertUpdates) {
for (int i = 0; i < updateCounts.length; i++) {
int value = updateCounts[i];
if (value == 0) {
throw new EmptyResultDataAccessException("Item " + i + " of " + updateCounts.length
+ " did not update any rows: [" + students.get(i) + "]", 1);
}
}
}
}
}

以前使用していた JdbcBatchItemWriterwrite メソッドをオーバーライドすることでこれを実装しました。コード内で major の存在を確認し、適切なSQLを選択して実行することで、duplicateKeyException に遭遇することなく Upsert ステートメントが正しく動作するようにします。

使用例は以下の通りです:

@Bean
SelectConstraintWriter studentItemWriter() {
String sql1 =
"INSERT INTO student(id, name, major) "
+ "VALUES (nextval('hibernate_sequence'), :name, :major) "
+ "ON CONFLICT (name, major) WHERE major IS NOT NULL "
+ "DO UPDATE "
+ "SET name = :name, "
+ " major = :major";

String sql2 =
"INSERT INTO student(id, name, major) "
+ "VALUES (nextval('hibernate_sequence'), :name, :major) "
+ "ON CONFLICT (name) WHERE major IS NULL "
+ "DO UPDATE "
+ "SET name = :name, "
+ " major = :major";

SelectConstraintWriter writer = new SelectConstraintWriter();
writer.setSql(sql1);
writer.setAnotherSql(sql2);
writer.setDataSource(dataSource);
writer.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>());
writer.afterPropertiesSet();
return writer;
}

結論

PostgreSQLUpsert 実行中に複数の制約チェックを許可していれば、ここまでの手間をかける必要はなかったのは残念です。将来のバージョンでの更新を期待しています。


参考

create unique constraint with null columns

[Kotlin] 中置関数

· 2分の読み時間
Haril Song
Owner, Software Engineer at 42dot

Kotlinでは、中置関数と呼ばれる関数の定義方法があります。これは、Javaを主要な言語として使用していた時には想像もできなかった構文です。Kotlinを始めたばかりの方に向けて、これを紹介しましょう。

単一のパラメータを持つメンバー関数は、中置関数に変換することができます。

中置関数の代表的な例の一つに、標準ライブラリに含まれている to 関数があります。

val pair = "Ferrari" to "Katrina"
println(pair)
// (Ferrari, Katrina)

必要に応じて、to のような新しい中置関数を定義することもできます。例えば、Int を次のように拡張することができます:

infix fun Int.times(str: String) = str.repeat(this)
println(2 times "Hello ")
// Hello Hello

to を新しい中置関数 onto として再定義したい場合は、次のように書くことができます:

infix fun String.onto(other: String) = Pair(this, other)
val myPair = "McLaren" onto "Lucas"
println(myPair)
// (McLaren, Lucas)

このようなKotlinの構文により、非常に独特なコーディング方法が可能になります。

class Person(val name: String) {
val likedPeople = mutableListOf<Person>()

infix fun likes(other: Person) {
likedPeople.add(other)
}
}

fun main() {
val sophia = Person("Sophia")
val claudia = Person("Claudia")

sophia likes claudia // !!
}

参考

Kotlin Docs