🎉

Apartment gem の仕組みを理解する

に公開

memo

  • Apartmentは本来、動的にdatabaseを生成する仕組みを作れる
    • テナントの管理者がログインしたらそのテナント用のdatabaseをcreateする、ということができる(後述のElevators)
  • "public" tenant

Elevators

  • 「アパートメント」の中にある「エレベーター」を使って、それぞれの「住居(テナント)」に行く(作る)ことを指してる?
    • のことを「ルーティング」とも言ってそう
  • 「エレベーター」には、様々な行き方があるので、色々な「Elevator」をサポートしてる
  • Elevator を移住者(リクエスト)毎に変える場合は、Rackミドルウェア(順番に気を付けて)使ってね

Config

抜粋

  • config.use_schemas = false
    • use を発行してスキーマを切り替えない
  • config.append_environment = false
    • テナント名の末尾に環境名を付与しない
  • config.tenant_presence_check = false
    • テナントを切り替えるたびに、スキーマの存在チェックをしない
  • config.prepend_environment = false
    • テナント名の先頭に環境名を付与しない
  • config.db_migrate_tenants = false
    • db:migrate による全てのテナントのマイグレーションが無効
  • config.database_schema_file = nil
    • db/schema.rb 以外を使う時に指定する
  • config.seed_after_create = false
    • テナント作成したあとに、seedは入れない

Managing Migrations

  • config.tenant_names
    • 全テナントのdatabase名を設定
    • ここで設定されたdatabaseが、マイグレーション実行される
      • 内部的には Apartment::Tenant.migrate(#{tenant_name}) が呼ばれるだけ

ローカルで試す

  • テナントDBを3つ作成(下記のテナント名変えて3回実行しただけ)
    • まだ schema.rb 作成してないので、無いよーって言われて、マイグレーションもされない、DB作られるだけ
    • schema.rb があると、CREATEと同時にマイグレーションもされる(設定次第ではある)
irb(main):001:0> Apartment::Tenant.create('blog_tenant_1_development')
    (8.2ms)  CREATE DATABASE `blog_tenant_1_development` DEFAULT CHARACTER SET `utf8mb4`
    (0.9ms)  use `blog_tenant_1_development`
    (0.6ms)  use `blog_development`
 /Users/kazumeat/opt/rails_sandbox/blog/vendor/bundle/ruby/3.0.0/gems/ros-apartment-2.11.0/lib/apartment/adapters/abstract_adapter.rb:219:in `load_or_raise': /Users/kazumeat/opt/rails_sandbox/blog/db/schema.rb doesn't exist yet (Apartment::FileNotFound)
  • switch 試した
 irb(main):001:0> Apartment::Tenant.switch('blog_tenant_3') { p 'hoge' }
    (0.8ms)  use `blog_tenant_3`
 "hoge"
    (0.6ms)  use `blog_development`
 => "hoge"
  • マイグレートの準備
config/initializers/apartment.rb
  config.tenant_names = lambda do
    (1..3).to_a.map do |i|
      "blog_tenant_#{i}_development"
    end
  end
  • モデルの生成(schemaファイル作成される)
bin/rails generate model Article title:string body:text
  • マイグレートする
 bundle exec rake db:migrate
 == 20231220142129 CreateArticles: migrating ===================================
 -- create_table(:articles)
    -> 0.0722s
 == 20231220142129 CreateArticles: migrated (0.0722s) ==========================
 
 Migrating blog_tenant_1_development tenant
 == 20231220142129 CreateArticles: migrating ===================================
 -- create_table(:articles)
    -> 0.0205s
 == 20231220142129 CreateArticles: migrated (0.0205s) ==========================
 
 Migrating blog_tenant_2_development tenant
 == 20231220142129 CreateArticles: migrating ===================================
 -- create_table(:articles)
    -> 0.0210s
 == 20231220142129 CreateArticles: migrated (0.0211s) ==========================
 
 Migrating blog_tenant_3_development tenant
 == 20231220142129 CreateArticles: migrating ===================================
 -- create_table(:articles)
    -> 0.0229s
 == 20231220142129 CreateArticles: migrated (0.0229s) ==========================

public db もマイグレートされた!(ActiveRecord の task を実行してるんだから、そりゃそうか・・となった)

なんで、rake db:migrate すると、テナントのDBもマイグレーションされるの?

config の設定値みて、rake db:migrate したら apartment:migrate も実行されるようになってる
CHANGELOG にも書いてあった

vendor/bundle/ruby/3.0.0/gems/ros-apartment-2.11.0/lib/apartment/railtie.rb
    #
    #   Ensure rake tasks are loaded
    #
    rake_tasks do
      load 'tasks/apartment.rake'
      require 'apartment/tasks/enhancements' if Apartment.db_migrate_tenants
    end

タスクはこれで

vendor/bundle/ruby/3.0.0/gems/ros-apartment-2.11.0/lib/tasks/apartment.rake
  desc 'Migrate all tenants'
  task :migrate do
    Apartment::TaskHelper.warn_if_tenants_empty  # <-- ★ 直接 rake apartment:migrate することは deprecated で Warning出る
    Apartment::TaskHelper.each_tenant do |tenant|
      Apartment::TaskHelper.migrate_tenant(tenant)
    end
  end

実際の処理はこっち

vendor/bundle/ruby/3.0.0/gems/ros-apartment-2.11.0/lib/apartment/tasks/task_helper.rb
    def self.migrate_tenant(tenant_name)
      strategy = Apartment.db_migrate_tenant_missing_strategy
      create_tenant(tenant_name) if strategy == :create_tenant

      puts("Migrating #{tenant_name} tenant")
      Apartment::Migrator.migrate tenant_name
    rescue Apartment::TenantNotFound => e
      raise e if strategy == :raise_exception

      puts e.message
    end

Railtieの仕組みは、既存の拡張できるんだろうな〜くらいの理解しかないので、これはまた別途

Discussion