Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เชี่ยวชาญ Cross-Cluster Associations ใน Rails: แก้ปัญหา Database Partitioning

ทำไม Database ถึงอยู่ใน Cluster ต่างกัน

Rails associations ทำงานโปร่งใสตลอดที่ข้อมูลอยู่ใน database เดียว แต่เมื่อข้อมูลกระจายข้าม multiple database clusters ความโปร่งใสจะพัง

JOIN ต้องการให้ทั้งสองตารางอยู่ใน database server เดียวกัน พยายาม JOIN ข้าม cluster จะได้ error:

ActiveRecord::StatementInvalid (Table 'people_cluster.humans' doesn't exist)

นี่ไม่ใช่ configuration mistake แต่เป็น physical constraint: database servers ไม่สามารถ JOIN ตารางที่ไม่ได้ host เอง ปัญหาเกิดกับ has_many :through และ has_one :through เพราะ generate intermediate JOIN queries ส่วน has_many หรือ belongs_to โดยตรง ไม่ต้อง join จึงทำงานข้าม cluster ได้

ตัวอย่าง: User ใน database accounts, Post ใน content -> User has_many :posts OK แต่เพิ่ม Subscription ใน billing และ User has_many :posts, through: :subscriptions -> Rails พยายาม join subscriptions และ posts ใน query เดียว -> cluster boundary problem

Three-tier Database Configuration

Rails ใช้โครงสร้าง 3 ระดับใน config/database.yml:

development:
  primary: <<: *default
    database: myapp_primary_dev
  accounts: <<: *default
    database: myapp_accounts_dev
    migrations_paths: db/accounts_migrate
  content: <<: *default
    database: myapp_content_dev
    migrations_paths: db/content_migrate

migrations_paths จำเป็นสำหรับ generators และ db:migrate route migrations ไป directory ที่ถูกต้อง

แต่ละ secondary database ต้องมี abstract record class ใช้ --database flag:

rails generate model Subscription plan:string --database accounts

สร้าง AccountsRecord class และ Subscription inherit จากมัน

Abstract Record Classes และ Connection Routing

Abstract record classes = mechanism ที่ Rails ใช้ route queries ไป cluster ที่ถูกต้อง ใช้ connects_to declare database mapping

# application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  connects_to database: { writing: :primary, reading: :primary }
end

# accounts_record.rb
class AccountsRecord < ApplicationRecord
  self.abstract_class = true
  connects_to database: { writing: :accounts, reading: :accounts }
end

# content_record.rb
class ContentRecord < ApplicationRecord
  self.abstract_class = true
  connects_to database: { writing: :content, reading: :content }
end

User model อยู่ใน accounts cluster inherit AccountsRecord Models ใน content cluster inherit ContentRecord อื่นๆ inherit ApplicationRecord -> primary database

Active Record หา connection pool โดยเดินขึ้น class hierarchy จนเจอ class ที่เรียก connects_to

Common mistake: ใช้ establish_connection บน model ตัวๆ แทน abstract classes -> สร้าง connection pools แยกกันเยอะ(Abstract classes แชร์ pool เดียว)

Cross-Cluster Associations ทำงานอย่างไร

disable_joins: true = mechanism ตรงๆ สำหรับทำให้ through associations ข้าม cluster ได้ Rails ทิ้ง single JOIN query strategy กลายเป็น 2+ sequential SELECT statements pipe IDs จาก query แรกเข้า WHERE ... IN (...) ของ query ที่สอง

Model Setup ตัวอย่าง (3 clusters)

# user.rb - accounts database
class User < AccountsRecord
  has_many :subscriptions
  has_many :posts, through: :subscriptions, disable_joins: true
end

# subscription.rb - accounts database
class Subscription < AccountsRecord
  belongs_to :user
  has_many :posts
end

# post.rb - content database
class Post < ContentRecord
  belongs_to :subscription
end

เรียก user.posts -> Rails สร้าง 2 queries แทน JOIN:

-- Query 1 (accounts cluster)
SELECT "subscriptions"."id" FROM "subscriptions" WHERE "subscriptions"."user_id" = 1

-- Query 2 (content cluster)
SELECT "posts".* FROM "posts" WHERE "posts"."subscription_id" IN (4, 7, 12)

Query แรกเก็บ primary keys จาก subscriptions ส่งเข้า IN clause ของ query ที่สอง ไม่มี cross-cluster join Rails ประกอบ result set ใน application memory

disable_joins ทำงานเหมือนกันกับ has_one :through

เมื่อไหร่ต้องตั้ง disable_joins ให้ชัดเจน

Rails ไม่ auto-detect cluster boundaries Association loading เป็น lazy SQL กำหนดตอน define association ไม่ใช่ตอน trigger ทุก through association ที่ข้าม cluster ต้องมี disable_joins: true

Audit: หา through association ที่ source และ target model inherit จาก abstract record classes ต่างกัน

Eager Loading ข้าม Clusters

disable_joins ส่งผลต่อ association loading แต่ไม่เปลี่ยน eager loading strategies

  • eager_load: ใช้ LEFT OUTER JOIN -> fail ข้าม cluster
  • preload: ใช้ separate queries ประกอบใน Ruby -> ถูกต้อง สำหรับ cross-cluster
  • includes: delegate ไป preload โดย default (ถ้าไม่มี where บน associated table) -> works แต่ถ้ามี where บน associated table -> switch ไป eager_load -> fail

ถ้าลังเล ใช้ preload โดยตรง

Scoped Associations และ Cross-Cluster Filtering

Scope บน has_many ข้าม cluster -> scope SQL รันบน target database ไม่ใช่ source

has_many :published_posts, -> { where(published: true) },
  through: :subscriptions, source: :posts, class_name: "Post", disable_joins: true

where(published: true) ต่อท้าย query ที่ 2 (content database) - ถูกต้อง

ไม่ได้: reference columns จาก intermediate table ใน scope (intermediate query จบแล้ว)

แก้: filter intermediate records โดยเพิ่ม scope บน intermediate association แทน

Horizontal Sharding และ Cross-Shard Associations

Split logical database ตาม partition key (tenant_id) เพิ่มมิติที่ 2 ของ cross-cluster problem disable_joins ยังใช้ได้ แต่ connection routing ซับซ้อนขึ้น

Rails มี connected_to สำหรับ switch shards:

ActiveRecord::Base.connected_to(role: :writing, shard: :shard_one) do
  User.find(1) # Hits shard_one
end

Rails 8 เพิ่ม introspection methods: shard_keys, sharded?, connected_to_all_shards

Testing Cross-Cluster Associations

Test environment ต้อง mirror production database topology database.yml ต้องมี test environment block สำหรับแต่ละ database

Fixtures/factories ต้อง target database ที่ถูกต้อง Rails wrap test ใน transaction per-connection multiple databases = multiple transactions cleanup อิสระต่อ database

Verify query count ด้วย sql.active_record notification - has_many :through + disable_joins บน single record ควรเป็น 2 queries พอดี

Caveats

disable_joins แก้ association loading แต่ไม่ขยายไป query chaining ไม่ chain .where, .order, .group ที่ reference columns ข้าม cluster บน single relation ได้

แก้: decompose manually - fetch IDs จาก cluster หนึ่ง ใช้เป็น input query cluster อีกตัว ทำ cluster boundaries มองเห็นในโค้ด