Ruby on Rails 3で複数のデータベースを操作する

みなさんこんにちは。 私は(比較的)初心者のRuby on Rails開発者です。 現在、いくつかのデータベースを使用するアプリケーションを開発しています。 この問題に関する情報はインターネット上にはあまりないので、すべてをまとめてhabrasocietyと共有することにしました。

繰り返しますが、私は自分がRailsの初心者であると考えているため、これを正しく行う方法に関する記事ではありません。 これは、私が何をどのようにしたかに関するメモのコレクションです。



かなり具体的なタスクがありますが、コードはそれほど多くなく、自分のニーズに合わせてリメイクすることは難しくありません。



挑戦する



一部の商品を販売している企業に対して特定のMDGを作成する必要があります。 企業は一度に複数のブランドと連携し、各ブランドは個別のデータベースを備えた独自のCRMを必要とします。 私の実装では、会社はサブドメインによって決定され、URLからのブランド、たとえばURL company1.myapp.dev/brand1/は、company1およびbrand1ブランドで作業していることを示しています。



それはすべてモデルから始まります



この場合、会社とブランドの2つのモデルを区別することは論理的でした。



会社




ブランド名






注:ほとんどの場合、データベースはドメインごとにのみ切り替える必要があるため、ブランドモデルを削除し、db_nameフィールドをCompanyモデルに転送できます。 ローカルサーバーでDBMSのみを使用する場合は、db_user、db_hostなどのフィールドを完全に削除できます。 いつかクラウドサービスに切り替えることを計画しており、便利になるかもしれません。



これらのモデルのテーブルは、アプリケーションが動作する各データベースに存在する必要がありますが、データは1つ(RAILS_ENVに応じて本番または開発)にのみ保存されます。 アプリケーションが特定のデータベース内のデータのみを検索するには、 establish_connectionメソッドを使用する必要があります。



/models/company.rb


class Company < ActiveRecord::Base establish_connection "production" has_many :brands, dependent: :destroy validates :subdomain, :db_user, :db_host, :db_port, :name, presence: true end
      
      







/models/brand.rb


 class Brand < ActiveRecord::Base establish_connection "production" belongs_to :company validates :name, :db_name, presence: true end
      
      







コードを書く



routes.rb


現在どのブランドで作業しているのかを常に把握する必要があるため、すべてを包括する必要があります。

 MyApp::Application.routes.draw do scope ':brand' do resource :sessions, only: [:new, :create, :destroy] #  .. match '/' => redirect("/%{brand}/orders"), as: 'brand_root' end root :to => "main#index" end
      
      





application.rb


 class ApplicationController < ActionController::Base protect_from_forgery before_filter :override_db before_filter :authenticate_user! def not_found raise ActionController::RoutingError.new('Not Found') end #    scope,     #     ,    #      # Ex:  orders_path(brand: @current_brand.name)    orders_path def url_options if @current_brand.present? { :brand => @current_brand.name }.merge(super) else super end end private #     def override_db @current_company = Company.where("(subdomain = ? or alias = ?) AND active = ?", request.env['HTTP_HOST'][/^[\w\-]+/], request.env['HTTP_HOST'], true).first not_found unless @current_company.present? && @current_company.brands.present? if params[:brand].present? @current_brand = @current_company.brands.find_by_name params[:brand] if @current_brand.present? ActiveRecord::Base.clear_cache! ActiveRecord::Base.establish_connection( :adapter => "postgresql", :host => @current_company.db_host, :username => @current_company.db_user, :password => @current_company.db_password, :database => @current_company.db_name ) redefine_uploaders_store_dir else redirect_to root_url end end end #    CarrierWave def redefine_uploaders_store_dir CarrierWave::Uploader::Base.descendants.each do |d| d.class_eval <<-RUBY, __FILE__, __LINE__+1 def store_dir "uploads/#{@current_company.subdomain}/\#{model.class.to_s.underscore}/\#{mounted_as}/\#{model.id}" end RUBY end end end
      
      







