Mysql
 sql >> Baza danych >  >> RDS >> Mysql

Przełączanie się między wieloma bazami danych w Railsach bez przerywania transakcji

Jest to trudny problem z powodu ścisłego sprzężenia wewnątrz ActiveRecord , ale udało mi się stworzyć jakiś dowód koncepcji, który działa. A przynajmniej wygląda na to, że działa.

Niektóre tło

ActiveRecord używa ActiveRecord::ConnectionAdapters::ConnectionHandler klasa, która jest odpowiedzialna za przechowywanie pul połączeń na model. Domyślnie jest tylko jedna pula połączeń dla wszystkich modeli, ponieważ zwykła aplikacja Railsowa jest połączona z jedną bazą danych.

Po wykonaniu establish_connection dla innej bazy danych w danym modelu tworzona jest nowa pula połączeń dla tego modelu. A także dla wszystkich modeli, które mogą po nim odziedziczyć.

Przed wykonaniem zapytania ActiveRecord najpierw pobiera pulę połączeń dla odpowiedniego modelu, a następnie pobiera połączenie z puli.

Pamiętaj, że powyższe wyjaśnienie może nie być w 100% dokładne, ale powinno być zbliżone.

Rozwiązanie

Pomysł polega więc na zastąpieniu domyślnego programu obsługi połączeń niestandardowym, który zwróci pulę połączeń na podstawie dostarczonego opisu fragmentu.

Można to zrealizować na wiele różnych sposobów. Zrobiłem to, tworząc obiekt proxy, który przekazuje nazwy fragmentów jako zamaskowane ActiveRecord zajęcia. Program obsługi połączenia spodziewa się uzyskać model AR i patrzy na name właściwość, a także w superclass chodzić po hierarchicznym łańcuchu modelu. Wdrożyłem DatabaseModel klasa, która jest zasadniczo nazwą fragmentu, ale zachowuje się jak model AR.

Wdrożenie

Oto przykładowa implementacja. Użyłem bazy danych sqlite dla uproszczenia, możesz po prostu uruchomić ten plik bez żadnej konfiguracji. Możesz również spojrzeć na ten opis

# 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

Myślę, że to powinno dać pomysł, jak wdrożyć gotowe rozwiązanie produkcyjne. Mam nadzieję, że nie przeoczyłam tu niczego oczywistego. Mogę zaproponować kilka różnych podejść:

  1. Podklasa ActiveRecord::ConnectionAdapters::ConnectionHandler i nadpisz metody odpowiedzialne za pobieranie pul połączeń
  2. Stwórz zupełnie nową klasę implementującą to samo API co ConnectionHandler
  3. Wydaje mi się, że można też po prostu nadpisać retrieve_connection metoda. Nie pamiętam, gdzie jest zdefiniowany, ale myślę, że jest w ActiveRecord::Core .

Myślę, że podejścia 1 i 2 są dobrym rozwiązaniem i powinny obejmować wszystkie przypadki podczas pracy z bazami danych.




  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Symulacja polecenia ORDER BY FIELD() MySQL w Postgresql

  2. PHP i MySQL — jak wyświetlić wybraną wartość w menu rozwijanym

  3. mysql Stored Procedure, Zapytanie, aby sprawdzić, czy istnieje, czy nie

  4. Niedozwolona mieszanka zestawień (utf8mb4_unicode_ci,IMPLICIT) i (utf8mb4_general_ci,IMPLICIT) dla operacji '='

  5. Wiele relacji jeden do wielu w GORM