1 of 133

Active Recordの

複数DB対応を活用して、Railsアプリケーションを

統廃合する

2024/10/17 フサギコ(髙﨑 尚人)

2 of 133

自己紹介

  • 名前:フサギコ
    • 本名 :髙﨑 尚人(TAKASAKI Naoto)
    • Twitter :fusagiko
    • GitHub :takayamaki
    • Bluesky :fusagiko.jp
  • 得意:Ruby, Rails, AWS, Terraform, CI/CD
    • 一応できる:TypeScript, React, Next.js
  • OSS:マストドンにcontributeしてました(近年できていない)
  • 仕事:株式会社ブックウォーカー メディアサービス開発部
    • バックエンド方面のリードエンジニア(主にニコニコ漫画)
  • 趣味:アイドルマスターシリーズ
    • 初代から学マス、ヴイアライヴまで一通り
    • 他にはアニソンDJ(ほぼアイマス)とかサバゲーとか野球観戦とか…

2

3 of 133

Active Recordの

複数DB対応を活用して、Railsアプリケーションを

統廃合する

2024/10/17 フサギコ(髙﨑 尚人)

4 of 133

おしながき

  1. 自己紹介
  2. 株式会社ブックウォーカーの概要
  3. ニコニコ漫画の概要
  4. Railsの複数データベース対応
  5. ニコニコ漫画に2つのRailsアプリケーションが爆誕した経緯
  6. Railsアプリケーションを統廃合する手順
  7. まとめ

4

5 of 133

株式会社ブックウォーカー

  • KADOKAWAグループ傘下
  • 電子書籍関連の事業子会社
    • BOOK☆WALKER(電子書籍ストア)
      • 日本、GLOBAL(英語)、台湾、タイ
    • ニコニコ漫画(漫画連載サービス)
    • 読書メーター(書籍レビュー投稿コミュニティ)
    • カドコミ(漫画連載サービス) スマホアプリ版
    • 電子書籍取次事業
    • その他
      • 電子書籍関連サービスの開発受託など

5

6 of 133

株式会社ブックウォーカー

  • KADOKAWAグループ傘下
  • 電子書籍関連の事業子会社
    • BOOK☆WALKER(電子書籍ストア)
      • 日本、GLOBAL(英語)、台湾、タイ
    • ニコニコ漫画(漫画連載サービス)
    • 読書メーター(書籍レビュー投稿コミュニティ)
    • カドコミ(漫画連載サービス) スマホアプリ版
    • 電子書籍取次事業
    • その他
      • 電子書籍関連サービスの開発受託など

6

7 of 133

ニコニコ漫画

  • 株式会社ドワンゴのニコニコファミリーサービスに連なる漫画連載サービス
    • ニコニコ漫画だけ運営開発がブックウォーカー(電子書籍扱い)
  • ニコニコユーザーが投稿するユーザー漫画と商業作品の編集部が投稿する公式漫画が混在
    • 公式漫画はKADOKAWA系に限らず、様々な出版社から連載が集まる
  • サービス開始から12年半
    • ↑は漫画投稿機能が一般公開された日(2012年3月)起算
    • 更なる起源はニコニコ静画の特設企画(2010年9月ごろ)
  • 2019年2月、公式漫画において無料公開が終了したエピソードを�コインを消費して読める機能(単話課金)をスマホアプリにおいて提供開始

7

8 of 133

おしながき

  • 自己紹介
  • 株式会社ブックウォーカーの概要
  • ニコニコ漫画の概要
  • Railsの複数データベース対応
  • ニコニコ漫画に2つのRailsアプリケーションが爆誕した経緯
  • Railsアプリケーションを統廃合する手順
  • まとめ

8

9 of 133

Railsの複数データベース対応

  • GitHub社の貢献によってRails 6.0から対応され始めた
  • プライマリ / リードレプリカ(Rails 6.0~)
    • 読み込みをリードレプリカから行うことで負荷を分散
    • リクエストが書き込みを伴うかどうかで自動振り分け
  • 垂直分割(Rails 6.0~)
    • 読み書きするデータベースをモデルによって変えることで負荷を分散
  • 水平分割(Rails 6.1~)
    • 同じモデルを、なんらかの基準によって異なるDBに振り分けることで負荷を分散
      • 基準としてよくあるのはユーザIDの範囲など

9

10 of 133

Railsの複数データベース対応

  • GitHub社の貢献によってRails 6.0から対応され始めた
  • プライマリ / リードレプリカ(Rails 6.0~)
    • 読み込みをリードレプリカから行うことで負荷を分散
    • リクエストが書き込みを伴うかどうかで自動振り分け
  • 垂直分割(Rails 6.0~)
    • 読み書きするデータベースをモデルによって変えることで負荷を分散
  • 水平分割(Rails 6.1~)
    • 同じモデルを、なんらかの基準によって異なるDBに振り分けることで負荷を分散
      • 基準としてよくあるのはユーザIDの範囲など

10

11 of 133

Railsの複数データベース対応

  • GitHub社の貢献によってRails 6.0から対応され始めた
  • プライマリ / リードレプリカ(Rails 6.0~)
    • 読み込みをリードレプリカから行うことで負荷を分散
    • リクエストが書き込みを伴うかどうかで自動振り分け
  • 垂直分割(Rails 6.0~)
    • 読み書きするデータベースをモデルによって変えることで負荷を分散
  • 水平分割(Rails 6.1~)
    • 同じモデルを、なんらかの基準によって異なるDBに振り分けることで負荷を分散
      • 基準としてよくあるのはユーザIDの範囲など

11

複数データベース対応と聞いて

ほとんどの人が

思い浮かべるのは負荷分散

12 of 133

Railsの複数データベース対応

  • GitHub社の貢献によってRails 6.0から対応され始めた
  • プライマリ / リードレプリカ(Rails 6.0~)
    • 読み込みをリードレプリカから行うことで負荷を分散
    • リクエストが書き込みを伴うかどうかで自動振り分け
  • 垂直分割(Rails 6.0~)
    • 読み書きするデータベースをモデルによって変えることで負荷を分散
  • 水平分割(Rails 6.1~)
    • 同じモデルを、なんらかの基準によって異なるDBに振り分けることで負荷を分散
      • 基準としてよくあるのはユーザIDの範囲など

12

なんですが

13 of 133

Railsの複数データベース対応

  • GitHub社の貢献によってRails 6.0から対応され始めた
  • プライマリ / リードレプリカ(Rails 6.0~)
    • 読み込みをリードレプリカから行うことで負荷を分散
    • リクエストが書き込みを伴うかどうかで自動振り分け
  • 垂直分割(Rails 6.0~)
    • 読み書きするデータベースをモデルによって変えることで負荷を分散
  • 水平分割(Rails 6.1~)
    • 同じモデルを、なんらかの基準によって異なるDBに振り分けることで負荷を分散
      • 基準としてよくあるのはユーザIDの範囲など

13

今回お話するのは

複数データベース対応を活用した

データベース間データ移行�およびアプリケーション統廃合

14 of 133

おしながき

  • 自己紹介
  • 株式会社ブックウォーカーの概要
  • ニコニコ漫画の概要
  • Railsの複数データベース対応
  • ニコニコ漫画に2つのRailsアプリケーションが爆誕した経緯
  • Railsアプリケーションを統廃合する手順
  • まとめ

14

15 of 133

初期(2012年3月~)

サービス開始時はPHP製独自フレームワークのモノリス

15

internet

DB

ニコニコ漫画

(PHP独自FW)

16 of 133

単話課金開始(2019年2月~)

単話課金の実装にあたって、ドワンゴの基盤サービスを利用する課金サブシステムが誕生

16

internet

本体DB

課金サブシステム(Rails)

ドワンゴ

基盤サービス

ニコニコ漫画

(PHP独自FW)

課金DB

17 of 133

新バックエンド誕生(2020年4月~)

PHP製ニコニコ静画本体の改修容易性の低下が限界に達していたため、

Well-testedなバックエンド実現のためにRails製新バックエンドが誕生

17

internet

本体DB

課金サブシステム(Rails)

ドワンゴ

基盤サービス

ニコニコ漫画

(PHP独自FW)

課金DB

新バックエンド

(Rails)

18 of 133

新バックエンド誕生(2020年4月~)

PHP製ニコニコ静画本体の改修容易性の低下が限界に達していたため、

Well-testedなバックエンド実現のためにRails製新バックエンドが誕生

18

internet

本体DB

課金サブシステム(Rails)

ドワンゴ

基盤サービス

ニコニコ漫画

(PHP独自FW)

課金DB

新バックエンド

(Rails)

課金系なので

あまり触りたくない

という恐怖心

本体とDBを共有する特殊構成 &

小さく高速に実装サイクルを

回していきたい

19 of 133

課金サブシステムがドワンゴ基盤サービスから脱依存(2023年9月~)

諸事情により課金サブシステムがドワンゴ基盤サービスから脱依存

新バックエンドへの式年遷宮(ビジネスロジックの仕様明確化、移植)は順調

19

internet

本体DB

課金サブシステム(Rails)

ニコニコ漫画

(PHP独自FW)

課金DB

新バックエンド

(Rails)

20 of 133

課金サブシステムがドワンゴ基盤サービスから脱依存(2023年9月~)

諸事情により課金サブシステムがドワンゴ基盤サービスから脱依存

新バックエンドへの式年遷宮(ビジネスロジックの仕様明確化、移植)は順調

20

internet

本体DB

課金サブシステム(Rails)

ニコニコ漫画

(PHP独自FW)

課金DB

新バックエンド

(Rails)

その後

しばらくして気付いた

21 of 133

課金サブシステムがドワンゴ基盤サービスから脱依存(2023年9月~)

諸事情により課金サブシステムがドワンゴ基盤サービスから脱依存

新バックエンドへの式年遷宮(ビジネスロジックの仕様明確化、移植)は順調

