Mysql
 sql >> Datenbank >  >> RDS >> Mysql

Wechseln zwischen mehreren Datenbanken in Rails, ohne Transaktionen zu unterbrechen

Dies ist aufgrund der engen Kopplung innerhalb von ActiveRecord ein kniffliges Problem , aber ich habe es geschafft, einen Proof of Concept zu erstellen, der funktioniert. Oder zumindest sieht es so aus, als ob es funktioniert.

Einige Hintergrundinformationen

ActiveRecord verwendet einen ActiveRecord::ConnectionAdapters::ConnectionHandler Klasse, die für das Speichern von Verbindungspools pro Modell verantwortlich ist. Standardmäßig gibt es nur einen Verbindungspool für alle Modelle, da die gewöhnliche Rails-App mit einer Datenbank verbunden ist.

Nach dem Ausführen von establish_connection Für verschiedene Datenbanken in bestimmten Modellen wird ein neuer Verbindungspool für dieses Modell erstellt. Und auch für alle Modelle, die davon erben können.

Bevor Sie eine Abfrage ausführen, ActiveRecord ruft zuerst den Verbindungspool für das relevante Modell ab und ruft dann die Verbindung aus dem Pool ab.

Beachten Sie, dass die obige Erklärung möglicherweise nicht 100 % genau ist, aber nahe dran sein sollte.

Lösung

Die Idee ist also, den standardmäßigen Verbindungshandler durch einen benutzerdefinierten zu ersetzen, der den Verbindungspool basierend auf der bereitgestellten Shard-Beschreibung zurückgibt.

Dies kann auf viele verschiedene Arten implementiert werden. Ich habe es geschafft, indem ich das Proxy-Objekt erstellt habe, das Shard-Namen als getarntes ActiveRecord weitergibt Klassen. Der Verbindungshandler erwartet, das AR-Modell zu erhalten, und sieht sich name an -Eigenschaft und auch bei superclass die Hierarchiekette des Modells zu durchlaufen. Ich habe DatabaseModel implementiert Klasse, die im Grunde ein Shard-Name ist, sich aber wie ein AR-Modell verhält.

Implementierung

Hier ist eine Beispielimplementierung. Ich habe der Einfachheit halber eine SQLite-Datenbank verwendet. Sie können diese Datei einfach ohne Einrichtung ausführen. Sie können sich auch diese Zusammenfassung ansehen

# 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

Ich denke, dies sollte eine Vorstellung davon geben, wie eine produktionsreife Lösung implementiert werden kann. Ich hoffe, ich habe hier nichts Offensichtliches übersehen. Ich kann einige verschiedene Ansätze vorschlagen:

  1. Unterklasse ActiveRecord::ConnectionAdapters::ConnectionHandler und überschreiben Sie die Methoden, die für das Abrufen von Verbindungspools verantwortlich sind
  2. Erstellen Sie eine komplett neue Klasse, die dieselbe API wie ConnectionHandler implementiert
  3. Ich denke, es ist auch möglich, retrieve_connection einfach zu überschreiben Methode. Ich weiß nicht mehr, wo es definiert ist, aber ich glaube, es ist in ActiveRecord::Core .

Ich denke, die Ansätze 1 und 2 sind der richtige Weg und sollten alle Fälle bei der Arbeit mit Datenbanken abdecken.