Est-il possible de postuler à mon propre projet concernant la prise en charge de plusieurs bases de données implémentées à partir de Rails 6.0? Je suivrai la mise en œuvre pour juger que
https://railsguides.jp/active_record_multiple_databases.html#コネクションの自動切り替えを有効にする
La fonction de commutation automatique permet à l'application de passer du primaire au réplica ou du réplica au primaire en fonction du verbe HTTP et de la présence ou de l'absence de l'écriture la plus récente. Lorsqu'une application reçoit une requête POST, PUT, DELETE ou PATCH, elle écrit automatiquement dans le primaire. L'application lira à partir du primaire jusqu'à ce que le temps spécifié s'écoule après l'écriture. Lorsque l'application reçoit une demande GET ou HEAD, elle lit à partir du réplica s'il n'y a pas d'écriture récente.
Il est précisé qu'il est jugé par deux types de jugement par méthode HTTP + jugement par temps d'écriture La méthode de réglage est décrite ci-dessous
config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
Commençons par la partie application du middleware
activerecord-6.0.3.2/lib/active_record/railtie.rb
  class Railtie < Rails::Railtie # :nodoc:
    config.active_record = ActiveSupport::OrderedOptions.new
    initializer "active_record.database_selector" do
      if options = config.active_record.delete(:database_selector)
        resolver = config.active_record.delete(:database_resolver)
        operations = config.active_record.delete(:database_resolver_context)
        config.app_middleware.use ActiveRecord::Middleware::DatabaseSelector, resolver, operations, options
      end
    end
--Exécuté si database_selector est défini
--database_resolver et database_resolver_context sont maintenant juste passés
Ensuite, nous suivrons l'implémentation de la partie middleware
https://api.rubyonrails.org/classes/ActiveSupport/OrderedOptions.html
Un mécanisme qui permet d'accéder à Hash par points Renvoie nil s'il n'est pas défini avec delete
Ruby2.6.5 pry(main)> x = ActiveSupport::OrderedOptions.new
=> {}
Ruby2.6.5 pry(main)> x.delete(:hoge)
=> nil
activerecord-6.0.3.2/lib/active_record/middleware/database_selector.rb
module ActiveRecord
  module Middleware
    class DatabaseSelector
      def initialize(app, resolver_klass = nil, context_klass = nil, options = {})
        @app = app
        @resolver_klass = resolver_klass || Resolver
        @context_klass = context_klass || Resolver::Session
        @options = options
      end
      attr_reader :resolver_klass, :context_klass, :options
      def call(env)
        request = ActionDispatch::Request.new(env)
        select_database(request) do
          @app.call(env)
        end
      end
--Si database_resolver n'est pas défini, DatabaseSelector :: Resolver est défini.
--Si database_resolver_context n'est pas défini, DatabaseSelector :: Resolver :: Session est défini.
Ensuite, jetons un œil à l'important # select_database
DatabaseSelector#select_database
module ActiveRecord
  module Middleware
    class DatabaseSelector
      private
        def select_database(request, &blk)
          context = context_klass.call(request)
          resolver = resolver_klass.call(context, options)
          if reading_request?(request)
            resolver.read(&blk)
          else
            resolver.write(&blk)
          end
        end
        def reading_request?(request)
          request.get? || request.head?
        end
    end
  end
end
Puisque «context» est exécuté en premier et «résolveur» est créé sur cette base, vérifions d'abord «context».
Resolver::Session.call
class Resolver # :nodoc:
  class Session # :nodoc:
    def self.call(request)
      new(request.session)
    end
    def initialize(session)
      @session = session
    end
    attr_reader :session
