chromeでも利用可能なオレオレ証明書をワンライナーで作成する

システム開発部の @nukisashineko (ぬさし) です。

オレオレ証明書の作成が年々面倒になってたりします。
特に chrome は厳しくて、chromeでも利用できるオレオレ証明書をワン・コマンドで作成できる方法はなかなかありません。

今回は、chrome でも 利用可能なオレオレ証明書をサクッと作成できる方法が判明しました。 コマンドについて共有させていただこうと思います。

作成コマンド

mkdir -p certs && cd certs

# 作りたいオレオレ証明書のドメイン
export MY_DOMAIN=xxxx.examle.com 

# Key Access で表示される 表示名
export MY_CA_NAME=xxxx-example-com-ca

# オレオレ証明書を作成 ( ※ 期限は 1 年)
mkdir -p ${MY_DOMAIN}/certs && \
    docker run --rm -it \
        --name certs \
        -v $(pwd)/${MY_DOMAIN}/certs:/certs \
        -e SSL_SUBJECT=${MY_DOMAIN} \
        -e SSL_DNS=${MY_DOMAIN} \
        -e CA_EXPIRE=365 \
        -e SSL_EXPIRE=365 \
        -e CA_SUBJECT=${MY_CA_NAME} \
        stakater/ssl-certs-generator:1.0
  • stakater/ssl-certs-generator:1.0 を利用するのが肝です
    • 有名な paulczar/omgwtfssl は 最新のchrome に対応してません。

作成されたファイルの確認

tree .
.
└── xxxx.examle.com
    └── certs
        ├── ca-key.pem
        ├── ca.pem
        ├── ca.srl
        ├── cert.pem
        ├── key.csr
        ├── key.pem
        └── openssl.cnf

オレオレ証明書を keychain accessコマンドラインから追加

sudo security add-trusted-cert -d \
  -r trustRoot \
  -k /Library/Keychains/System.keychain \
  ./xxxx.examle.com/certs/ca.pem

keychain access を確認

f:id:hibikiosawa4388:20220124151512j:plain
keychain_access_only_example_com

参考資料

第5回定期勉強会「Clean Architecture勉強会」

f:id:e125731:20220121183201p:plain
第5回定期勉強会「Clean Architecture勉強会」

こんにちは。エキサイトのあはれんです。

今年第1回目の定期勉強会のお題は「Clean Architecture」でした。

最近、カンファレンスや求人サイト等でも、Clean Architectureという文字はよく見かけるようになったと思います。 弊社でもClean Architectureを参考にしたプロジェクトが増えております。 それでも、全メンバーが知っているわけではありませんので、 Clean Architectureにそってプロジェクト開発しているメンバーに説明いただきました。

なお過去開催分は以下になりますので、よければ御覧ください。

tech.excite.co.jp

勉強会内容

www.amazon.co.jp

Clean Architectureの本の内容にそいながら、 「SOLID原則」「非循環依存関係の原則」「安定依存の原則」「安定度・抽象度等価の原則」について説明いただきました。 Clean Architectueとは、どんなソフトウェアにも適用可能な共通のルールのことであり、 依存の方向は常に内側制御の流れと依存方向は分離して考えることが大事と説明いただきました。

質問タイムには、 「クリーンアーキテクチャを気にせず、フレームワークに依存しながらゴリゴリ書いたほうが早いのではないか?」という質問があり、「初速はゴリゴリ書いたほうが早く思えるが、サービスが成長し複雑になっていくと改修が大変になるので、 クリーンアーキテクチャになるように書いたほうが良く、実際にクリーンアーキテクチャのルールに則って書いたほうがコードは読みやすくなった。」と回答されていました。

話を聞いて私は、Clean Architectueのルールに則って実装するのは難しく時間がかかりそうですが、 挑戦したほうが、サービスの成長を止めないアーキテクチャになりそうだと思いました。挑戦していきたいです!

最後に

今回の勉強会は、知らない人にとっては知識を得ることができ、 知っていた人にとっては改めて理解を深める機会になったのではないかと思います。 今後とも様々な勉強会を開催していくのでよろしくおねがいします。

quarkusを使う(ExceptionMapper編)

