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

Passaggio tra più database in Rails senza interrompere le transazioni

Questo è un problema complicato, a causa dell'accoppiamento stretto all'interno di ActiveRecord , ma sono riuscito a creare un proof of concept che funziona. O almeno sembra che funzioni.

Un po' di background

ActiveRecord utilizza un ActiveRecord::ConnectionAdapters::ConnectionHandler classe responsabile della memorizzazione dei pool di connessione per modello. Per impostazione predefinita c'è un solo pool di connessioni per tutti i modelli, perché la solita app Rails è connessa a un database.

Dopo aver eseguito establish_connection per database diversi in un particolare modello, viene creato un nuovo pool di connessioni per quel modello. E anche per tutti i modelli che ne possono ereditare.

Prima di eseguire qualsiasi query, ActiveRecord prima recupera il pool di connessioni per il modello pertinente e quindi recupera la connessione dal pool.

Tieni presente che la spiegazione di cui sopra potrebbe non essere accurata al 100%, ma dovrebbe essere simile.

Soluzione

Quindi l'idea è quella di sostituire il gestore di connessione predefinito con uno personalizzato che restituirà il pool di connessioni in base alla descrizione dello shard fornita.

Questo può essere implementato in molti modi diversi. L'ho fatto creando l'oggetto proxy che sta passando i nomi di shard come ActiveRecord mascherato classi. Il gestore della connessione si aspetta di ottenere il modello AR e guarda name proprietà e anche in superclass per percorrere la catena gerarchica del modello. Ho implementato DatabaseModel classe che è fondamentalmente un nome shard, ma si comporta come un modello AR.

Attuazione

Ecco un esempio di implementazione. Ho usato il database sqlite per semplicità, puoi semplicemente eseguire questo file senza alcuna configurazione. Puoi anche dare un'occhiata a questo succo

# 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

Penso che questo dovrebbe dare un'idea di come implementare una soluzione pronta per la produzione. Spero di non aver perso nulla di ovvio qui. Posso suggerire un paio di approcci diversi:

  1. Sottoclasse ActiveRecord::ConnectionAdapters::ConnectionHandler e sovrascrivi quei metodi responsabili del recupero dei pool di connessione
  2. Crea una classe completamente nuova implementando la stessa API di ConnectionHandler
  3. Immagino che sia anche possibile sovrascrivere semplicemente retrieve_connection metodo. Non ricordo dove sia definito, ma penso che sia in ActiveRecord::Core .

Penso che gli approcci 1 e 2 siano la strada da percorrere e dovrebbero coprire tutti i casi quando si lavora con i database.