sql >> Database teknologi >  >> RDS >> Mysql

Skift mellem flere databaser i Rails uden at bryde transaktioner

Dette er et vanskeligt problem på grund af tæt kobling inde i ActiveRecord , men jeg har formået at skabe noget proof of concept, der virker. Eller i det mindste ser det ud til, at det virker.

Noget baggrund

ActiveRecord bruger en ActiveRecord::ConnectionAdapters::ConnectionHandler klasse, der er ansvarlig for at opbevare forbindelsespuljer pr. model. Som standard er der kun én forbindelsespulje for alle modeller, fordi den sædvanlige Rails-app er forbundet til én database.

Efter at have udført establish_connection for forskellige databaser i en bestemt model, oprettes en ny forbindelsespulje for den model. Og også for alle modeller, der måtte arve fra det.

Før du udfører en forespørgsel, ActiveRecord henter først forbindelsespuljen til relevant model og henter derefter forbindelsen fra poolen.

Bemærk, at ovenstående forklaring måske ikke er 100 % nøjagtig, men den bør være tæt på.

Løsning

Så ideen er at erstatte standardforbindelseshandleren med en brugerdefineret, der returnerer forbindelsespuljen baseret på den angivne shard-beskrivelse.

Dette kan implementeres på mange forskellige måder. Jeg gjorde det ved at oprette proxy-objektet, der sender shard-navne som forklædte ActiveRecord klasser. Forbindelseshandler forventer at få AR-model og ser på name ejendom og også på superclass at gå i modelhierarkikæden. Jeg har implementeret DatabaseModel klasse, der grundlæggende er et shard navn, men den opfører sig som AR-model.

Implementering

Her er et eksempel på implementering. Jeg har brugt sqlite database for enkelhedens skyld, du kan bare køre denne fil uden nogen opsætning. Du kan også tage et kig på denne oversigt

# Define some required dependencies
require "bundler/inline"
gemfile(false) do
  source "https://rubygems.org"
  gem "activerecord", "~> 4.2.8"
  gem "sqlite3"
end

require "active_record"

class User < ActiveRecord::Base
end

DatabaseModel = Struct.new(:name) do
  def superclass
    ActiveRecord::Base
  end
end

# Setup database connections and create databases if not present
connection_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
resolver = ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.new({
  "users_shard_1" => { adapter: "sqlite3", database: "users_shard_1.sqlite3" },
  "users_shard_2" => { adapter: "sqlite3", database: "users_shard_2.sqlite3" }
})

databases = %w{users_shard_1 users_shard_2}
databases.each do |database|
  filename = "#{database}.sqlite3"

  ActiveRecord::Base.establish_connection({
    adapter: "sqlite3",
    database: filename
  })

  spec = resolver.spec(database.to_sym)
  connection_handler.establish_connection(DatabaseModel.new(database), spec)

  next if File.exists?(filename)

  ActiveRecord::Schema.define(version: 1) do
    create_table :users do |t|
      t.string :name
      t.string :email
    end
  end
end

# Create custom connection handler
class ShardHandler
  def initialize(original_handler)
    @original_handler = original_handler
  end

  def use_database(name)
    @model= DatabaseModel.new(name)
  end

  def retrieve_connection_pool(klass)
    @original_handler.retrieve_connection_pool(@model)
  end

  def retrieve_connection(klass)
    pool = retrieve_connection_pool(klass)
    raise ConnectionNotEstablished, "No connection pool for #{klass}" unless pool
    conn = pool.connection
    raise ConnectionNotEstablished, "No connection for #{klass} in connection pool" unless conn
    puts "Using database \"#{conn.instance_variable_get("@config")[:database]}\" (##{conn.object_id})"
    conn
  end
end

User.connection_handler = ShardHandler.new(connection_handler)

User.connection_handler.use_database("users_shard_1")
User.create(name: "John Doe", email: "[email protected]")
puts User.count

User.connection_handler.use_database("users_shard_2")
User.create(name: "Jane Doe", email: "[email protected]")
puts User.count

User.connection_handler.use_database("users_shard_1")
puts User.count

Jeg tror, ​​at dette burde give en idé om, hvordan man implementerer en produktionsklar løsning. Jeg håber ikke, jeg gik glip af noget åbenlyst her. Jeg kan foreslå et par forskellige tilgange:

  1. Underklasse ActiveRecord::ConnectionAdapters::ConnectionHandler og overskriv de metoder, der er ansvarlige for at hente forbindelsespuljer
  2. Opret en helt ny klasse, der implementerer det samme API som ConnectionHandler
  3. Det er vel også muligt bare at overskrive retrieve_connection metode. Jeg kan ikke huske, hvor det er defineret, men jeg tror, ​​det er i ActiveRecord::Core .

Jeg tror, ​​tilgang 1 og 2 er vejen at gå og bør dække alle tilfælde, når man arbejder med databaser.




  1. Opret et skalerbart databaseskema til lagring af golfresultater

  2. PHP + MySQL-kø

  3. Korrekt måde at rense input i MySQL ved hjælp af PDO

  4. Sådan gemmer du et Java Instant i en MySQL-database