📜 ⬆️ ⬇️

Working with multiple databases in Ruby on Rails 3

Hello. I am a beginner (relatively) Ruby on Rails developer. Currently I am developing an application that uses several databases. Information on this issue on the Internet is not as much as we would like, so I decided to put it all together and share it with the media community.
Again, I consider myself a newcomer to the rails, so this is not an article on how to do it right. This is just a collection of notes about what and how I did it.

I have a rather specific task, but the code is not so much and it is not difficult to remake it to fit my needs.

Task


It is necessary to make a certain CRM for companies selling certain goods. Companies work with several brands at once, and each brand needs its own CRM with a separate database. In my implementation, the company is determined by the subdomain, and the brand from the URL, for example, the URL company1.myapp.dev/brand1/ tells us that we work with the company1 company and the brand1 brand.

It all starts with models


In this case, it was logical to distinguish 2 models: Company and Brand.
')
Company


Brand



Note: most often the database needs to be switched only by domain, so the Brand model can be removed and the db_name field transferred to the Company model. If you plan to use only the DBMS on the local server, then you can completely remove the db_user, db_host, etc. fields. I plan to sometime go to cloud services and it can come in handy.

The tables for these models should be in each database with which the application will work, but the data will be stored only in one (production or development, depending on your RAILS_ENV). In order for the application to search for data only in a specific database, you need to use the establish_connection method.

/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 


Write the code


routes.rb

Since we always need to know which brand we are working with now, we need to wrap everything in scope.
 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 


Before connecting to the new database, it is necessary to clear the ActiveRecord cache (ActiveRecord :: Base.clear_cache! Or ActiveRecord :: Base.connection_pool.pool_reloadable_connections!).
The redefine_uploaders_store_dir method overrides the directories in which CarrierWave will store files. You could not do this hack, the probability of a conflict is very small (the names of the files and the id of the models must match), but it is there, so I decided to make sure.

Another little thing without which nothing will work


In config / environments / production.rb, you need to disable class caching.
 config.cache_classes = false 


Yes, productivity is declining, but I don’t know how to solve this problem otherwise

Sessions


In my case, a session can store a lot of data, so I need to store it in a database, not in cookies. In addition, on the main page I have an authorization form for different brands and I would like to show the user in which he is already authorized and can enter by pressing one button, and for which you still need to enter a password. Therefore, you need to store the session in one database, and not spread over several.
First we tell the rails to use ActiveRecord for storing sessions:
 rails g session_migration rake db:migrate 

config / initializers / session_store.rb

 MyApp::Application.config.session_store :active_record_store 


And then we tell them to use a certain database to store them:
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" 


Note: just in case I will make a reservation that “production” here and in models is not the name of the database, but the name of the section in config / database.yml.

Migrations


For migrations, you can use this solution:
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 


For everything to work, all databases must be listed in database.yml. For myself, I came up with the rule that the section (environment) in database.yml will be called like a database.
Now, when I add a new company to the admin, the controller adds a new section to database.yml, but you can also assign this model (I don’t know how kosher it will be, but convenient). Like that:
 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 

Attention! Code not tested. Just suggested how to do it. In addition, you still need to add a callback to after_destroy.

It seems to be all, it turned out to rewrite the existing Rails application for working with several databases is very simple. I would be happy to share the sources that helped me in solving, but there were a lot of them and it would be difficult to find them (in one word, laziness). But I can give the source to the picture for the post .

Source: https://habr.com/ru/post/143761/


All Articles