こんばんは

お久しぶりです。エキサイト株式会社 中尾です。

最近は趣味でQuarkusを使っています。 本記事ではQuarkusの例外処理について紹介します。

Red Hatの皆様、コメントください、DM待っています。

今回はエラーのハンドリングということで、exceptionのcustom handlerを作っていきます。

ExceptionMapperを継承して、BadRequestExceptionを発生させます。

package org.my.hobby.controller.exception.handler;

import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class BadRequestExceptionHandler implements ExceptionMapper<BadRequestException> {

    @Override
    public Response toResponse(BadRequestException e) {
        return Response.status(Response.Status.BAD_REQUEST).
                entity(new ErrorMessage(e.getMessage())).build();
    }
}
package org.my.hobby.controller.exception.handler;

import java.io.Serializable;

public class BadRequestException extends
        RuntimeException implements Serializable {

    private static final long serialVersionUID = 1L;

    public BadRequestException() {
    }

    public BadRequestException(String message) {
        super(message);
    }

    public BadRequestException(String message, Throwable cause) {
        super(message, cause);
    }

    public BadRequestException(Throwable cause) {
        super(cause);
    }

    public BadRequestException(String message, Throwable cause,
                               boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}
package org.my.hobby.controller.exception.handler;

public record ErrorMessage(
        String message
) {
}

これでcontroller内でthrow new BadRequestException("invalid emai");などで、ErrorMessageを返すことができます。

SpringBootのプラグインを入れたらControllerAdviceも使えますが、使いません(不自由を楽しむのもまた一興)

次回はこっちでやっていきます。

https://quarkus.io/guides/resteasy-reactive

【Canva】ブログ用テンプレ素材をCanvaで作ってみた

f:id:excite_ny:20220120173838p:plain

はじめに

こんにちは!21卒デザイナーの山崎です。

今回は「テックブログのアイキャッチ画像をCanva化+テンプレ化して、誰でも簡単にアイキャッチ画像を作れるようにした話」をしようと思います。

Figmaでテンプレを作る問題点

今までブログ用アイキャッチ画像のテンプレートはあったのですが、そのデータ全てはFigmaにあった為、誰でも使えるという状況ではありませんでした。

f:id:excite_ny:20220119180723p:plain
これまで投稿した記事の表紙がFigmaに格納されています。

f:id:excite_ny:20220119180811p:plain
学生時代に自作した素材たちです。これを使って普段アイキャッチ画像を制作しています。

Figmaでテンプレートを作る問題点は2点ありました。

Figmaを誰でも自在に使えるわけではない

②権限問題で誰でも編集できるわけではない

①はFigmaはエンジニアやビジネスにとってはハードルが高いツールであり、テンプレートが置いてあっても「誰でも」「すぐに」使えるという点においてFigmaは適したものではありませんでした…

②はエキサイトではFigmaを企業用の「organization plan」で使っており、編集権限はクリエイティブ職のみなのでエンジニア・ビジネスはこのデータにアクセスしても閲覧しかできず、「データをコピーして一旦自分のワークスペースに持ち帰らなければいけない」という一手間がありました。

編集権限を与えると月一人当たり4500円かかってしまうので気軽に付与することもできず…

Canvaにした理由

Canvaを採用した理由は3点あります。

① 誰でも直感的に操作できる

②テンプレート機能で誰でも簡単に使える

②Canva Proが最大5人で使えて月200円/人

Canvaはバナー制作に特化している為、Figmaに比べて初心者にも直感的に扱うことができ、テンプレートも豊富でフォントも309種類使うことができます。(個人的にフォントワークスのフォントがあらかじめ揃ってるのはすごくよかったです)

テンプレートの制作

ただツールをCanvaに切り替えるだけではなく、テンプレートを用意して気軽にアイキャッチ画像を作れるようにしなければいけません。

そこで、Figmaにあったアイキャッチ素材をSVGで書き出してテンプレートも何種類か作ってCanvaに移行してみました。

f:id:excite_ny:20220120170308p:plain

f:id:excite_ny:20220120170412p:plain
説明文

f:id:excite_ny:20220120170530p:plain
テンプレート

Canvaにはテンプレート機能があるので、テンプレート用のリンクを使えばわざわざデータをコピー→自分のワークスペースにペーストしなくてもテンプレートを使うことができます。

f:id:excite_ny:20220120171253p:plain

終わりに

エンジニア・ビジネスの方達にCanvaを通じて手軽にクリエイティブを作れる手助けになれたら嬉しいです。

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があれば連絡いただければと思います🙇‍♀️

それではまた!

www.wantedly.com

GoのcobraでCLIを作った

f:id:excite-kazuki:20220114192237p:plain

はじめに

エキサイト株式会社 バックエンドエンジニアの山縣です。

既存サービスのリビルドを進めていく上で、Redisのキャッシュ操作を行うためのツールが欲しいという要望がでました。 そこで、Goのcobraを使ってキャッシュ操作を行うCLIを作成しました。 本記事ではcobraの使い方と実際に作成したCLIのサンプルについて紹介します。

環境

  • Go: 1.17
  • go-redis: 8.11.4
  • cobra: 1.3.0

cobraコマンドの導入については、下記リンクを参考にしてください。 github.com

サブコマンドの作成

$ cobra init で初期設定を行い、$ cobra add [command] で任意のコマンドを追加することができます。

# 初期設定
$ cobra init
Your Cobra application is ready at
/Users/example/workspace/tutorial/go-cobra

# サブコマンドを追加
$ cobra add get
get created at /Users/example/workspace/tutorial/go-cobra

これにより、下記のようなディレクトリ構成でいくつかファイルが生成されました。

$ tree
.
├── LICENSE
├── cmd
│   ├── get.go
│   └── root.go
├── go.mod
├── go.sum
└── main.go

1 directory, 6 files

データ取得

package cmd

import (
    "context"
    "fmt"
    "log"

    "github.com/go-redis/redis/v8"
    "github.com/spf13/cobra"
)

var getCmd = &cobra.Command{
    Use:   "get",
    Short: "get the value of a key",
    Long:  "get the value of a key",
    Run: func(cmd *cobra.Command, args []string) {
        key, err := cmd.Flags().GetString("key")
        if err != nil {
            log.Fatal(err)
        }

        client := redis.NewClient(&redis.Options{
            Addr: "localhost:6379",
        })

        value, err := client.Get(context.Background(), key).Result()
        if err != nil {
            log.Fatal(err)
        }

        fmt.Print(value)
    },
}

func init() {
    rootCmd.AddCommand(getCmd)
    getCmd.Flags().StringP("key", "k", "", "cache key")
}

データ登録

package cmd

import (
    "context"
    "log"
    "time"

    "github.com/go-redis/redis/v8"
    "github.com/spf13/cobra"
)

var setCmd = &cobra.Command{
    Use:   "set",
    Short: "set the string value of a key",
    Long:  "set the string value of a key",
    Run: func(cmd *cobra.Command, args []string) {
        key, err := cmd.Flags().GetString("key")
        if err != nil {
            log.Fatal(err)
        }

        value, err := cmd.Flags().GetString("value")
        if err != nil {
            log.Fatal(err)
        }

        client := redis.NewClient(&redis.Options{
            Addr: "localhost:6379",
        })

        err = client.Set(context.Background(), key, value, time.Minute).Err()
        if err != nil {
            log.Fatal(err)
        }
    },
}

func init() {
    rootCmd.AddCommand(setCmd)
    setCmd.Flags().StringP("key", "k", "", "cache key")
    setCmd.Flags().StringP("value", "v", "", "cache value")
}

実行

バイナリファイルとして利用するため、まずはビルドした後に実行します。

# ビルド
$ go build

# データを登録
$ ./cache set --key 'item' --value 'test-value'

# データを取得
$ ./cache get --key 'item'
// test-value

作成したCLIを使用して、データの登録および取得ができることを確認できました 🎉

ロスコンパイル

開発環境はMacですがLinux環境で動作することを想定しています。 Goではクロスコンパイルができるため、Linuxt環境で動作するようにコンパイルしています。

$ GOOS=linux GOARCH=amd64 go build

おわりに

cobraの使い方と実際に作成したCLIのサンプルについて紹介しました。 実際に業務で使用するためには、ホストやポートの指定、タイムアウト、取得できなかったときの処理など考慮する必要があります。 cobraを使うことで、かんたんにコマンドを作成することができるため、今後も使っていきたいです!

MySQLの自動Index選択にどこまで任せるべきか

こんにちは。 エキサイト株式会社の三浦です。

MySQLでは、SQL実行時に適切なIndexを自動的に選んでくれる機能(オプティマイザ)があります。 ただこれは、万能というわけではありません。

今回は、手動でIndexを選んだ方が早い例を挙げ、そのことを確認していきます。

なお今回は、 MySQL5.7 環境にて確認を行っています。

MySQLオプティマイザ

MySQLでは、こちらから何も指定しなくても、実行するSQLをもとに検索対象のテーブルに存在するIndexを自動的に選択してくれます。 これは、オプティマイザという機能によって実現されています。

実行するSQL

EXPLAIN
SELECT
    *
FROM
    test
WHERE
    test.code = "test_code";

Explain結果

id select_type table partitions type possible_keys
1 SIMPLE test const test_index
key key_len ref rows filtered Extra
test_index 302 const 1 100.00

上記の例の場合は、 SQLtest.code カラムを定数で検索するものであり、かつ test.code カラムにIndexが存在するため、自動的に test.code カラムのIndexを選択してくれています。

このように簡単なSQLであれば適切にIndexを選択してくれますし、ある程度複雑でもオプティマイザに任せておけば問題ない場面は多いです。

ただし、常にオプティマイザに任せていればいいかというとそうではありません。

オプティマイザでは不適切なパターン例

例えば、「指定期間内に公開されている、特定の提供元・カテゴリの記事を取得するSQL」を考えてみます。

実行するSQL

EXPLAIN
SELECT
    article.*

FROM article

# packageという単位で提供元・カテゴリを組み合わせている
INNER JOIN master_package_list
    ON article.source = master_package_list.source
    AND article.category = master_package_list.category

WHERE
    # 指定期間内で有効な記事を取得する
    article.deleted_at IS NULL
    AND article.status = 1
    AND article.publish_start_date BETWEEN (NOW() - INTERVAL 3 MONTH) AND NOW()
    AND article.publish_end_date >= NOW()

    AND master_package_list.package = "sample_package"

# 最後に記事公開日順に並び替える
ORDER BY article.publish_start_date DESC

LIMIT 10;

オプティマイザに任せると以下のようなExplain結果になります。

Explain結果

(わかりやすさのため、 possible_keysref は一部省略しています)

id select_type table partitions type possible_keys
1 SIMPLE master_package_list ref package_index,source_category_index
1 SIMPLE article ref sample_index,publish_start_date_index
key key_len ref rows filtered Extra
package_index 62 const 40 100.00 Using temporary; Using filesort
sample_index 206 master_package_list.source,master_package_list.category,const,const 885 9.23 Using index condition; Using where

rowsfiltered を見る限り、そこまで大きな問題があるようには見えません。

ただしこのSQLでは ORDER BY article.publish_start_date DESC の並び替えを行っており、このカラムが今回のIndexには適切に含まれていないために、 Using temporary; Using filesort (一時テーブルに保存して並び替え)が動いてしまっています。

では今度は、手動でIndexを指定してみましょう。 なお変更部分は、 USE INDEX 部分のみです。

実行するSQL

EXPLAIN
SELECT
    article.*

FROM article
    # article.publish_start_date だけのIndexを指定
    USE INDEX (publish_start_date_index)

INNER JOIN master_package_list
    ON article.source = master_package_list.source
    AND article.category = master_package_list.category

WHERE
    article.deleted_at IS NULL
    AND article.status = 1
    AND article.publish_start_date BETWEEN (NOW() - INTERVAL 3 MONTH) AND NOW()
    AND article.publish_end_date >= NOW()

    AND master_package_list.package = "sample_package"

ORDER BY article.publish_start_date DESC

LIMIT 10;

Explainは以下のようになります。

Explain結果

(わかりやすさのため、 possible_keysref は一部省略しています)

id select_type table partitions type possible_keys
1 SIMPLE article range publish_start_date_index
1 SIMPLE master_package_list ref package_index,source_category_index
key key_len ref rows filtered Extra
publish_start_date_index 5 99670 0.33 Using index condition; Using where
source_category_index 199 article.source,article.category 1 3.10 Using where

rowsfiltered だけを見ると、先程のExplain結果に比べて明らかに悪化しているように見えます。 改善点で言えば、 Using temporary; Using filesort がなくなったくらいでしょうか。

上記の2つを見比べると、一見前者のIndexを選ぶのは妥当だと思うかもしれません。

ですが実際は、 SQLの実行速度は圧倒的に後者が早い という結果になるのです。

これは、今回のテーブルの内容であれば「最後の並び替えのコスト」が「最初・途中の絞り込みのコスト」に比べて大きいために、絞り込みを効率化するよりも最初からソートされているIndexを使った方が実行速度としては早かった、ということが原因として考えられます。

もちろんこれは逆に言えば、 article テーブルに保存されているレコード数や master_package_list のデータ構造が異なっていれば、同じSQLであってもオプティマイザの選定の方がSQLの速度が早い場合もありえるということになります。

ただ、この結果から、

  • オプティマイザはある程度合理的にIndexを選択するが、それが必ずしもSQL実行速度を最速化する結果につながるとは限らない

ということがわかります。

最後に

オプティマイザは、MySQLにおいてとても有用な機能です。 ですが、SQL文やそこで使用するテーブルのデータ構造、レコード数等によっては、手動でIndexを指定したほうが実行速度が早くなる場合も存在します。

SQLを実行してみて速度が遅いと感じた場合は、「手動でIndexを選択することも手の一つである」として認識しておくと良いのではないでしょうか。

Spring Bootで、DBのPrimary/ReplicationインスタンスにSQLを振り分ける2つの方法

こんにちは。 エキサイト株式会社の三浦です。

DBでは、可用性の担保のために PrimaryReplicationインスタンスをそれぞれ用意することが多々あります。 こうしたDBをSpring Bootで扱う際、適切にSQL実行先を振り分けないとせっかく分けた意味がなくなってしまいます。

今回は、適切に Primary / Replication インスタンスSQLを振り分ける方法を2通り紹介します。

DBの可用性と Primary / Replication インスタンス

DBは、多くのアプリケーションにとって、アプリケーション上で扱うデータが入っている重要なサービスであり、DBに障害が起きてしまった場合サービス自体に障害が起きてしまうケースがほとんどです。 そのため、DBの可用性を高めることは、DBを扱うアプリケーションにとってはかなり重要性の高い要件と言えます。

DBの可用性を高める方法の1つに、 Primary / Replication インスタンスを別々に用意する、というものがあります。

Primary インスタンスではDBへのデータの書き込み、及び読み込みの両方が可能であり、常にオリジナルのデータが入っています。 データの整合性を保つため、基本的に Primary インスタンスは1台のみで、かつ書き込みはこのインスタンスのみにしか行なえません。

Replication インスタンスではデータの読み込みのみが可能であり、 Primary インスタンスのデータを常時同期するようになっています。 Replication インスタンスではあくまで複製されたデータのみを扱い、かつ読み込みのみを行うため、 Primary インスタンスと異なり複数台作ることが可能です。

多くのアプリケーションでは書き込みに対して読み込みの量のほうが圧倒的に多いため、上記の構成にした上で、基本的に Primary インスタンスでは書き込みのみ、 Replication インスタンスでは読み込みのみを行うようにし、アクセス量に応じて Replication インスタンスの数を増やすことで負荷を分散して、可用性を担保します。

Spring Bootで上記のような構成のDBを扱う際も、もちろん Primary インスタンスでは書き込みのみ、 Replication インスタンスでは読み込みのみを行うようにする必要があります。

Spring Bootで Primary / Replication インスタンスを持つDBを扱う方法

Spring BootでこういったDBを扱う際は、 JDBC の機能を使用する方法と、 DataSource クラスをカスタマイズする方法の2通りが主に存在します。

JDBCの機能を使用する方法

おそらく最も簡単な方法が JDBC の機能を使用する方法です。

JDBCには replication の機能があり、以下の2工程でPrimary / Replication に適切に振り分けてくれるようになります。

1. @Transactional を使用する

Primary インスタンスにアクセスしてほしいメソッドには @Transactional() アノテーションを、 Replication インスタンスにアクセスしてほしいメソッドには @Transactional(readOnly = true) アノテーションを付与する

2. アクセス先DBのURLを以下のように指定する

jdbc:mysql:replication://primary_db:3306,replication_db:3306/sample_schema

これだけで、適切にSQLを割り振ってくれます。 これには以下のメリット・デメリットが存在します。

メリット

  • 非常に簡単に指定できる

デメリット

  • JDBCのreplication機能が使えない場合は使用できない
  • コネクションプールで保持されるコネクション数が PrimaryReplication で同数になってしまう
    • Primary インスタンスは1台しか作れないため、 Primary の最大同時接続数にコネクション数を合わせると Replication 用のコネクションが不足したり、 Replication で必要なコネクション数に合わせると Primary の最大同時接続数が足りなくなってしまう恐れがある

DataSource クラスをカスタマイズする方法

DataSouce を使う方法は少し複雑ですが、JDBCに比べて柔軟な対応が可能になります。

DataSource を使う場合も、JDBCと同じく @Transactional で振り分け先を指定します。 変わるのはその後です。

1. @Transactional を使用する

Primary インスタンスにアクセスしてほしいメソッドには @Transactional() アノテーションを、 Replication インスタンスにアクセスしてほしいメソッドには @Transactional(readOnly = true) アノテーションを付与する

2. DataSource 周りをカスタマイズする

DataSource 周りをカスタマイズします。 ここでは HikariDataSource を使用していますが、おそらく他の DataSource を使っても大丈夫だと思います。 また、MaximumPoolSize 等の細かい設定も、必要に応じて変更してください。

今回はコード内に直接各種プロパティを書いていますが、実際に使用する際はそういったプロパティは application.properties などに書いておき、このコード内で呼び出すようにするほうが管理しやすいでしょう。

import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {
    private final String primaryDataSourcePropertiesName = "primaryDataSourceProperties";
    private final String replicationDataSourcePropertiesName = "replicationDataSourceProperties";
    private final String primaryDataSourceName = "primaryDataSource";
    private final String replicationDataSourceName = "replicationDataSource";
    private final String routerDataSourceName = "routingDataSource";
    private final String mainDataSourceName = "dataSource";

    public enum DataSourceType {
        PRIMARY, REPLICATION
    }

    /**
     * TransactionのreadOnlyをもとにDataSourceをルーティングするためのカスタムルーティングクラス
     */
    public static class CustomRoutingDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
                    ? DataSourceType.REPLICATION
                    : DataSourceType.PRIMARY;
        }
    }

    /**
     * Primary用DataSourceのプロパティを作成
     * @return Primary用DataSourceのプロパティ
     */
    @Bean(name = primaryDataSourcePropertiesName)
    @Primary
    public DataSourceProperties primaryDataSourceProperties() {
            DataSourceProperties dataSourceProperties = new DataSourceProperties();
            dataSourceProperties.setDriverClassName("com.mysql.cj.jdbc.Driver");
            dataSourceProperties.setUsername("user");
            dataSourceProperties.setPassword("password");
            dataSourceProperties.setUrl("jdbc:mysql://primary_db:3306/sample_schema");

            return dataSourceProperties;
    }

    /**
     * Replication用DataSourceのプロパティを作成
     * @return Replication用DataSourceのプロパティ
     */
    @Bean(name = replicationDataSourcePropertiesName)
    public DataSourceProperties replicationDataSourceProperties() {
            DataSourceProperties dataSourceProperties = new DataSourceProperties();
            dataSourceProperties.setDriverClassName("com.mysql.cj.jdbc.Driver");
            dataSourceProperties.setUsername("user");
            dataSourceProperties.setPassword("password");
            dataSourceProperties.setUrl("jdbc:mysql://replication_db:3306/sample_schema");

            return dataSourceProperties;
    }

    /**
     * Primary用DBエンドポイントのDataSourceを作成
     * @return Primary用DBエンドポイントのDataSource
     */
    @Bean(name = primaryDataSourceName)
    @Primary
    public DataSource primaryDataSource(@Qualifier(primaryDataSourcePropertiesName) DataSourceProperties dataSourceProperties) {
        HikariDataSource hikariDataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        hikariDataSource.setMaxLifetime(600000);
        hikariDataSource.setMaximumPoolSize(10);
        hikariDataSource.setReadOnly(false);

        return hikariDataSource;
    }

    /**
     * Replication用DBエンドポイントのDataSourceを作成
     * @return Replication用DBエンドポイントのDataSource
     */
    @Bean(name = replicationDataSourceName)
    public DataSource replicationDataSource(@Qualifier(replicationDataSourcePropertiesName) DataSourceProperties dataSourceProperties) {
        HikariDataSource hikariDataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        hikariDataSource.setMaxLifetime(600000);
        hikariDataSource.setMaximumPoolSize(20);
        hikariDataSource.setReadOnly(true);

        return hikariDataSource;
    }

    /**
     * SQLを実行するDataSourceを、Transactionによってルーティングするための設定を作成
     * @param primaryDataSource Primary用DBエンドポイントのDataSource
     * @param replicationDataSource Replication用DBエンドポイントのDataSource
     * @return SQLを実行するDataSourceを、Transactionによってルーティングするための設定
     */
    @Bean(name = routerDataSourceName)
    public DataSource routingDataSource(
            @Qualifier(primaryDataSourceName) final DataSource primaryDataSource,
            @Qualifier(replicationDataSourceName) final DataSource replicationDataSource
    ) {
        CustomRoutingDataSource routingDataSource = new CustomRoutingDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(DataSourceType.PRIMARY, primaryDataSource);
        dataSourceMap.put(DataSourceType.REPLICATION, replicationDataSource);

        routingDataSource.setTargetDataSources(dataSourceMap);
        routingDataSource.setDefaultTargetDataSource(primaryDataSource);

        return routingDataSource;
    }

    /**
     * TransactionによってDataSourceをルーティングするDataSourceを作成
     * @param routingDataSource SQLを実行するDataSourceを、Transactionによってルーティングするための設定
     * @return TransactionによってDataSourceをルーティングするDataSource
     */
    @Bean(name = mainDataSourceName)
    public DataSource dataSource(@Qualifier(routerDataSourceName) DataSource routingDataSource) {
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }

    /**
     * トランザクションマネージャを作成
     * @param dataSource TransactionによってDataSourceをルーティングするDataSource
     * @return トランザクションマネージャ
     */
    @Bean
    @Primary
    public PlatformTransactionManager transactionManager(@Qualifier(mainDataSourceName) DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    /**
     * SQLセッションファクトリを作成
     * @param dataSource TransactionによってDataSourceをルーティングするDataSource
     * @return SQLセッションファクトリ
     * @throws Exception 例外
     */
    @Bean
    @Primary
    public SqlSessionFactory sqlSessionFactory(@Qualifier(mainDataSourceName) DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);

        return sqlSessionFactoryBean.getObject();
    }
}

少し長くなりましたが、この設定で適切にSQLを割り振ってくれます。 これには以下のメリット・デメリットが存在します。

メリット

  • JDBCのreplicationを使えなくても使用することが出来る
  • Primary / Replication でコネクションプールのコネクション数などを完全に別々に設定することが出来る

デメリット

  • 設定が少し複雑

最後に

今回は、 Primary / Replication 構成のDBにSpring Bootからアクセスする方法を2つ紹介しました。 個人的には、

  • アクセスがかなり少なく、JDBCのreplicationが使えるアプリケーション:JDBCを使用する方法
  • それ以外:DataSource をカスタマイズする方法

が適切だと思っており、そのため、多くの場合 DataSource をカスタマイズする方法が適切なのではないかと考えています。

もちろん上記で挙げた以外の思わぬ副作用がある可能性もありますし、アプリケーションごとに様々な要件もあると思いますので、一概に「どちらがいい」と言うのは難しいところです。 このあたりは、各アプリケーションごとに判断していただけると良いかと思います。

この記事が何かしら参考になりましたら幸いです。