sql >> Database >  >> RDS >> Mysql

Schakelen tussen meerdere databases in Rails zonder transacties te verbreken

Dit is een lastig probleem, vanwege de strakke koppeling binnen ActiveRecord , maar ik ben erin geslaagd om een ​​proof of concept te maken dat werkt. Of in ieder geval lijkt het alsof het werkt.

Wat achtergrond

ActiveRecord gebruikt een ActiveRecord::ConnectionAdapters::ConnectionHandler klasse die verantwoordelijk is voor het opslaan van verbindingspools per model. Standaard is er slechts één verbindingspool voor alle modellen, omdat de gebruikelijke Rails-app is verbonden met één database.

Na het uitvoeren van establish_connection voor een andere database in een bepaald model wordt een nieuwe verbindingspool voor dat model gemaakt. En ook voor alle modellen die er van kunnen erven.

Voordat u een zoekopdracht uitvoert, ActiveRecord haalt eerst de verbindingspool op voor het relevante model en haalt vervolgens de verbinding op uit de pool.

Merk op dat bovenstaande uitleg misschien niet 100% nauwkeurig is, maar het zou in de buurt moeten komen.

Oplossing

Het idee is dus om de standaardverbindingshandler te vervangen door een aangepaste die de verbindingspool retourneert op basis van de opgegeven shardbeschrijving.

Dit kan op veel verschillende manieren worden geïmplementeerd. Ik deed het door het proxy-object te maken dat shard-namen doorgeeft als vermomd ActiveRecord klassen. Verbindingshandler verwacht AR-model te krijgen en kijkt naar name eigendom en ook bij superclass om de hiërarchieketen van het model te doorlopen. Ik heb DatabaseModel geïmplementeerd klasse die in feite een Shard-naam is, maar het gedraagt ​​zich als een AR-model.

Implementatie

Hier is een voorbeeldimplementatie. Ik heb de sqlite-database gebruikt voor de eenvoud, je kunt dit bestand gewoon uitvoeren zonder enige setup. Je kunt ook deze kern bekijken

# 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

Ik denk dat dit een idee zou moeten geven hoe een productieklare oplossing kan worden geïmplementeerd. Ik hoop dat ik hier niets voor de hand liggends heb gemist. Ik kan een aantal verschillende benaderingen voorstellen:

  1. Subklasse ActiveRecord::ConnectionAdapters::ConnectionHandler en overschrijf de methoden die verantwoordelijk zijn voor het ophalen van verbindingspools
  2. Maak een geheel nieuwe klasse met dezelfde api als ConnectionHandler
  3. Ik denk dat het ook mogelijk is om gewoon retrieve_connection te overschrijven methode. Ik weet niet meer waar het is gedefinieerd, maar ik denk dat het in ActiveRecord::Core staat .

Ik denk dat benaderingen 1 en 2 de beste keuze zijn en alle gevallen moeten dekken bij het werken met databases.




  1. Records van de afgelopen 24 uur selecteren in PostgreSQL

  2. mysql tijdsverschil naar uren

  3. mysql-telgroep door te hebben

  4. Is er een manier om meerdere triggers in één script te maken?