21

internet

本体DB

課金サブシステム(Rails)

ニコニコ漫画

(PHP独自FW)

課金DB

新バックエンド

(Rails)

小さく高速に実装サイクルを

回せており成功体験

22 of 133

課金サブシステムがドワンゴ基盤サービスから脱依存(2023年9月~)

諸事情により課金サブシステムがドワンゴ基盤サービスから脱依存

新バックエンドへの式年遷宮(ビジネスロジックの仕様明確化、移植)は順調

22

internet

本体DB

課金サブシステム(Rails)

ニコニコ漫画

(PHP独自FW)

課金DB

新バックエンド

(Rails)

小さく高速に実装サイクルを

回せており成功体験

機能が安定しきっていて

デプロイされない

依存gem更新も滞りがち

23 of 133

課金サブシステムがドワンゴ基盤サービスから脱依存(2023年9月~)

諸事情により課金サブシステムがドワンゴ基盤サービスから脱依存

新バックエンドへの式年遷宮(ビジネスロジックの仕様明確化、移植)は順調

23

internet

本体DB

課金サブシステム(Rails)

ニコニコ漫画

(PHP独自FW)

課金DB

新バックエンド

(Rails)

小さく高速に実装サイクルを

回せており成功体験

機能が安定しきっていて

デプロイされない

依存gem更新も滞りがち

これ、課金サブシステムが

独立してると

逆にリスクが大きいのでは?

保守されないまま動いてしまって忘れ去られる、野生化したシステム

また、インフラ層でも理解容易性を低下させる負債になっていた

24 of 133

課金サブシステムがドワンゴ基盤サービスから脱依存(2023年9月~)

諸事情により課金サブシステムがドワンゴ基盤サービスから脱依存

新バックエンドへの式年遷宮(ビジネスロジックの仕様明確化、移植)は順調

24

internet

本体DB

課金サブシステム(Rails)

ニコニコ漫画

(PHP独自FW)

課金DB

新バックエンド

(Rails)

小さく高速に実装サイクルを

回せており成功体験

機能が安定しきっていて

デプロイされない

依存gem更新も滞りがち

2024年5月

課金サブシステム

吸収合併計画始動

25 of 133

課金サブシステムがドワンゴ基盤サービスから脱依存(2023年9月~)

諸事情により課金サブシステムがドワンゴ基盤サービスから脱依存

新バックエンドへの式年遷宮(ビジネスロジックの仕様明確化、移植)は順調

25

internet

本体DB

課金サブシステム(Rails)

ニコニコ漫画

(PHP独自FW)

課金DB

新バックエンド

(Rails)

小さく高速に実装サイクルを

回せており成功体験

機能が安定しきっていて

デプロイされない

依存gem更新も滞りがち

13エンドポイント

11テーブル

レコード数1億超

26 of 133

おしながき

  • 自己紹介
  • 株式会社ブックウォーカーの概要
  • ニコニコ漫画の概要
  • Railsの複数データベース対応
  • ニコニコ漫画に2つのRailsアプリケーションが爆誕した経緯
  • Railsアプリケーションを統廃合する手順
  • まとめ

26

27 of 133

Railsアプリケーションを統廃合する

まず方針

27

28 of 133

Railsアプリケーションを統廃合するための方針

  • 統合が先、改修は後
  • まずは課金サブシステムのアプリケーションサーバを止めたい
    • 新バックエンドに課金サブシステムの完全互換APIを実装
    • 本体からのリクエストを課金サブシステムから新バックエンドに呼び変え
    • 課金サブシステムのアプリケーションサーバを停止
    • 課金サブシステムのELBを除却
  • 課金DBから本体DBにデータを無停止で移行したい
    • 完全互換APIにおいて、課金DBと本体DBの両方に書き込みを複製する
    • 全レコードを順次upsertで課金DBから本体DBへ複製していく
    • 読み込みを本体DBからへ切り替え
    • 複製書き込みを解除
    • 課金DBを除却

28

29 of 133

Railsアプリケーションを統廃合する手順

  • 新バックエンドで課金DBを読み書きできるようにする
  • 新バックエンドに課金サブシステムのモデルをprefix付きで移植する
  • 新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
  • バッチを移植する
  • 本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
  • 課金サブシステムのアプリケーションサーバを除却する
  • 本体DBに課金DBのスキーマを丸ごとコピーする
  • 課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る
  • 新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む
  • 課金DBから本体DBへ各種レコードをbulk upsertで移行する
  • 課金DBが主、本体DBが従になっているところを逆転させる
  • 分析系ダッシュボードのデータソースを切り替える
  • 両DBへの同時書き込みを切り、課金DBを除却する

29

30 of 133

Railsアプリケーションを統廃合する手順

  • 新バックエンドで課金DBを読み書きできるようにする
  • 新バックエンドに課金サブシステムのモデルをprefix付きで移植する
  • 新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
  • バッチを移植する
  • 本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
  • 課金サブシステムのアプリケーションサーバを除却する
  • 本体DBに課金DBのスキーマを丸ごとコピーする
  • 課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る
  • 新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む
  • 課金DBから本体DBへ各種レコードをbulk upsertで移行する
  • 課金DBが主、本体DBが従になっているところを逆転させる
  • 分析系ダッシュボードのデータソースを切り替える
  • 両DBへの同時書き込みを切り、課金DBを除却する

30

31 of 133

Railsアプリケーションを統廃合する手順

その前に前提知識

31

32 of 133

前提知識

Railsが複数DBを

扱うときのconfigの書き方

32

33 of 133

Railsが複数DBを扱うときの基本的書き方

見慣れた/config/database.ymlとActiveRecord::Baseを継承するクラス

33

production:

<<: *nicomanga_default

host: <%= ENV['DB_HOST'] %>

class ApplicationRecord < ActiveRecord::Base

self.abstract_class = true

end

34 of 133

Railsが複数DBを扱うときの基本的書き方

/config/database.ymlにおいて、

環境名の下にもう一段オブジェクトを作ると、そのキーが接続情報名になる

34

production:

nicomanga_writer:

<<: *nicomanga_default

host: <%= ENV['DB_HOST'] %>

nicomanga_reader:

<<: *nicomanga_default

host: <%= ENV['DB_RO_HOST'] %>

replica: true

class ApplicationRecord < ActiveRecord::Base

self.abstract_class = true

end

35 of 133

Railsが複数DBを扱うときの基本的書き方

ActiveRecord::Baseを継承するクラスの定義の中でconnects_toメソッドを用いて

どのロールがどの接続情報を使うかを記述する

35

class ApplicationRecord < ActiveRecord::Base

self.abstract_class = true

connects_to database: {

writing: :nicomanga_writer,

reading: :nicomanga_reader,

}

end

production:

nicomanga_writer:

<<: *nicomanga_default

host: <%= ENV['DB_HOST'] %>

nicomanga_reader:

<<: *nicomanga_default

host: <%= ENV['DB_RO_HOST'] %>

replica: true

36 of 133

Railsアプリケーションを統廃合する手順

  • 新バックエンドで課金DBを読み書きできるようにする
  • 新バックエンドに課金サブシステムのモデルをprefix付きで移植する
  • 新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
  • バッチを移植する
  • 本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
  • 課金サブシステムのアプリケーションサーバを除却する
  • 本体DBに課金DBのスキーマを丸ごとコピーする
  • 課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る
  • 新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む
  • 課金DBから本体DBへ各種レコードをbulk upsertで移行する
  • 課金DBが主、本体DBが従になっているところを逆転させる
  • 分析系ダッシュボードのデータソースを切り替える
  • 両DBへの同時書き込みを切り、課金DBを除却する

36

37 of 133

新バックエンドで課金DBを読み書きできるようにする

新バックエンドから

課金DBへの接続を許可する

実はVPCすら別(VPCピアリングしている)だったので

セキュリティグループ指定で許可できなかった

仕方なくサブネットのCIDRで許可した

37

38 of 133

新バックエンドで課金DBを読み書きできるようにする

本体DBのモデルと別に、課金DBのモデルの基底クラスを

名前空間のprefixを付けて定義する

38

class ApplicationRecord < ActiveRecord::Base

self.abstract_class = true

connects_to database: {

writing: :nicomanga_writer,

reading: :nicomanga_reader,

}

end

production:

nicomanga_writer:

<<: *nicomanga_default

host: <%= ENV['DB_HOST'] %>

nicomanga_reader:

<<: *nicomanga_default

host: <%= ENV['DB_RO_HOST'] %>

replica: true

39 of 133

新バックエンドで課金DBを読み書きできるようにする

本体DBのモデルと別に、課金DBのモデルの基底クラスを

名前空間のprefixを付けて定義する

39

class ApplicationRecord < ActiveRecord::Base

self.abstract_class = true

connects_to database: {

writing: :nicomanga_writer,

reading: :nicomanga_reader,

}

end

production:

nicomanga_writer:

<<: *nicomanga_default

host: <%= ENV['DB_HOST'] %>

nicomanga_reader:

<<: *nicomanga_default

host: <%= ENV['DB_RO_HOST'] %>

replica: true

kakin_writer:

<<: *kakin_default

