sql >> Databasteknik >  >> RDS >> Mysql

Växla mellan flera databaser i Rails utan att bryta transaktioner

Detta är ett knepigt problem på grund av tät koppling inuti ActiveRecord , men jag har lyckats skapa några proof of concept som fungerar. Eller åtminstone ser det ut som att det fungerar.

Lite bakgrund

ActiveRecord använder en ActiveRecord::ConnectionAdapters::ConnectionHandler klass som ansvarar för lagring av anslutningspooler per modell. Som standard finns det bara en anslutningspool för alla modeller, eftersom den vanliga Rails-appen är ansluten till en databas.

Efter att ha kört establish_connection för olika databas i en viss modell skapas en ny anslutningspool för den modellen. Och även för alla modeller som kan ärva från det.

Innan du utför någon fråga, ActiveRecord hämtar först anslutningspoolen för relevant modell och hämtar sedan anslutningen från poolen.

Observera att förklaringen ovan kanske inte är 100 % korrekt, men den bör vara nära.

Lösning

Så tanken är att ersätta standardanslutningshanteraren med en anpassad som kommer att returnera anslutningspoolen baserat på den angivna fragmentbeskrivningen.

Detta kan implementeras på många olika sätt. Jag gjorde det genom att skapa proxyobjektet som skickar shardnamn som förklädda ActiveRecord klasser. Anslutningshanteraren förväntar sig att få AR-modell och tittar på name egenskap och även i superclass att gå modellhierarkikedjan. Jag har implementerat DatabaseModel klass som i grunden är ett skärvnamn, men den beter sig som AR-modell.

Implementering

Här är exempel på implementering. Jag har använt sqlite databas för enkelhetens skull, du kan bara köra den här filen utan någon installation. Du kan också ta en titt på denna sammanfattning

# 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

Jag tror att detta borde ge en idé om hur man implementerar en produktionsklar lösning. Jag hoppas att jag inte har missat något uppenbart här. Jag kan föreslå ett par olika tillvägagångssätt:

  1. Underklass ActiveRecord::ConnectionAdapters::ConnectionHandler och skriv över de metoder som är ansvariga för att hämta anslutningspooler
  2. Skapa en helt ny klass som implementerar samma API som ConnectionHandler
  3. Jag antar att det också är möjligt att bara skriva över retrieve_connection metod. Jag kommer inte ihåg var den är definierad, men jag tror att den finns i ActiveRecord::Core .

Jag tror att tillvägagångssätt 1 och 2 är vägen att gå och bör täcka alla fall när man arbetar med databaser.




  1. UTF-8 med mysql och php i freebsd svenska chars (åäö)

  2. Förstå skillnaden mellan EXCEPT och NOT IN-operatörer

  3. GROUP BY-beteende när inga aggregerade funktioner finns i SELECT-satsen

  4. Hur undkommer man strängar i PDO?