.call est juste # new, ici seule la session de ʻActionDispatch :: Requestest définie  Alors vérifiez ensuite le.call` du résolveur
Resolver.call
class Resolver # :nodoc:
  SEND_TO_REPLICA_DELAY = 2.seconds
  def self.call(context, options = {})
    new(context, options)
  end
  def initialize(context, options = {})
    @context = context
    @options = options
    @delay = @options && @options[:delay] ? @options[:delay] : SEND_TO_REPLICA_DELAY
    @instrumenter = ActiveSupport::Notifications.instrumenter
  end
  attr_reader :context, :delay, :instrumenter
Je suis juste nouveau ici aussi, donc je vais lire un peu plus l'original DatabaseSelector # select_database.
def select_database(request, &blk)
  context = context_klass.call(request)
  resolver = resolver_klass.call(context, options)
  if reading_request?(request)
    resolver.read(&blk)
  else
    resolver.write(&blk)
  end
end
def reading_request?(request)
  request.get? || request.head?
end
Ici pour GET ou HEAD => lire Sinon => écrire Exécute le traitement du résolveur en tant que (le résolveur semble faire d'autres jugements)
Commençons par lire
Resolver#read
class Resolver # :nodoc:
  def read(&blk)
    if read_from_primary?
      read_from_primary(&blk)
    else
      read_from_replica(&blk)
    end
  end
  private
    def read_from_primary(&blk)
      ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role, prevent_writes: true) do
        instrumenter.instrument("database_selector.active_record.read_from_primary") do
          yield
        end
      end
    end
    def read_from_replica(&blk)
      ActiveRecord::Base.connected_to(role: ActiveRecord::Base.reading_role, prevent_writes: true) do
        instrumenter.instrument("database_selector.active_record.read_from_replica") do
          yield
        end
      end
    end
Il semble que le processus se déroule après avoir notifié par instrument le rôle à utiliser. Alors, qu'est-ce qui est jugé par «# read_from_primary?»?
  private
    def read_from_primary?
      !time_since_last_write_ok?
    end
    def send_to_replica_delay
      delay
    end
    def time_since_last_write_ok?
      Time.now - context.last_write_timestamp >= send_to_replica_delay
    end
context La soi-disant session # last_write_timestamp est
Si le délai du premier config.active_record.database_selector = {delay: 2.seconds} n'est pas passé, définissez-le sur primaire.
Si le délai est passé, la réplique sera visualisée.
Vérifiez donc le # last_write_timestamp de la session
class Session # :nodoc:
  # Converts milliseconds since epoch timestamp into a time object.
  def self.convert_timestamp_to_time(timestamp)
    timestamp ? Time.at(timestamp / 1000, (timestamp % 1000) * 1000) : Time.at(0)
  end
  def last_write_timestamp
    self.class.convert_timestamp_to_time(session[:last_write])
  end
Changez simplement sesion [: last_write] en un objet temporel
C'est la fin de la lecture, alors écrivez
Resolver#write
class Resolver # :nodoc:
  def write(&blk)
    write_to_primary(&blk)
  end
  private
    def write_to_primary(&blk)
      ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role, prevent_writes: false) do
        instrumenter.instrument("database_selector.active_record.wrote_to_primary") do
          yield
        ensure
          context.update_last_write_timestamp
        end
      end
    end
C'est juste l'écriture dans le rôle d'écriture
class Session # :nodoc:
  def self.convert_time_to_timestamp(time)
    time.to_i * 1000 + time.usec / 1000
  end
  def update_last_write_timestamp
    session[:last_write] = self.class.convert_time_to_timestamp(Time.now)
  end
Et donnez l'horodatage actuel à session [: last_write]
Comme prévu, c'est Rails. Cela a été rendu très facile à comprendre.
Après tout, il n'est traité que sur la base de ActionDispatch :: Request, et si vous réécrivez à la fois # read et # write de Resolver
Il me semblait que je pouvais écrire tout ce que je voulais, pas seulement GET and HEAD.
Le jugement par en-tête, adresse IP, hôte local, etc. peut également être utilisé car ce sont les informations dont dispose Request. Je pensais que je pouvais faire beaucoup.
Recommended Posts