host: <%= ENV[KAKIN_DB_HOST'] %>

kakin_reader:

<<: *kakin_default

host: <%= ENV[KAKIN_DB_RO_HOST'] %>

replica: true

40 of 133

新バックエンドで課金DBを読み書きできるようにする

本体DBのモデルと別に、課金DBのモデルの基底クラスを

名前空間のprefixを付けて定義する

40

class ApplicationRecord < ActiveRecord::Base

self.abstract_class = true

connects_to database: {

writing: :nicomanga_writer,

reading: :nicomanga_reader,

}

end

class Kakin::ApplicationRecord < ActiveRecord::Base

self.abstract_class = true

connects_to database: {

writing: :kakin_writer,

reading: :kakin_reader,

}

end

production:

nicomanga_writer:

<<: *nicomanga_default

host: <%= ENV['DB_HOST'] %>

nicomanga_reader:

<<: *nicomanga_default

host: <%= ENV['DB_RO_HOST'] %>

replica: true

kakin_writer:

<<: *kakin_default

host: <%= ENV[KAKIN_DB_HOST'] %>

kakin_reader:

<<: *kakin_default

host: <%= ENV[KAKIN_DB_RO_HOST'] %>

replica: true

41 of 133

Railsアプリケーションを統廃合する手順

  • 新バックエンドで課金DBを読み書きできるようにする
  • 新バックエンドに課金サブシステムのモデルをprefix付きで移植する
  • 新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
  • バッチを移植する
  • 本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
  • 課金サブシステムのアプリケーションサーバを除却する
  • 本体DBに課金DBのスキーマを丸ごとコピーする
  • 課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る
  • 新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む
  • 課金DBから本体DBへ各種レコードをbulk upsertで移行する
  • 課金DBが主、本体DBが従になっているところを逆転させる
  • 分析系ダッシュボードのデータソースを切り替える
  • 両DBへの同時書き込みを切り、課金DBを除却する

41

42 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

まずModel specとFactoryBotのfactoryをコピーする

42

# 新バックエンド /spec/models/kakin/deal.rb

describe Kakin::Deal do

describe 'user_id' do

# (中略)

end

describe ’coin_balance_changed_events’ do

# (中略)

end

end

# 課金サブシステム /spec/models/deal.rb

describe Deal do

describe 'user_id' do

# (中略)

end

describe ’coin_balance_changed_events’ do

# (中略)

end

end

43 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

まずModel specとFactoryBotのfactoryをコピーする

43

# 新バックエンド /spec/models/kakin/deal.rb

describe Kakin::Deal do

describe 'user_id' do

# (中略)

end

describe ’coin_balance_changed_events’ do

# (中略)

end

end

# 課金サブシステム /spec/models/deal.rb

describe Deal do

describe 'user_id' do

# (中略)

end

describe ’coin_balance_changed_events’ do

# (中略)

end

end

名前空間にprefixを付ける

44 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

まずModel specとFactoryBotのfactoryをコピーする

44

# 新バックエンド /spec/models/kakin/deal.rb

describe Kakin::Deal do

describe 'user_id' do

# (中略)

end

describe ’coin_balance_changed_events’ do

# (中略)

end

end

# 課金サブシステム /spec/models/deal.rb

describe Deal do

describe 'user_id' do

# (中略)

end

describe ’coin_balance_changed_events’ do

# (中略)

end

end

名前空間にprefixを付ける

このとき、コピーしてくるcommitと

prefixを付けるcommitを必ず分ける

prefix付ける差分を混ぜてしまうと

ミスに気付けない、レビューしづらい

45 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

まずModel specとFactoryBotのfactoryをコピーする

45

# 新バックエンド /spec/models/kakin/deal.rb

describe Kakin::Deal do

describe 'user_id' do

# (中略)

end

describe ’coin_balance_changed_events’ do

# (中略)

end

end

# 課金サブシステム /spec/models/deal.rb

describe Deal do

describe 'user_id' do

# (中略)

end

describe ’coin_balance_changed_events’ do

# (中略)

end

end

46 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

まずModel specとFactoryBotのfactoryをコピーする

46

# 新バックエンド /spec/models/kakin/deal.rb

describe Kakin::Deal do

describe 'user_id' do

# (中略)

end

describe ’coin_balance_changed_events’ do

# (中略)

end

end

# 新バックエンド /spec/factories/kakin_deal.rb

FactoryBot.define do

factory :kakin_deal, class: 'Kakin::Deal' do

sequence(:user_id) { |n| "user_id_#{n}" }

end

end

# 課金サブシステム /spec/models/deal.rb

describe Deal do

describe 'user_id' do

# (中略)

end

describe ’coin_balance_changed_events’ do

# (中略)

end

end

# 課金サブシステム /spec/factories/deal.rb

FactoryBot.define do

factory :deal do

sequence(:user_id) { |n| "user_id_#{n}" }

end

end

47 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

まずModel specとFactoryBotのfactoryをコピーする

47

# 新バックエンド /spec/models/kakin/deal.rb

describe Kakin::Deal do

describe 'user_id' do

# (中略)

end

describe ’coin_balance_changed_events’ do

# (中略)

end

end

# 新バックエンド /spec/factories/kakin_deal.rb

FactoryBot.define do

factory :kakin_deal, class: 'Kakin::Deal' do

sequence(:user_id) { |n| "user_id_#{n}" }

end

end

# 課金サブシステム /spec/models/deal.rb

describe Deal do

describe 'user_id' do

# (中略)

end

describe ’coin_balance_changed_events’ do

# (中略)

end

end

# 課金サブシステム /spec/factories/deal.rb

FactoryBot.define do

factory :deal do

sequence(:user_id) { |n| "user_id_#{n}" }

end

end

名前空間が変わるのでclass指定が必要

factory名にもprefixを付ける

48 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

実装を名前空間を変えてコピーしてくる

48

# 新バックエンド /models/kakin/deal.rb

class Kakin::Deal < Kakin::ApplicationRecord

# has_many :coin_balance_changed_events,

# class_name: 'Kakin::CoinBalanceChangedEvent’

validates :user_id, presence: true

end

# 課金サブシステム /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

end

49 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

実装を名前空間を変えてコピーしてくる

49

# 新バックエンド /models/kakin/deal.rb

class Kakin::Deal < Kakin::ApplicationRecord

# has_many :coin_balance_changed_events,

# class_name: 'Kakin::CoinBalanceChangedEvent’

validates :user_id, presence: true

end

# 課金サブシステム /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

end

継承元と名前空間を変える

50 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

実装を名前空間を変えてコピーしてくる

50

# 新バックエンド /models/kakin/deal.rb

class Kakin::Deal < Kakin::ApplicationRecord

# has_many :coin_balance_changed_events,

# class_name: 'Kakin::CoinBalanceChangedEvent’

validates :user_id, presence: true

end

# 課金サブシステム /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

end

子レコードのクラスへの

関連は一旦コメントアウト

51 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

同じ要領で他のモデルも移植する

51

# 新バックエンド /models/kakin/deal.rb

class Kakin::Deal < Kakin::ApplicationRecord

has_many :coin_balance_changed_events,

class_name: 'Kakin::CoinBalanceChangedEvent’

validates :user_id, presence: true

end

# /models/kakin/coin_balance_changed_events.rb

class Kakin::CoinBalanceChangedEvent

< Kakin::ApplicationRecord

belongs_to :deal, class_name: 'Kakin::Deal’

validates :user_id, presence: true

validates :changed_amount,

numericality: { only_integer: true }

end

# 課金サブシステム /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

end

# /models/coin_balance_changed_events.rb

class CoinBalanceChangedEvent < ApplicationRecord

belongs_to :deal

validates :user_id, presence: true

validates :changed_amount,

numericality: { only_integer: true }

end

52 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

同じ要領で他のモデルも移植する

52

# 新バックエンド /models/kakin/deal.rb

class Kakin::Deal < Kakin::ApplicationRecord

has_many :coin_balance_changed_events,

class_name: 'Kakin::CoinBalanceChangedEvent’

validates :user_id, presence: true

end

# /models/kakin/coin_balance_changed_events.rb

class Kakin::CoinBalanceChangedEvent

< Kakin::ApplicationRecord

belongs_to :deal, class_name: 'Kakin::Deal’

validates :user_id, presence: true

validates :changed_amount,

numericality: { only_integer: true }

end

# 課金サブシステム /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

end

# /models/coin_balance_changed_events.rb

class CoinBalanceChangedEvent < ApplicationRecord

belongs_to :deal

validates :user_id, presence: true

validates :changed_amount,

numericality: { only_integer: true }

end

ここで一つ問題を発見

53 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

違い、わかりますか?

53

# 新バックエンド rails console

new_backend(production)> Kakin::Deal.last.created_at

=> Wed, 12 Jun 2024 03:43:41.641262000 JST +09:00

new_backend(production)>

# 課金サブシステム rails console

kakin(production)> Deal.last.created_at

=> Wed, 12 Jun 2024 12:43:41.641262000 JST +09:00

kakin(production)>

54 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

時刻が違う

54

# 新バックエンド rails console

new_backend(production)> Kakin::Deal.last.created_at

=> Wed, 12 Jun 2024 03:43:41.641262000 JST +09:00

new_backend(production)>

# 課金サブシステム rails console

kakin(production)> Deal.last.created_at

=> Wed, 12 Jun 2024 12:43:41.641262000 JST +09:00

kakin(production)>

55 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

application.rbを確認するとこうなっていた

55

# 新バックエンド rails console

new_backend(production)> Kakin::Deal.last.created_at

=> Wed, 12 Jun 2024 03:43:41.641262000 JST +09:00

new_backend(production)>

# 新バックエンド /config/application.rb

module NewBackend

class Application < Rails::Application

# (中略)

config.time_zone = 'Asia/Tokyo'

config.active_record.default_timezone = :local

end

end

# 課金サブシステム rails console

kakin(production)> Deal.last.created_at

=> Wed, 12 Jun 2024 12:43:41.641262000 JST +09:00

kakin(production)>

# 課金サブシステム /config/application.rb

module Kakin

class Application < Rails::Application

# (中略)

config.time_zone = ‘Tokyo’

end

end

56 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

application.rbを確認するとこうなっていた

56

# 新バックエンド rails console

new_backend(production)> Kakin::Deal.last.created_at

=> Wed, 12 Jun 2024 03:43:41.641262000 JST +09:00

new_backend(production)>

# 新バックエンド /config/application.rb

module NewBackend

class Application < Rails::Application

# (中略)

config.time_zone = 'Asia/Tokyo'

config.active_record.default_timezone = :local

end

end

# 課金サブシステム rails console

kakin(production)> Deal.last.created_at

=> Wed, 12 Jun 2024 12:43:41.641262000 JST +09:00

kakin(production)>

# 課金サブシステム /config/application.rb

module Kakin

class Application < Rails::Application

# (中略)

config.time_zone = ‘Tokyo’

end

end

57 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

時刻が違う

57

# 新バックエンド rails console

new_backend(production)> Kakin::Deal.last.created_at

=> Wed, 12 Jun 2024 03:43:41.641262000 JST +09:00

new_backend(production)>

# 新バックエンド /config/application.rb

module NewBackend

class Application < Rails::Application

# (中略)

config.time_zone = 'Asia/Tokyo'

config.active_record.default_timezone = :local

end

end

# 課金サブシステム rails console

kakin(production)> Deal.last.created_at

=> Wed, 12 Jun 2024 12:43:41.641262000 JST +09:00

kakin(production)>

# 課金サブシステム /config/application.rb

module Kakin

class Application < Rails::Application

# (中略)

config.time_zone = ‘Tokyo’

end

end

課金DB: Rails内ではJST、保存はUTC

本体DB: Rails内ではJST、保存もJST

本体DBがJST保存なのは現行PHP由来

58 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

/config/database.yml

58

class ApplicationRecord < ActiveRecord::Base

self.abstract_class = true

connects_to database: {

writing: :nicomanga_writer,

reading: :nicomanga_reader,

}

end

class Kakin::ApplicationRecord < ActiveRecord::Base

self.abstract_class = true

connects_to database: {

writing: :kakin_writer,

reading: :kakin_reader,

}

end

production:

nicomanga_writer:

<<: *nicomanga_default

host: <%= ENV['DB_HOST'] %>

nicomanga_reader:

<<: *nicomanga_default

host: <%= ENV['DB_RO_HOST'] %>

replica: true

kakin_writer:

<<: *kakin_default

host: <%= ENV[KAKIN_DB_HOST'] %>

kakin_reader:

<<: *kakin_default

host: <%= ENV[KAKIN_DB_RO_HOST'] %>

replica: true

59 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

default_timezoneを追加で指定した

(実際にはkakin_defaultエイリアス内に追加した)

59

class ApplicationRecord < ActiveRecord::Base

self.abstract_class = true

connects_to database: {

writing: :nicomanga_writer,

reading: :nicomanga_reader,

}

end

class Kakin::ApplicationRecord < ActiveRecord::Base

self.abstract_class = true

connects_to database: {

writing: :kakin_writer,

reading: :kakin_reader,

}

end

production:

nicomanga_writer:

<<: *nicomanga_default

host: <%= ENV['DB_HOST'] %>

nicomanga_reader:

<<: *nicomanga_default

host: <%= ENV['DB_RO_HOST'] %>

replica: true

kakin_writer:

<<: *kakin_default

host: <%= ENV[KAKIN_DB_HOST'] %>

default_timezone: utc

kakin_reader:

<<: *kakin_default

host: <%= ENV[KAKIN_DB_RO_HOST'] %>

replica: true

default_timezone: utc

60 of 133

新バックエンドに課金サブシステムのモデルをprefix付きで移植する

default_timezoneを追加で指定した

(実際にはkakin_defaultエイリアス内に追加した)

60

class ApplicationRecord < ActiveRecord::Base

self.abstract_class = true

connects_to database: {

writing: :nicomanga_writer,

reading: :nicomanga_reader,

}

end

class Kakin::ApplicationRecord < ActiveRecord::Base

self.abstract_class = true

connects_to database: {

writing: :kakin_writer,

reading: :kakin_reader,

}

end

production:

nicomanga_writer:

<<: *nicomanga_default

host: <%= ENV['DB_HOST'] %>

nicomanga_reader:

<<: *nicomanga_default

host: <%= ENV['DB_RO_HOST'] %>

replica: true

kakin_writer:

<<: *kakin_default

host: <%= ENV[KAKIN_DB_HOST'] %>

default_timezone: utc

kakin_reader:

<<: *kakin_default

host: <%= ENV[KAKIN_DB_RO_HOST'] %>

replica: true

default_timezone: utc

保存するタイムゾーンが

複数DB間で異なる場合、

database.ymlで上書きできる

https://github.com/rails/rails/blob/v7.1.3.3/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb#L171

61 of 133

Railsアプリケーションを統廃合する手順

  • 新バックエンドで課金DBを読み書きできるようにする
  • 新バックエンドに課金サブシステムのモデルをprefix付きで移植する
  • 新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
  • バッチを移植する
  • 本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
  • 課金サブシステムのアプリケーションサーバを除却する
  • 本体DBに課金DBのスキーマを丸ごとコピーする
  • 課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る
  • 新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む
  • 課金DBから本体DBへ各種レコードをbulk upsertで移行する
  • 課金DBが主、本体DBが従になっているところを逆転させる
  • 分析系ダッシュボードのデータソースを切り替える
  • 両DBへの同時書き込みを切り、課金DBを除却する

61

62 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

まずRequest specをコピーする

62

# 新バックエンド

# /spec/requests/kakin/deals_controller_index_spec.rb

describe Kakin::DealsController do

describe 'GET /kakin/deals' do

subject do

get('/kakin/deals', params:)

response

end

# (中略)

end

end

# 課金サブシステム

# /spec/requests/deals_controller_index_spec.rb

describe DealsController do

describe 'GET /deals' do

subject do

get('/deals', params:)

response

end

# (中略)

end

end

63 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

まずRequest specをコピーする

63

# 新バックエンド

# /spec/requests/kakin/deals_controller_index_spec.rb

describe Kakin::DealsController do

describe 'GET /kakin/deals' do

subject do

get('/kakin/deals', params:)

response

end

# (中略)

end

end

# 課金サブシステム

# /spec/requests/deals_controller_index_spec.rb

describe DealsController do

describe 'GET /deals' do

subject do

get('/deals', params:)

response

end

# (中略)

end

end

名前空間、pathに

prefixを付ける

64 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

routesはnamespaceを1段掘る

64

# 新バックエンド

# /config/routes.rb

Rails.application.routes.draw do

namespace :kakin do

resources :deals, only: [:index]

end

# (中略)

end

# 課金サブシステム

# /config/routes.rb

Rails.application.routes.draw do

resources :deals, only: [:index]

# (中略)

end

65 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

routesはnamespaceを1段掘る

65

# 新バックエンド

# /config/routes.rb

Rails.application.routes.draw do

namespace :kakin do

resources :deals, only: [:index]

end

# (中略)

end

# 課金サブシステム

# /config/routes.rb

Rails.application.routes.draw do

resources :deals, only: [:index]

# (中略)

end

namespaceを掘る

66 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

空のコントローラーを作る

66

# 新バックエンド

# /config/routes.rb

Rails.application.routes.draw do

namespace :kakin do

resources :deals, only: [:index]

end

# (中略)

end

# /app/controllers/kakin/deals_controller.rb

class Kakin::DealsController < Kakin::BaseController

def index; end

end

# 課金サブシステム

# /config/routes.rb

Rails.application.routes.draw do

resources :deals, only: [:index]

# (中略)

end

# /app/controllers/deals_controller.rb

class DealsController < BaseController

def index

deals = Deal.where(user_id: params[:user_id])

# (中略)

end

end

67 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

空のコントローラーを作る

67

# 新バックエンド

# /config/routes.rb

Rails.application.routes.draw do

namespace :kakin do

resources :deals, only: [:index]

end

# (中略)

end

# /app/controllers/kakin/deals_controller.rb

class Kakin::DealsController < Kakin::BaseController

def index; end

end

# 課金サブシステム

# /config/routes.rb

Rails.application.routes.draw do

resources :deals, only: [:index]

# (中略)

end

# /app/controllers/deals_controller.rb

class DealsController < BaseController

def index

deals = Deal.where(user_id: params[:user_id])

# (中略)

end

end

コントローラーの

基底クラスも

移植専用に用意

68 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

空のコントローラーを作る

68

# 新バックエンド

# /config/routes.rb

Rails.application.routes.draw do

namespace :kakin do

resources :deals, only: [:index]

end

# (中略)

end

# /app/controllers/kakin/deals_controller.rb

class Kakin::DealsController < Kakin::BaseController

def index; end

end

# 課金サブシステム

# /config/routes.rb

Rails.application.routes.draw do

resources :deals, only: [:index]

# (中略)

end

# /app/controllers/deals_controller.rb

class DealsController < BaseController

def index

deals = Deal.where(user_id: params[:user_id])

# (中略)

end

end

空のアクションを作り

テストがfailするのを確認

69 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

コントローラーの実装をコピーする

69

# 新バックエンド

# /config/routes.rb

Rails.application.routes.draw do

namespace :kakin do

resources :deals, only: [:index]

end

# (中略)

end

# /app/controllers/kakin/deals_controller.rb

class Kakin::DealsController < Kakin::BaseController

def index

deals = Kakin::Deal.where(user_id: params[:user_id])

# (中略)

end

end

# 課金サブシステム

# /config/routes.rb

Rails.application.routes.draw do

resources :deals, only: [:index]

# (中略)

end

# /app/controllers/deals_controller.rb

class DealsController < BaseController

def index

deals = Deal.where(user_id: params[:user_id])

# (中略)

end

end

実装をコピーして

テストがpassするのを確認

70 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

コントローラーの実装をコピーする

70

# 新バックエンド

# /config/routes.rb

Rails.application.routes.draw do

namespace :kakin do

resources :deals, only: [:index]

end

# (中略)

end

# /app/controllers/kakin/deals_controller.rb

class Kakin::DealsController < Kakin::BaseController

def index

deals = Kakin::Deal.where(user_id: params[:user_id])

# (中略)

end

end

# 課金サブシステム

# /config/routes.rb

Rails.application.routes.draw do

resources :deals, only: [:index]

# (中略)

end

# /app/controllers/deals_controller.rb

class DealsController < BaseController

def index

deals = Deal.where(user_id: params[:user_id])

# (中略)

end

end

実装をコピーして

テストがpassするのを確認

読み込み系は問題なく完了

71 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

しかし

書き込み系でテストがfailする

71

72 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

課金サブシステム側の実装がこう

72

# 新バックエンド

# 課金サブシステム

# /app/controllers/contents_controller.rb

class ContentsController < BaseController

def buy

deal = ActiveRecord::Base.transaction do

deal = Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

73 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

素直に移植するとこう

どこが問題か、わかりますか?

73

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = ActiveRecord::Base.transaction do

deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

# 課金サブシステム

# /app/controllers/contents_controller.rb

class ContentsController < BaseController

def buy

deal = ActiveRecord::Base.transaction do

deal = Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

74 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

素直に移植するとこう

どこが問題か、わかりますか?

74

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = ActiveRecord::Base.transaction do

deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

# 課金サブシステム

# /app/controllers/contents_controller.rb

class ContentsController < BaseController

def buy

deal = ActiveRecord::Base.transaction do

deal = Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

75 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

素直に移植するとこう

どこが問題か、わかりますか?

75

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = ActiveRecord::Base.transaction do

deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

# 課金サブシステム

# /app/controllers/contents_controller.rb

class ContentsController < BaseController

def buy

deal = ActiveRecord::Base.transaction do

deal = Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

ActiveRecord::Base.transaction

を使っている

76 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

素直に移植するとこう

どこが問題か、わかりますか?

76

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = ActiveRecord::Base.transaction do

deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

# 課金サブシステム

# /app/controllers/contents_controller.rb

class ContentsController < BaseController

def buy

deal = ActiveRecord::Base.transaction do

deal = Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

本体DBに対して

トランザクションを開いていた

課金DBをロールバックできていなかった

77 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

ActiveRecord::BaseをKakin::ApplicationRecordに書き換えて解決

ついでに既存APIが本体DBでトランザクションを開くことの明示のためにApplicationRecordに書き換え

77

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = Kakin::ApplicationRecord.transaction do

deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

# 課金サブシステム

# /app/controllers/contents_controller.rb

class ContentsController < BaseController

def buy

deal = ActiveRecord::Base.transaction do

deal = Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

78 of 133

新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する

ActiveRecord::BaseをKakin::ApplicationRecordに書き換えて解決

ついでに既存APIが本体DBでトランザクションを開くことの明示のためにApplicationRecordに書き換え

78

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = Kakin::ApplicationRecord.transaction do

deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

# 課金サブシステム

# /app/controllers/contents_controller.rb

class ContentsController < BaseController

def buy

deal = ActiveRecord::Base.transaction do

deal = Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

書き込み系も完了

79 of 133

Railsアプリケーションを統廃合する手順

  • 新バックエンドで課金DBを読み書きできるようにする
  • 新バックエンドに課金サブシステムのモデルをprefix付きで移植する
  • 新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
  • バッチを移植する
  • 本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
  • 課金サブシステムのアプリケーションサーバを除却する
  • 本体DBに課金DBのスキーマを丸ごとコピーする
  • 課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る
  • 新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む
  • 課金DBから本体DBへ各種レコードをbulk upsertで移行する
  • 課金DBが主、本体DBが従になっているところを逆転させる
  • 分析系ダッシュボードのデータソースを切り替える
  • 両DBへの同時書き込みを切り、課金DBを除却する

79

80 of 133

バッチを移植する

移植した

コントローラーの移植とほぼ同じなので省略

80

81 of 133

Railsアプリケーションを統廃合する手順

  • 新バックエンドで課金DBを読み書きできるようにする
  • 新バックエンドに課金サブシステムのモデルをprefix付きで移植する
  • 新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
  • バッチを移植する
  • 本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
  • 課金サブシステムのアプリケーションサーバを除却する
  • 本体DBに課金DBのスキーマを丸ごとコピーする
  • 課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る
  • 新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む
  • 課金DBから本体DBへ各種レコードをbulk upsertで移行する
  • 課金DBが主、本体DBが従になっているところを逆転させる
  • 分析系ダッシュボードのデータソースを切り替える
  • 両DBへの同時書き込みを切り、課金DBを除却する

81

82 of 133

本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える

  • 現行PHPの課金サブシステム向けクライアントクラスを改修
    • ユーザID指定で新バックエンドにリクエストさせる
      • 自分のユーザIDを指定して問題なく購入できることを確認
    • 割合(ユーザIDの100の剰余)で新バックエンドにリクエストさせる
    • 100%開放
  • 特にトラブルもなく切り替え完了
    • pathにprefixが付く以外は全て互換としてAPIを移植したので楽に切り替えできた

82

83 of 133

Railsアプリケーションを統廃合する手順

  • 新バックエンドで課金DBを読み書きできるようにする
  • 新バックエンドに課金サブシステムのモデルをprefix付きで移植する
  • 新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
  • バッチを移植する
  • 本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
  • 課金サブシステムのアプリケーションサーバを除却する
  • 本体DBに課金DBのスキーマを丸ごとコピーする
  • 課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る
  • 新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む
  • 課金DBから本体DBへ各種レコードをbulk upsertで移行する
  • 課金DBが主、本体DBが従になっているところを逆転させる
  • 分析系ダッシュボードのデータソースを切り替える
  • 両DBへの同時書き込みを切り、課金DBを除却する

83

84 of 133

課金サブシステムのアプリケーションサーバを除却する

除却した

アプリケーションサーバは

ECS Fargateなのでタスクを0個にするだけ

ELBなどもterraformで記述していたのでサクッと

84

85 of 133

Railsアプリケーションを統廃合する手順

  • 新バックエンドで課金DBを読み書きできるようにする
  • 新バックエンドに課金サブシステムのモデルをprefix付きで移植する
  • 新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
  • バッチを移植する
  • 本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
  • 課金サブシステムのアプリケーションサーバを除却する
  • 本体DBに課金DBのスキーマを丸ごとコピーする
  • 課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る
  • 新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む
  • 課金DBから本体DBへ各種レコードをbulk upsertで移行する
  • 課金DBが主、本体DBが従になっているところを逆転させる
  • 分析系ダッシュボードのデータソースを切り替える
  • 両DBへの同時書き込みを切り、課金DBを除却する

85

86 of 133

本体DBに課金DBのスキーマを丸ごとコピーする

コピーした

新バックエンドは現行PHPとDBを共有しているため

Railsのマイグレーション機能ではなくridgepoleを使っており、

課金DBからスキーマをダンプして本体DBにコピペしただけ

86

87 of 133

Railsアプリケーションを統廃合する手順

  • 新バックエンドで課金DBを読み書きできるようにする
  • 新バックエンドに課金サブシステムのモデルをprefix付きで移植する
  • 新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
  • バッチを移植する
  • 本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
  • 課金サブシステムのアプリケーションサーバを除却する
  • 本体DBに課金DBのスキーマを丸ごとコピーする
  • 課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る
  • 新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む
  • 課金DBから本体DBへ各種レコードをbulk upsertで移行する
  • 課金DBが主、本体DBが従になっているところを逆転させる
  • 分析系ダッシュボードのデータソースを切り替える
  • 両DBへの同時書き込みを切り、課金DBを除却する

87

88 of 133

課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る

新バックエンド内でKakin名前空間を外したモデルを作る

88

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

end

# 新バックエンド /spec/factories/deal.rb

FactoryBot.define do

factory :deal do

sequence(:user_id) { |n| "user_id_#{n}" }

end

end

# 新バックエンド /models/kakin/deal.rb

class Kakin::Deal < Kakin::ApplicationRecord

has_many :coin_balance_changed_events,

class_name: 'Kakin::CoinBalanceChangedEvent’

validates :user_id, presence: true

end

# 新バックエンド /spec/factories/kakin_deal.rb

FactoryBot.define do

factory :kakin_deal, class: 'Kakin::Deal' do

sequence(:user_id) { |n| "user_id_#{n}" }

end

end

89 of 133

課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る

新バックエンド内でKakin名前空間を外したモデルを作る

89

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

end

# 新バックエンド /spec/factories/deal.rb

FactoryBot.define do

factory :deal do

sequence(:user_id) { |n| "user_id_#{n}" }

end

end

# 新バックエンド /models/kakin/deal.rb

class Kakin::Deal < Kakin::ApplicationRecord

has_many :coin_balance_changed_events,

class_name: 'Kakin::CoinBalanceChangedEvent’

validates :user_id, presence: true

end

# 新バックエンド /spec/factories/kakin_deal.rb

FactoryBot.define do

factory :kakin_deal, class: 'Kakin::Deal' do

sequence(:user_id) { |n| "user_id_#{n}" }

end

end

継承元を本体DBの

ApplicationRecordに

90 of 133

課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る

新バックエンド内でKakin名前空間を外したモデルを作る

90

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

end

# 新バックエンド /spec/factories/deal.rb

FactoryBot.define do

factory :deal do

sequence(:user_id) { |n| "user_id_#{n}" }

end

end

# 新バックエンド /models/kakin/deal.rb

class Kakin::Deal < Kakin::ApplicationRecord

has_many :coin_balance_changed_events,

class_name: 'Kakin::CoinBalanceChangedEvent’

validates :user_id, presence: true

end

# 新バックエンド /spec/factories/kakin_deal.rb

FactoryBot.define do

factory :kakin_deal, class: 'Kakin::Deal' do

sequence(:user_id) { |n| "user_id_#{n}" }

end

end

リレーションの

class_name指定は不要になる

91 of 133

課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る

新バックエンド内でKakin名前空間を外したモデルを作る

91

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

end

# 新バックエンド /spec/factories/deal.rb

FactoryBot.define do

factory :deal do

sequence(:user_id) { |n| "user_id_#{n}" }

end

end

# 新バックエンド /models/kakin/deal.rb

class Kakin::Deal < Kakin::ApplicationRecord

has_many :coin_balance_changed_events,

class_name: 'Kakin::CoinBalanceChangedEvent’

validates :user_id, presence: true

end

# 新バックエンド /spec/factories/kakin_deal.rb

FactoryBot.define do

factory :kakin_deal, class: 'Kakin::Deal' do

sequence(:user_id) { |n| "user_id_#{n}" }

end

end

factoryも

class指定が不要になる

92 of 133

Railsアプリケーションを統廃合する手順

  • 新バックエンドで課金DBを読み書きできるようにする
  • 新バックエンドに課金サブシステムのモデルをprefix付きで移植する
  • 新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
  • バッチを移植する
  • 本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
  • 課金サブシステムのアプリケーションサーバを除却する
  • 本体DBに課金DBのスキーマを丸ごとコピーする
  • 課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る
  • 新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む
  • 課金DBから本体DBへ各種レコードをbulk upsertで移行する
  • 課金DBが主、本体DBが従になっているところを逆転させる
  • 分析系ダッシュボードのデータソースを切り替える
  • 両DBへの同時書き込みを切り、課金DBを除却する

92

93 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

この時点で新バックエンドの実装

93

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = Kakin::ApplicationRecord.transaction do

deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

end

94 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

Kakin::Dealクラスのインスタンスを受け取り、

Dealクラスのインスタンスをupsertするdouble_writeクラスメソッドを作る

94

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = Kakin::ApplicationRecord.transaction do

deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

def self.double_write(kakin_deal)

deal = find_or_initialize_by(id: kakin_deal.id)

deal.update!(

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at

)

deal

end

end

95 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

Kakin::Dealクラスのインスタンスを受け取り、

Dealクラスのインスタンスをupsertするdouble_writeクラスメソッドを作る

95

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = Kakin::ApplicationRecord.transaction do

deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

def self.double_write(kakin_deal)

deal = find_or_initialize_by(id: kakin_deal.id)

deal.update!(

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at

)

deal

end

end

渡されたKakin::Dealクラスのインスタンスの�idでfind_or_initialize_byする

96 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

Kakin::Dealクラスのインスタンスを受け取り、

Dealクラスのインスタンスをupsertするdouble_writeクラスメソッドを作る

96

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = Kakin::ApplicationRecord.transaction do

deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

def self.double_write(kakin_deal)

deal = find_or_initialize_by(id: kakin_deal.id)

deal.update!(

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at

)

deal

end

end

created_at, updated_atまで

含めて全てupdate!する

(値が全て同じならUPDATE文は発行されない)

97 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

Kakin::Dealクラスのインスタンスを受け取り、

Dealクラスのインスタンスをupsertするdouble_writeクラスメソッドを作る

97

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = Kakin::ApplicationRecord.transaction do

deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

end

end

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

def self.double_write(kakin_deal)

deal = find_or_initialize_by(id: kakin_deal.id)

deal.update!(

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at

)

deal

end

end

98 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

コントローラーで両方のトランザクションを開き、

Dealクラスにdouble_writeする

98

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = Kakin::ApplicationRecord.transaction do

ApplicationRecord.transaction do

kakin_deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

)

Deal.double_write(kakin_deal)

# (中略)

end

end

end

end

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

def self.double_write(kakin_deal)

deal = find_or_initialize_by(id: kakin_deal.id)

deal.update!(

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at

)

deal

end

end

本体DBにも

トランザクションを開く

99 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

コントローラーで両方のトランザクションを開き、double_writeする

99

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = Kakin::ApplicationRecord.transaction do

ApplicationRecord.transaction do

kakin_deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

)

Deal.double_write(kakin_deal)

# (中略)

end

end

end

end

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

def self.double_write(kakin_deal)

deal = find_or_initialize_by(id: kakin_deal.id)

deal.update!(

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at

)

deal

end

end

本体DBのDealクラスにもdouble_writeする

100 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

コントローラーで両方のトランザクションを開き、double_writeする

100

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = Kakin::ApplicationRecord.transaction do

ApplicationRecord.transaction do

kakin_deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

)

Deal.double_write(kakin_deal)

# (中略)

end

end

end

end

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

def self.double_write(kakin_deal)

deal = find_or_initialize_by(id: kakin_deal.id)

deal.update!(

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at

)

deal

end

end

関連するレコードを全てeachで複製する

101 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

作成したレコードだけでなく、その関連レコードも全て複製する

101

# 新バックエンド /app/services/kakin/consume_coin_service.rb

Kakin::CoinBalanceChangedEvent.create!(

owned_coin_id:,

deal_id: @deal.id,

changed_coins: -[deficit, balance].min,

is_washed:

)

kakin_owned_coin = Kakin::OwnedCoin.find(owned_coin_id)

Deal.double_write(kakin_owned_coin.deal)

owned_coin = OwnedCoin.double_write(kakin_owned_coin)

kakin_owned_coin .coin_balance_changed_events.each do |kakin_coin_balance_changed_event|

Deal.double_write(kakin_coin_balance_changed_event.deal)

OwnedCoin.double_write(kakin_coin_balance_changed_event.owned_coin)

CoinBalanceChangedEvent.double_write(kakin_coin_balance_changed_event)

end

# 以降略

関連レコードを全て複製する分

レスポンスタイムは伸びるが

分間リクエスト数から考えて

影響は軽微と判断

102 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

request specに複製書き込みのテストを書く

102

end

# (中略)

end

end

# 新バックエンド

# /spec/requests/kakin/contents_controller_buy_spec.rb

describe Kakin::ContentsController do

describe 'POST /kakin/contents/buy' do

subject do

post('/kakin/contents/buy', params:)

response

end

# (中略)

context 'コインの足りるユーザがエピソード購入するとき' do

it '200が返り、openapiのスキーマに一致する' do

expect(subject).to have_http_status(200)

assert_schema_conform(200)

end

# 元々あったテストケース群

# (中略)

# 右に続く

103 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

request specに複製書き込みのテストを書く

103

describe 'main DBにも複製される' do

it 'dealが複製される' do

subject

kakin_deal = Kakin::Deal.last

expect(Deal.find(kakin_deal.id)).to have_attributes(

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at

)

end

# (中略)

end

end

# (中略)

end

end

# 新バックエンド

# /spec/requests/kakin/contents_controller_buy_spec.rb

describe Kakin::ContentsController do

describe 'POST /kakin/contents/buy' do

subject do

post('/kakin/contents/buy', params:)

response

end

# (中略)

context 'コインの足りるユーザがエピソード購入するとき' do

it '200が返り、openapiのスキーマに一致する' do

expect(subject).to have_http_status(200)

assert_schema_conform(200)

end

# 元々あったテストケース群

# (中略)

# 右に続く

104 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

request specに複製書き込みのテストを書く

104

describe 'main DBにも複製される' do

it 'dealが複製される' do

subject

kakin_deal = Kakin::Deal.last

expect(Deal.find(kakin_deal.id)).to have_attributes(

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at

)

end

# (中略)

end

end

# (中略)

end

end

# 新バックエンド

# /spec/requests/kakin/contents_controller_buy_spec.rb

describe Kakin::ContentsController do

describe 'POST /kakin/contents/buy' do

subject do

post('/kakin/contents/buy', params:)

response

end

# (中略)

context 'コインの足りるユーザがエピソード購入するとき' do

it '200が返り、openapiのスキーマに一致する' do

expect(subject).to have_http_status(200)

assert_schema_conform(200)

end

# 元々あったテストケース群

# (中略)

# 右に続く

各contextの複製書き込みに関する

テストケースはdescribeにまとめた

double writeを切るときに消すのが簡単

105 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

request specに複製書き込みのテストを書く

105

describe 'main DBにも複製される' do

it 'dealが複製される' do

subject

kakin_deal = Kakin::Deal.last

expect(Deal.find(kakin_deal.id)).to have_attributes(

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at

)

end

# (中略)

end

end

# (中略)

end

end

# 新バックエンド

# /spec/requests/kakin/contents_controller_buy_spec.rb

describe Kakin::ContentsController do

describe 'POST /kakin/contents/buy' do

subject do

post('/kakin/contents/buy', params:)

response

end

# (中略)

context 'コインの足りるユーザがエピソード購入するとき' do

it '200が返り、openapiのスキーマに一致する' do

expect(subject).to have_http_status(200)

assert_schema_conform(200)

end

# 元々あったテストケース群

# (中略)

# 右に続く

各contextの複製書き込みに関する

テストケースはdescribeにまとめた

double writeを切るときに消すのが簡単

idによるfind_or_initialize_byでは

上手くいかないケース

106 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

ActiveRecordのフックでレコードを作成している場合、

コントローラー側で受け取れないので複製できない

106

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

# (中略)

after_create :upsert_episode_viewing_right,

if: :buy_episode?

def upsert_episode_viewing_right

EpisodeViewingRight.upsert!(

user_id:,

episode_id:,

comic_id:,

expired_at:

)

end

# (中略)

end

107 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

ActiveRecordフックで作成する側のモデルのdouble_writeクラスメソッド内で子モデルも作成

子モデルはidではなくunique keyでfind_or_initialize_byする

107

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

# (中略)

def self.double_write(kakin_deal)

deal = find_or_initialize_by(id: kakin_deal.id)

# (中略)

deal

end

108 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

ActiveRecordフックで作成する側のモデルのdouble_writeクラスメソッド内で子モデルも作成

子モデルはidではなくunique keyでfind_or_initialize_byする

108

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

# (中略)

def self.double_write(kakin_deal)

deal = find_or_initialize_by(id: kakin_deal.id)

# (中略)

if kakin_deal.buy_episode?

kakin_episode_viewing_right =

Kakin::EpisodeViewingRight.find_by(

user_id: kakin_deal.user_id,

episode_id: kakin_deal.episode_id

)

EpisodeViewingRight.double_write(

kakin_episode_viewing_right

)

end

deal

end

user_idとepisode_idの

組み合わせがunique keyなので

これでfind_byしてdouble_write

109 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

ActiveRecordフックで作成する側のモデルのdouble_writeクラスメソッド内で子モデルも作成

子モデルはidではなくunique keyでfind_or_initialize_byする

109

# 新バックエンド /models/episode_viewing_right.rb

class EpisodeViewingRight< ApplicationRecord

# (中略)

def self.double_write(kakin_episode_viewing_right)

episode_viewing_right = find_or_initialize_by(

user_id: kakin_deal.user_id,

episode_id: kakin_deal.episode_id

)

episode_viewing_right.update!(

id: kakin_episode_viewing_right.id,

# (中略)

created_at: kakin_episode_viewing_right.created_at,

updated_at: kakin_episode_viewing_right.updated_at

)

episode_viewing_right

# (中略)

end

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

# (中略)

def self.double_write(kakin_deal)

deal = find_or_initialize_by(id: kakin_deal.id)

# (中略)

if kakin_deal.buy_episode?

kakin_episode_viewing_right =

Kakin::EpisodeViewingRight.find_by(

user_id: kakin_deal.user_id,

episode_id: kakin_deal.episode_id

)

EpisodeViewingRight.double_write(

kakin_episode_viewing_right

)

end

deal

end

110 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

ActiveRecordフックで作成する側のモデルのdouble_writeクラスメソッド内で子モデルも作成

子モデルはidではなくunique keyでfind_or_initialize_byする

110

# 新バックエンド /models/episode_viewing_right.rb

class EpisodeViewingRight< ApplicationRecord

# (中略)

def self.double_write(kakin_episode_viewing_right)

episode_viewing_right = find_or_initialize_by(

user_id: kakin_deal.user_id,

episode_id: kakin_deal.episode_id

)

episode_viewing_right.update!(

id: kakin_episode_viewing_right.id,

# (中略)

created_at: kakin_episode_viewing_right.created_at,

updated_at: kakin_episode_viewing_right.updated_at

)

episode_viewing_right

# (中略)

end

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

# (中略)

def self.double_write(kakin_deal)

deal = find_or_initialize_by(id: kakin_deal.id)

# (中略)

if kakin_deal.buy_episode?

kakin_episode_viewing_right =

Kakin::EpisodeViewingRight.find_by(

user_id: kakin_deal.user_id,

episode_id: kakin_deal.episode_id

)

EpisodeViewingRight.double_write(

kakin_episode_viewing_right

)

end

deal

end

user_idとepisode_idでfind_or_initialize_by

111 of 133

新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む

ActiveRecordフックで作成する側のモデルのdouble_writeクラスメソッド内で子モデルも作成

子モデルはidではなくunique keyでfind_or_initialize_byする

111

# 新バックエンド /models/episode_viewing_right.rb

class EpisodeViewingRight< ApplicationRecord

# (中略)

def self.double_write(kakin_episode_viewing_right)

episode_viewing_right = find_or_initialize_by(

user_id: kakin_deal.user_id,

episode_id: kakin_deal.episode_id

)

episode_viewing_right.update!(

id: kakin_episode_viewing_right.id,

# (中略)

created_at: kakin_episode_viewing_right.created_at,

updated_at: kakin_episode_viewing_right.updated_at

)

episode_viewing_right

# (中略)

end

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

# (中略)

def self.double_write(kakin_deal)

deal = find_or_initialize_by(id: kakin_deal.id)

# (中略)

if kakin_deal.buy_episode?

kakin_episode_viewing_right =

Kakin::EpisodeViewingRight.find_by(

user_id: kakin_deal.user_id,

episode_id: kakin_deal.episode_id

)

EpisodeViewingRight.double_write(

kakin_episode_viewing_right

)

end

deal

end

update!するときに「idを」updateする

逆にuser_idとepisode_idはupdateしない

112 of 133

Railsアプリケーションを統廃合する手順

  • 新バックエンドで課金DBを読み書きできるようにする
  • 新バックエンドに課金サブシステムのモデルをprefix付きで移植する
  • 新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
  • バッチを移植する
  • 本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
  • 課金サブシステムのアプリケーションサーバを除却する
  • 本体DBに課金DBのスキーマを丸ごとコピーする
  • 課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る
  • 新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む
  • 課金DBから本体DBへ各種レコードをbulk upsertで移行する
  • 課金DBが主、本体DBが従になっているところを逆転させる
  • 分析系ダッシュボードのデータソースを切り替える
  • 両DBへの同時書き込みを切り、課金DBを除却する

112

113 of 133

課金DBから本体DBへ各種レコードをbulk upsertで移行する

Kakin::Dealクラスのインスタンスの配列を受け取って

upsert_allするbulk_upsertクラスメソッドを実装

113

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

# (中略)

end

114 of 133

課金DBから本体DBへ各種レコードをbulk upsertで移行する

Kakin::Dealクラスのインスタンスの配列を受け取って

upsert_allするbulk_upsertクラスメソッドを実装

114

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

# (中略)

def self.bulk_upsert(kakin_deals)

params = kakin_deals.map do |kakin_deal|

{

id: kakin_deal.id,

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at,

}

end

upsert_all(params, record_timestamps: false)

end

end

115 of 133

課金DBから本体DBへ各種レコードをbulk upsertで移行する

Kakin::Dealクラスのインスタンスの配列を受け取って

upsert_allするbulk_upsertクラスメソッドを実装

115

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

# (中略)

def self.bulk_upsert(kakin_deals)

params = kakin_deals.map do |kakin_deal|

{

id: kakin_deal.id,

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at,

}

end

upsert_all(params, record_timestamps: false)

end

end

idとcreated_at, updated_atも

upsertのparamsに含める

idを含めないと自動採番されてしまう

116 of 133

課金DBから本体DBへ各種レコードをbulk upsertで移行する

Kakin::Dealクラスのインスタンスの配列を受け取って

upsert_allするbulk_upsertクラスメソッドを実装

116

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

# (中略)

def self.bulk_upsert(kakin_deals)

params = kakin_deals.map do |kakin_deal|

{

id: kakin_deal.id,

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at,

}

end

upsert_all(params, record_timestamps: false)

end

end

record_timestampsオプションに

falseを指定してタイムスタンプ

更新しないのを明示

117 of 133

課金DBから本体DBへ各種レコードをbulk upsertで移行する

find_in_batchesの結果をbulk_upsertクラスメソッドに渡すような

rakeタスクを各モデルについて作成、実行

117

# 新バックエンド /lib/tasks/migrate_kakin.rb

namespace :migrate do

namespace :kakin do

task deal: :environment do

Kakin::Deal.find_in_batches(of: 100) do |kakin_deals|

first_id = kakin_deals.first.id

last_id = kakin_deals.last.id

Rails.logger.info "deals: #{first_id} .. #{last_id}"

Deal.bulk_upsert(kakin_deals)

end

end

# 他のモデルについても同様

end

end

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

# (中略)

def self.bulk_upsert(kakin_deals)

params = kakin_deals.map do |kakin_deal|

{

id: kakin_deal.id,

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at,

}

end

upsert_all(params, record_timestamps: false)

end

end

118 of 133

課金DBから本体DBへ各種レコードをbulk upsertで移行する

find_in_batchesの結果をbulk_upsertクラスメソッドに渡すような

rakeタスクを各モデルについて作成、実行

118

# 新バックエンド /lib/tasks/migrate_kakin.rb

namespace :migrate do

namespace :kakin do

task deal: :environment do

Kakin::Deal.find_in_batches(of: 100) do |kakin_deals|

first_id = kakin_deals.first.id

last_id = kakin_deals.last.id

Rails.logger.info "deals: #{first_id} .. #{last_id}"

Deal.bulk_upsert(kakin_deals)

end

end

# 他のモデルについても同様

end

end

# 新バックエンド /models/deal.rb

class Deal < ApplicationRecord

has_many :coin_balance_changed_events

validates :user_id, presence: true

# (中略)

def self.bulk_upsert(kakin_deals)

params = kakin_deals.map do |kakin_deal|

{

id: kakin_deal.id,

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at,

}

end

upsert_all(params, record_timestamps: false)

end

end

実行した

元々は単純にfind_eachで1件ずつdouble_writeしようとしていたが、

ちょっと動かしてみて所要時間が推定1週間以上になったので断念

119 of 133

Railsアプリケーションを統廃合する手順

  • 新バックエンドで課金DBを読み書きできるようにする
  • 新バックエンドに課金サブシステムのモデルをprefix付きで移植する
  • 新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
  • バッチを移植する
  • 本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
  • 課金サブシステムのアプリケーションサーバを除却する
  • 本体DBに課金DBのスキーマを丸ごとコピーする
  • 課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る
  • 新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む
  • 課金DBから本体DBへ各種レコードをbulk upsertで移行する
  • 課金DBが主、本体DBが従になっているところを逆転させる
  • 分析系ダッシュボードのデータソースを切り替える
  • 両DBへの同時書き込みを切り、課金DBを除却する

119

120 of 133

課金DBが主、本体DBが従になっているところを逆転させる

課金DBに作成→本体DBに複製 となっている順番を逆にする

request specも本体DBを主とする

120

# 新バックエンド

# /spec/requests/kakin/contents_controller_buy_spec.rb

describe Kakin::ContentsController do

describe 'POST /kakin/contents/buy' do

# (中略)

describe 'main DBにも複製される' do

it 'dealが複製される' do

subject

kakin_deal = Kakin::Deal.last

expect(Deal.find(kakin_deal.id)).to have_attributes(

user_id: kakin_deal.user_id,

# (中略)

created_at: kakin_deal.created_at,

updated_at: kakin_deal.updated_at

)

# (中略)

end

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = Kakin::ApplicationRecord.transaction do

ApplicationRecord.transaction do

kakin_deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

)

Deal.double_write(kakin_deal)

# (中略)

end

end

end

end

121 of 133

課金DBが主、本体DBが従になっているところを逆転させる

課金DBに作成→本体DBに複製 となっている順番を逆にする

request specも本体DBを主とする

121

# 新バックエンド

# /spec/requests/kakin/contents_controller_buy_spec.rb

describe Kakin::ContentsController do

describe 'POST /kakin/contents/buy' do

# (中略)

describe 'kakin DBにも複製される' do

it 'dealが複製される' do

subject

deal = Deal.last

expect(Kakin::Deal.find(deal.id)).to have_attributes(

user_id: deal.user_id,

# (中略)

created_at: deal.created_at,

updated_at: deal.updated_at

)

# (中略)

end

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = Kakin::ApplicationRecord.transaction do

ApplicationRecord.transaction do

kakin_deal = Kakin::Deal.create!(

user_id: params[:user_id],

# (中略)

)

Deal.double_write(kakin_deal)

# (中略)

end

end

end

end

まずrequest specだけ書き換える

両方のDBに同じ内容を

書き込んでいるのでpassするはず

122 of 133

課金DBが主、本体DBが従になっているところを逆転させる

課金DBに作成→本体DBに複製 となっている順番を逆にする

request specも本体DBを主とする

122

# 新バックエンド

# /spec/requests/kakin/contents_controller_buy_spec.rb

describe Kakin::ContentsController do

describe 'POST /kakin/contents/buy' do

# (中略)

describe 'kakin DBにも複製される' do

it 'dealが複製される' do

subject

deal = Deal.last

expect(Kakin::Deal.find(deal.id)).to have_attributes(

user_id: deal.user_id,

# (中略)

created_at: deal.created_at,

updated_at: deal.updated_at

)

# (中略)

end

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = Kakin::ApplicationRecord.transaction do

ApplicationRecord.transaction do

deal = Deal.create!(

user_id: params[:user_id],

# (中略)

)

Kakin::Deal.double_write(deal)

# (中略)

end

end

end

end

実装も本体DBに作成して

課金DBにdouble_writeする順番に変更

Kakin側のモデルにもdouble_writeを実装

123 of 133

Railsアプリケーションを統廃合する手順

  • 新バックエンドで課金DBを読み書きできるようにする
  • 新バックエンドに課金サブシステムのモデルをprefix付きで移植する
  • 新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
  • バッチを移植する
  • 本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
  • 課金サブシステムのアプリケーションサーバを除却する
  • 本体DBに課金DBのスキーマを丸ごとコピーする
  • 課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る
  • 新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む
  • 課金DBから本体DBへ各種レコードをbulk upsertで移行する
  • 課金DBが主、本体DBが従になっているところを逆転させる
  • 分析系ダッシュボードのデータソースを切り替える
  • 両DBへの同時書き込みを切り、課金DBを除却する

123

124 of 133

分析系ダッシュボードのデータソースを切り替える

  • 本体DB、課金DBはAuroraのスナップショット経由でBigQueryにexportしている
    • 我々ではなくサービス分析課(別部署)管轄
  • 営業・運営はLookerStudioでダッシュボードを作成し可視化している
  • 本体DBと課金DBでスナップショット取得時刻が異なっていた
    • 本体DBのスナップショット取得時刻は毎日2時過ぎ(JST)
      • Auroraの自動スナップショットを利用
    • 課金DBのスナップショット取得時刻は毎日11時過ぎ(JST)
      • こちらもAuroraの自動スナップショットだが、その時刻を変更
      • エピソードの公開が原則11時からなので、購入数の集計を11時時点でやると都合がよい
  • 営業・運営から課金関連のスナップショット取得時刻は11時時点を維持してほしいという要望

124

125 of 133

分析系ダッシュボードのデータソースを切り替える

  • 本体DB、課金DBはAuroraのスナップショット経由でBigQueryにexportしている
    • 我々ではなくサービス分析課(別部署)管轄
  • 営業・運営はLookerStudioでダッシュボードを作成し可視化している
  • 本体DBと課金DBでスナップショット取得時刻が異なっていた
    • 本体DBのスナップショット取得時刻は毎日2時過ぎ(JST)
      • Auroraの自動スナップショットを利用
    • 課金DBのスナップショット取得時刻は毎日11時過ぎ(JST)
      • こちらもAuroraの自動スナップショットだが、その時刻を変更
      • エピソードの公開が原則11時からなので、購入数の集計を11時時点でやると都合がよい
  • 営業・運営から課金関連のスナップショット取得時刻は11時時点を維持してほしいという要望

125

任意時刻にexportを実行できるように

サービス分析課に改修依頼

126 of 133

分析系ダッシュボードのデータソースを切り替える

  • 本体DB、課金DBはAuroraのスナップショット経由でBigQueryにexportしている
    • 我々ではなくサービス分析課(別部署)管轄
  • 営業・運営はLookerStudioでダッシュボードを作成し可視化している
  • 本体DBと課金DBでスナップショット取得時刻が異なっていた
    • 本体DBのスナップショット取得時刻は毎日2時過ぎ(JST)
      • Auroraの自動スナップショットを利用
    • 課金DBのスナップショット取得時刻は毎日11時過ぎ(JST)
      • こちらもAuroraの自動スナップショットだが、その時刻を変更
      • エピソードの公開が原則11時からなので、購入数の集計を11時時点でやると都合がよい
  • 営業・運営から課金関連のスナップショット取得時刻は11時時点を維持してほしいという要望

126

サービス分析課側での改修は完了

127 of 133

分析系ダッシュボードのデータソースを切り替える

  • 本体DB、課金DBはAuroraのスナップショット経由でBigQueryにexportしている
    • 我々ではなくサービス分析課(別部署)管轄
  • 営業・運営はLookerStudioでダッシュボードを作成し可視化している
  • 本体DBと課金DBでスナップショット取得時刻が異なっていた
    • 本体DBのスナップショット取得時刻は毎日2時過ぎ(JST)
      • Auroraの自動スナップショットを利用
    • 課金DBのスナップショット取得時刻は毎日11時過ぎ(JST)
      • こちらもAuroraの自動スナップショットだが、その時刻を変更
      • エピソードの公開が原則11時からなので、購入数の集計を11時時点でやると都合がよい
  • 営業・運営から課金関連のスナップショット取得時刻は11時時点を維持してほしいという要望

127

スケジュールクエリ等の

差し替えはこれから

イマココ

128 of 133

Railsアプリケーションを統廃合する手順

  • 新バックエンドで課金DBを読み書きできるようにする
  • 新バックエンドに課金サブシステムのモデルをprefix付きで移植する
  • 新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
  • バッチを移植する
  • 本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
  • 課金サブシステムのアプリケーションサーバを除却する
  • 本体DBに課金DBのスキーマを丸ごとコピーする
  • 課金サブシステムのモデルのprefixを外した、本体DBに書く以外は全く同じモデルを作る
  • 新バックエンドの課金サブシステム互換APIにおいて両DBへ同時に書き込む
  • 課金DBから本体DBへ各種レコードをbulk upsertで移行する
  • 課金DBが主、本体DBが従になっているところを逆転させる
  • 分析系ダッシュボードのデータソースを切り替える
  • 両DBへの同時書き込みを切り、課金DBを除却する

128

129 of 133

両DBへの同時書き込みを切り、課金DBを除却する

実装でdouble_writeしている箇所と

specの複製に関するdescribeを削除すればいいはず

129

# 新バックエンド

# /spec/requests/kakin/contents_controller_buy_spec.rb

describe Kakin::ContentsController do

describe 'POST /kakin/contents/buy' do

# (中略)

describe 'kakin DBにも複製される' do

it 'dealが複製される' do

subject

deal = Deal.last

expect(Kakin::Deal.find(deal.id)).to have_attributes(

user_id: deal.user_id,

# (中略)

created_at: deal.created_at,

updated_at: deal.updated_at

)

# (中略)

end

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = Kakin::ApplicationRecord.transaction do

ApplicationRecord.transaction do

deal = Deal.create!(

user_id: params[:user_id],

# (中略)

)

Kakin::Deal.double_write(deal)

# (中略)

end

end

end

end

130 of 133

両DBへの同時書き込みを切り、課金DBを除却する

実装でdouble_writeしている箇所と

specの複製に関するdescribeを削除すればいいはず

130

# 新バックエンド

# /spec/requests/kakin/contents_controller_buy_spec.rb

describe Kakin::ContentsController do

describe 'POST /kakin/contents/buy' do

# (中略)

end

# 新バックエンド

# /app/controllers/kakin/contents_controller.rb

class Kakin::ContentsController < Kakin::BaseController

def buy

deal = ApplicationRecord.transaction do

deal = Deal.create!(

user_id: params[:user_id],

# (中略)

)

# (中略)

end

end

end

131 of 133

両DBへの同時書き込みを切り、課金DBを除却する

あとは単に課金DBを

除却すればいいはず

131

132 of 133

Railsアプリケーションを統廃合

できた!

(できるはず)

132

133 of 133

まとめ

  • Railsの複数データベース対応は負荷分散だけでなく�データベース間データ移行・アプリケーション統廃合にも使える
  • サービス無停止でのデータベース間データ移行の具体的手順
    • 完全互換なAPIを移植する
    • double writeする
    • bulk upsertで順次コピーする
    • double writeを切る
  • 下記のようなハマりどころがあった
    • 各アプリケーションのタイムゾーン設定
    • トランザクションをどちらのDBで開いているか
    • ActiveRecordフックでレコードを作成している場合
    • データ分析基盤へのエクスポートに使うスナップショットの作成時刻

133