新しいデータベースに接続する前に、必ずActiveRecordキャッシュをクリアする必要があります(ActiveRecord :: Base.clear_cache!またはActiveRecord :: Base.connection_pool.clear_reloadable_connections!)。

redefine_uploaders_store_dirメソッドは、CarrierWaveがファイルを保存するディレクトリをオーバーライドします。 このハックはできませんでした。競合の可能性は非常に小さいので(ファイル名とモデルIDは一致するはずです)、存在するので、安全にプレイすることにしました。



何も機能しない別のささいなこと



config / environment / production.rbで、クラスのキャッシュを無効にする必要があります。

 config.cache_classes = false
      
      







はい、パフォーマンスは低下していますが、そうでなければこの問題を解決する方法がわかりません。



セッション



私の場合、セッションは大量のデータを保存できるため、Cookieではなくデータベースに保存する必要があります。 さらに、メインページにはさまざまなブランドの承認フォームがあり、それらが既に承認されており、ボタンをクリックするだけで入力できるユーザーを表示したいのですが、パスワードを入力する必要があります。 したがって、セッションを1つのデータベースに格納する必要があり、複数に分散する必要はありません。

まず、ActiveRecordを使用してセッションを保存するようにRailsに指示します。

 rails g session_migration rake db:migrate
      
      





config / initializers / session_store.rb


 MyApp::Application.config.session_store :active_record_store
      
      







次に、特定のデータベースを使用して保存するように指示します。

config / environment.rb


 # Load the rails application require File.expand_path('../application', __FILE__) # Initialize the rails application MyApp::Application.initialize! ActiveRecord::SessionStore::Session.establish_connection "production"
      
      







注:念のため、こことモデルの「本番」はデータベース名ではなく、config / database.ymlのセクション名であるということを予約します。



移行



移行には、次のソリューションを使用できます。

lib / tasks / multimigrate.rake


 namespace :db do desc "Migrations for all databases" task :multimigrate => :environment do Company.all.each do |company| company.brands.each do |brand| puts "Run migration for #{company.name} (#{brand.name})" sh "cd #{Rails.root.to_s} && bundle exec rake db:migrate RAILS_ENV=#{brand.db_name}" end end end end
      
      







すべてが機能するには、すべてのデータベースがdatabase.ymlにリストされている必要があります。 私自身は、database.ymlのパーティション(環境)がデータベースのように呼び出されるというルールを思いつきました。

管理パネルで新しい会社を追加すると、コントローラーがdatabase.ymlに新しいセクションを追加しますが、このモデルに指示することもできます(どのモデルが適切かはわかりませんが、便利です)。 このようなもの:

 class Brand < ActiveRecord::Base establish_connection "production" belongs_to :company after_save :sync_to_yml validates :name, :db_name, presence: true private def sync_to_yml db_config = YAML.load_file(Rails.root.to_s + '/config/database.yml') db_config[self.db_name] = { 'adapter' => 'postgresql', 'encoding' => 'unicode', 'database' => self.db_name, 'pool' => 5, 'username' => self.company.db_user, 'password' => self.company.db_password.present? ? self.company.db_password : nil } if self.company.db_host != 'localhost' db_config[self.db_name].merge( { 'host' => self.company.db_host, 'port' => self.company.db_port } ) end File.open( Rails.root.to_s + '/config/database.yml', 'w' ) do |out| YAML.dump( db_config, out ) end end end
      
      





注意! コードはテストされていません。 方法を提案しただけです。 さらに、コールバックをafter_destroyに追加する必要があります。



すべてのようで、いくつかのデータベースを操作するために既存のRailsアプリケーションを非常に簡単に書き直すことが判明しました。 私が決定に役立った情報源を共有したいと思いますが、それらの多くがあり、それらを見つけることは困難です(一言で言えば、怠iness)。 しかし、私は投稿のために写真のソースを与えることができます。



All Articles