Active Recordの
複数DB対応を活用して、Railsアプリケーションを
統廃合する
2024/10/17 フサギコ(髙﨑 尚人)
自己紹介
2
Active Recordの
複数DB対応を活用して、Railsアプリケーションを
統廃合する
2024/10/17 フサギコ(髙﨑 尚人)
おしながき
4
株式会社ブックウォーカー
5
株式会社ブックウォーカー
6
ニコニコ漫画
7
おしながき
8
Railsの複数データベース対応
9
Railsの複数データベース対応
10
Railsの複数データベース対応
11
複数データベース対応と聞いて
ほとんどの人が
思い浮かべるのは負荷分散
Railsの複数データベース対応
12
なんですが
Railsの複数データベース対応
13
今回お話するのは
複数データベース対応を活用した
データベース間データ移行�およびアプリケーション統廃合
おしながき
14
初期(2012年3月~)
サービス開始時はPHP製独自フレームワークのモノリス
15
internet
DB
ニコニコ漫画
(PHP独自FW)
単話課金開始(2019年2月~)
単話課金の実装にあたって、ドワンゴの基盤サービスを利用する課金サブシステムが誕生
16
internet
本体DB
課金サブシステム(Rails)
ドワンゴ
基盤サービス
ニコニコ漫画
(PHP独自FW)
課金DB
新バックエンド誕生(2020年4月~)
PHP製ニコニコ静画本体の改修容易性の低下が限界に達していたため、
Well-testedなバックエンド実現のためにRails製新バックエンドが誕生
17
internet
本体DB
課金サブシステム(Rails)
ドワンゴ
基盤サービス
ニコニコ漫画
(PHP独自FW)
課金DB
新バックエンド
(Rails)
新バックエンド誕生(2020年4月~)
PHP製ニコニコ静画本体の改修容易性の低下が限界に達していたため、
Well-testedなバックエンド実現のためにRails製新バックエンドが誕生
18
internet
本体DB
課金サブシステム(Rails)
ドワンゴ
基盤サービス
ニコニコ漫画
(PHP独自FW)
課金DB
新バックエンド
(Rails)
課金系なので
あまり触りたくない
という恐怖心
本体とDBを共有する特殊構成 &
小さく高速に実装サイクルを
回していきたい
課金サブシステムがドワンゴ基盤サービスから脱依存(2023年9月~)
諸事情により課金サブシステムがドワンゴ基盤サービスから脱依存
新バックエンドへの式年遷宮(ビジネスロジックの仕様明確化、移植)は順調
19
internet
本体DB
課金サブシステム(Rails)
ニコニコ漫画
(PHP独自FW)
課金DB
新バックエンド
(Rails)
課金サブシステムがドワンゴ基盤サービスから脱依存(2023年9月~)
諸事情により課金サブシステムがドワンゴ基盤サービスから脱依存
新バックエンドへの式年遷宮(ビジネスロジックの仕様明確化、移植)は順調
20
internet
本体DB
課金サブシステム(Rails)
ニコニコ漫画
(PHP独自FW)
課金DB
新バックエンド
(Rails)
その後
しばらくして気付いた
課金サブシステムがドワンゴ基盤サービスから脱依存(2023年9月~)
諸事情により課金サブシステムがドワンゴ基盤サービスから脱依存
新バックエンドへの式年遷宮(ビジネスロジックの仕様明確化、移植)は順調
21
internet
本体DB
課金サブシステム(Rails)
ニコニコ漫画
(PHP独自FW)
課金DB
新バックエンド
(Rails)
小さく高速に実装サイクルを
回せており成功体験
課金サブシステムがドワンゴ基盤サービスから脱依存(2023年9月~)
諸事情により課金サブシステムがドワンゴ基盤サービスから脱依存
新バックエンドへの式年遷宮(ビジネスロジックの仕様明確化、移植)は順調
22
internet
本体DB
課金サブシステム(Rails)
ニコニコ漫画
(PHP独自FW)
課金DB
新バックエンド
(Rails)
小さく高速に実装サイクルを
回せており成功体験
機能が安定しきっていて
デプロイされない
依存gem更新も滞りがち
課金サブシステムがドワンゴ基盤サービスから脱依存(2023年9月~)
諸事情により課金サブシステムがドワンゴ基盤サービスから脱依存
新バックエンドへの式年遷宮(ビジネスロジックの仕様明確化、移植)は順調
23
internet
本体DB
課金サブシステム(Rails)
ニコニコ漫画
(PHP独自FW)
課金DB
新バックエンド
(Rails)
小さく高速に実装サイクルを
回せており成功体験
機能が安定しきっていて
デプロイされない
依存gem更新も滞りがち
これ、課金サブシステムが
独立してると
逆にリスクが大きいのでは?
保守されないまま動いてしまって忘れ去られる、野生化したシステム
また、インフラ層でも理解容易性を低下させる負債になっていた
課金サブシステムがドワンゴ基盤サービスから脱依存(2023年9月~)
諸事情により課金サブシステムがドワンゴ基盤サービスから脱依存
新バックエンドへの式年遷宮(ビジネスロジックの仕様明確化、移植)は順調
24
internet
本体DB
課金サブシステム(Rails)
ニコニコ漫画
(PHP独自FW)
課金DB
新バックエンド
(Rails)
小さく高速に実装サイクルを
回せており成功体験
機能が安定しきっていて
デプロイされない
依存gem更新も滞りがち
2024年5月
課金サブシステム
吸収合併計画始動
課金サブシステムがドワンゴ基盤サービスから脱依存(2023年9月~)
諸事情により課金サブシステムがドワンゴ基盤サービスから脱依存
新バックエンドへの式年遷宮(ビジネスロジックの仕様明確化、移植)は順調
25
internet
本体DB
課金サブシステム(Rails)
ニコニコ漫画
(PHP独自FW)
課金DB
新バックエンド
(Rails)
小さく高速に実装サイクルを
回せており成功体験
機能が安定しきっていて
デプロイされない
依存gem更新も滞りがち
13エンドポイント
11テーブル
レコード数1億超
おしながき
26
Railsアプリケーションを統廃合する
まず方針
27
Railsアプリケーションを統廃合するための方針
28
Railsアプリケーションを統廃合する手順
29
Railsアプリケーションを統廃合する手順
30
Railsアプリケーションを統廃合する手順
その前に前提知識
31
前提知識
Railsが複数DBを
扱うときのconfigの書き方
32
Railsが複数DBを扱うときの基本的書き方
見慣れた/config/database.ymlとActiveRecord::Baseを継承するクラス
33
production:
<<: *nicomanga_default
host: <%= ENV['DB_HOST'] %>
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
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
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
Railsアプリケーションを統廃合する手順
36
新バックエンドで課金DBを読み書きできるようにする
新バックエンドから
課金DBへの接続を許可する
実はVPCすら別(VPCピアリングしている)だったので
セキュリティグループ指定で許可できなかった
仕方なくサブネットのCIDRで許可した
37
新バックエンドで課金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
新バックエンドで課金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
新バックエンドで課金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
Railsアプリケーションを統廃合する手順
41
新バックエンドに課金サブシステムのモデルを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
新バックエンドに課金サブシステムのモデルを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を付ける
新バックエンドに課金サブシステムのモデルを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付ける差分を混ぜてしまうと
ミスに気付けない、レビューしづらい
新バックエンドに課金サブシステムのモデルを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
新バックエンドに課金サブシステムのモデルを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
新バックエンドに課金サブシステムのモデルを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を付ける
新バックエンドに課金サブシステムのモデルを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
新バックエンドに課金サブシステムのモデルを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
継承元と名前空間を変える
新バックエンドに課金サブシステムのモデルを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
子レコードのクラスへの
関連は一旦コメントアウト
新バックエンドに課金サブシステムのモデルを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
新バックエンドに課金サブシステムのモデルを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
ここで一つ問題を発見
新バックエンドに課金サブシステムのモデルを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)>
新バックエンドに課金サブシステムのモデルを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)>
新バックエンドに課金サブシステムのモデルを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
新バックエンドに課金サブシステムのモデルを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
新バックエンドに課金サブシステムのモデルを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由来
新バックエンドに課金サブシステムのモデルを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
新バックエンドに課金サブシステムのモデルを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
新バックエンドに課金サブシステムのモデルを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
Railsアプリケーションを統廃合する手順
61
新バックエンドに課金サブシステムのコントローラーを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
新バックエンドに課金サブシステムのコントローラーを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を付ける
新バックエンドに課金サブシステムのコントローラーを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
新バックエンドに課金サブシステムのコントローラーを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を掘る
新バックエンドに課金サブシステムのコントローラーを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
新バックエンドに課金サブシステムのコントローラーを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
コントローラーの
基底クラスも
移植専用に用意
新バックエンドに課金サブシステムのコントローラーを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するのを確認
新バックエンドに課金サブシステムのコントローラーを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するのを確認
新バックエンドに課金サブシステムのコントローラーを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するのを確認
読み込み系は問題なく完了
新バックエンドに課金サブシステムのコントローラーをprefix付きで移植する
しかし
書き込み系でテストがfailする
71
新バックエンドに課金サブシステムのコントローラーを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
新バックエンドに課金サブシステムのコントローラーを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
新バックエンドに課金サブシステムのコントローラーを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
新バックエンドに課金サブシステムのコントローラーを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
を使っている
新バックエンドに課金サブシステムのコントローラーを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をロールバックできていなかった
新バックエンドに課金サブシステムのコントローラーを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
新バックエンドに課金サブシステムのコントローラーを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
書き込み系も完了
Railsアプリケーションを統廃合する手順
79
バッチを移植する
移植した
コントローラーの移植とほぼ同じなので省略
80
Railsアプリケーションを統廃合する手順
81
本体から課金サブシステムへのリクエストを新バックエンドの互換APIに呼び変える
82
Railsアプリケーションを統廃合する手順
83
課金サブシステムのアプリケーションサーバを除却する
除却した
アプリケーションサーバは
ECS Fargateなのでタスクを0個にするだけ
ELBなどもterraformで記述していたのでサクッと
84
Railsアプリケーションを統廃合する手順
85
本体DBに課金DBのスキーマを丸ごとコピーする
コピーした
新バックエンドは現行PHPとDBを共有しているため
Railsのマイグレーション機能ではなくridgepoleを使っており、
課金DBからスキーマをダンプして本体DBにコピペしただけ
86
Railsアプリケーションを統廃合する手順
87
課金サブシステムのモデルの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
課金サブシステムのモデルの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に
課金サブシステムのモデルの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指定は不要になる
課金サブシステムのモデルの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指定が不要になる
Railsアプリケーションを統廃合する手順
92
新バックエンドの課金サブシステム互換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
新バックエンドの課金サブシステム互換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
新バックエンドの課金サブシステム互換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する
新バックエンドの課金サブシステム互換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文は発行されない)
新バックエンドの課金サブシステム互換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
新バックエンドの課金サブシステム互換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にも
トランザクションを開く
新バックエンドの課金サブシステム互換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する
新バックエンドの課金サブシステム互換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で複製する
新バックエンドの課金サブシステム互換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
# 以降略
関連レコードを全て複製する分
レスポンスタイムは伸びるが
分間リクエスト数から考えて
影響は軽微と判断
新バックエンドの課金サブシステム互換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
# 元々あったテストケース群
# (中略)
# 右に続く
新バックエンドの課金サブシステム互換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
# 元々あったテストケース群
# (中略)
# 右に続く
新バックエンドの課金サブシステム互換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を切るときに消すのが簡単
新バックエンドの課金サブシステム互換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では
上手くいかないケース
新バックエンドの課金サブシステム互換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
新バックエンドの課金サブシステム互換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
新バックエンドの課金サブシステム互換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
新バックエンドの課金サブシステム互換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
新バックエンドの課金サブシステム互換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
新バックエンドの課金サブシステム互換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しない
Railsアプリケーションを統廃合する手順
112
課金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
課金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
課金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を含めないと自動採番されてしまう
課金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を指定してタイムスタンプ
更新しないのを明示
課金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
課金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週間以上になったので断念
Railsアプリケーションを統廃合する手順
119
課金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
課金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するはず
課金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を実装
Railsアプリケーションを統廃合する手順
123
分析系ダッシュボードのデータソースを切り替える
124
分析系ダッシュボードのデータソースを切り替える
125
任意時刻にexportを実行できるように
サービス分析課に改修依頼
分析系ダッシュボードのデータソースを切り替える
126
サービス分析課側での改修は完了
分析系ダッシュボードのデータソースを切り替える
127
スケジュールクエリ等の
差し替えはこれから
イマココ
Railsアプリケーションを統廃合する手順
128
両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
両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
両DBへの同時書き込みを切り、課金DBを除却する
あとは単に課金DBを
除却すればいいはず
131
Railsアプリケーションを統廃合
できた!
(できるはず)
132
まとめ
133