Rails 3 - Multiple database with joins condition - ruby-on-rails

My environment: Ruby 1.9.2p290, Rails 3.0.9 and RubyGem 1.8.8
unfortunately I have an issue when come across multiple database.
The situation is this: I have two model connect with two different database and also establishing association between each other.
database connection specifying in each model, look likes
class Visit < ActiveRecord::Base
self.establish_connection "lab"
belongs_to :patient
end
class Patient < ActiveRecord::Base
self.establish_connection "main"
has_many :visits
end
I got an error when meet following scenario
#visits = Visit.joins(:patient)
Errors: Mysql2::Error: Table 'lab.patients' doesn't exist: SELECT visits.* FROM visits INNER JOIN patients ON patients.id IS NULL
Here 'patients' table is in 'main' database and 'visits' table in 'lab' database
I doubt when executing the code, that Rails is considering 'patients' table is part of 'lab' database [which holds 'visits' table].

Well, I don't know if this is the most elegant solution, but I did get this to work by defining self.table_name_prefix to explicitly return the database name.
class Visit < ActiveRecord::Base
def self.table_name_prefix
renv = ENV['RAILS_ENV'] || ENV['RACK_ENV']
(renv.empty? ? "lab." : "lab_#{renv}.")
end
self.establish_connection "lab"
belongs_to :patient
end
class Patient < ActiveRecord::Base
def self.table_name_prefix
renv = ENV['RAILS_ENV'] || ENV['RACK_ENV']
(renv.empty? ? "main." : "main_#{renv}.")
end
self.establish_connection "main"
has_many :visits
end
I'm still working through all the details when it comes to specifying the join conditions, but I hope this helps.

Might be cleaner to do something like this:
def self.table_name_prefix
"#{Rails.configuration.database_configuration["#{Rails.env}"]['database']}."
end
That will pull the appropriate database name from your database.yml file

Or even
def self.table_name_prefix
self.connection.current_database+'.'
end

Is your 2nd database on another machine? You can always do as suggested in this other question:
MySQL -- Joins Between Databases On Different Servers Using Python?

I'd use the self.table_name_prefix as proposed by others, but you can define it a little more cleanly like this:
self.table_name_prefix "#{Rails.configuration.database_configuration["#{Rails.env}"]['database']}."
alternatively you could also use this:
self.table_name_prefix "#{connection.current_database}."
You have to keep in mind that the latter will execute a query SELECT DATABASE() as db the first time that class is loaded.

Related

Rails / Multi-Tenancy: Conditional default scope based on a different model's db value / global setting?

I've got a Rails application that is multi-tenant. Every model has an account_id, belongs to an account, and has a default scope to a current account id:
class Derp < ApplicationRecord
default_scope { where(account_id: Account.current_id) }
belongs_to :account
end
This works well and I've used this pattern in production in other apps (I understand that default scopes are frowned upon, but this is an accepted pattern. See: https://leanpub.com/multi-tenancy-rails).
Now here's the kicker - I have one client (and potentially more down the line, who knows), who wants to run the software on their own server. To solve this, I simply made a Server model with a type attribute:
class Server < ApplicationRecord
enum server_type: { multitenant: 0, standalone: 1 }
end
Now on my multi-tenant server instance, I simply make one Server record and set the server_type to 0, and on my standalone instance I set it to 1. Then I've got some helper methods in my application controller to help with this, namely:
class ApplicationController < ActionController::Base
around_action :scope_current_account
...
def server
#server ||= Server.first
end
def current_account
if server.standalone?
#current_account ||= Account.first
elsif server.first.multitenant?
#current_account ||= Account.find_by_subdomain(subdomain) if subdomain
end
end
def scope_current_account
Account.current_id = current_account.id
yield
rescue ActiveRecord::RecordNotFound
redirect_to not_found_path
ensure
Account.current_id = nil
end
end
This works, but I've got large record sets that I'm querying on this particular standalone client (70,000 records). I've got an index on the account_id, but it took my main customers table from 100ms to 400ms on my development machine.
Then I realized: standalone servers really don't need to concern themselves with the account id at all, especially if it is going to affect performance.
So really all I've got to do is make this line conditional:
default_scope { where(account_id: Account.current_id) }
I'd like to do something like this:
class Derp < ApplicationRecord
if Server.first.multitenant?
default_scope { where(account_id: Account.current_id) }
end
end
But obviously that syntax wrong. I've seen some other examples on Stack Overflow for conditional scopes, but none seem to work with a conditional statement based on a completely separate model. Is there a way to accomplish something like that in Ruby?
EDIT: Kicker here that I just realized is that this will only solve the speed issue for the one standalone server, and all the multi-tenant accounts will still have to deal with querying with the account_id. Maybe I should focus on that instead...
I would avoid using default_scope as I've been bitten by it in the past. In particular, I've had places in an application where I want to definitely have it scoped, and other places where I don't. The places where I want the scoping typically end up being controllers / background jobs and the places where I don't want / need it end up being the tests.
So with that in mind, I would opt for an explicit method in the controller, rather than an implicit scoping in the model:
Whereas you have:
class Derp < ApplicationRecord
if Server.first.multitenant?
default_scope { where(account_id: Account.current_id) }
end
end
I would have a method in the controller called something like account_derps:
def account_derps
Derp.for_account(current_account)
end
Then wherever I wanted to load just the derps for the given account I would use account_derps. I would then be free to use Derp to do an unscoped find if I ever needed to do that.
Best part about this method is you could chuck your Server.first.multitenant? logic here too.
You mention another problem here:
This works, but I've got large record sets that I'm querying on this particular standalone client (70,000 records). I've got an index on the account_id, but it took my main customers table from 100ms to 400ms on my development machine.
I think this is most likely due to a missing index. But I don't see the table schema here or the query so I don't know for certain. It could be that you're doing a where query on account_id and some other field, but you've only added the index to the account_id. If you're using PostgreSQL, then an EXPLAIN ANALYZE before the query will point you in the right direction. If you're not sure how to decipher its results (and sometimes they can be tricky to) then I would recommend using the wonderful pev (Postgres EXPLAIN Visualizer) which will point you at the slowest parts of your query in a graphical format.
Lastly, thanks for taking the time to read my book and to ask such a detailed question about a related topic on SO :)
Here's my solution:
First, abstract the account scoping stuff that any account scoped model will have to an abstract base class that inherits from ApplicationRecord:
class AccountScopedRecord < ApplicationRecord
self.abstract_class = true
default_scope { where(account_id: Account.current_id) }
belongs_to :account
end
Now any model can cleanly be account scoped like:
class Job < AccountScopedRecord
...
end
To solve the conditional, abstract that one step further into an ActiveRecord concern:
module AccountScoped
extend ActiveSupport::Concern
included do
default_scope { where(account_id: Account.current_id) }
belongs_to :account
end
end
Then the AccountScopedRecord can do:
class AccountScopedRecord < ApplicationRecord
self.abstract_class = true
if Server.first.multitenant?
send(:include, AccountScoped)
end
end
Now standalone accounts can ignore any account related stuff:
# Don't need this callback on standalone anymore
around_action :scope_current_account, if: multitenant?
# Method gets simplified
def current_account
#current_account ||= Account.find_by_subdomain(subdomain) if subdomain
end

Rails codebase scaling and changing on ActiveRecord

I'm currently rewriting our rails server code since many months have passed since our project began and there's a lot of dead code. We needed to migrate from Rails 3.2.12 to Rails 4. The issue is we need to rename some of our models and also need them to connect to the same PostgresSQLdatabase (ActiveRecord) and Redis objects.
For example:
#api v1
class ModelA < ActiveRecord::Base
include Redis::Objects
...
attr_accessible :id, :text_body
...
set something_redis_ids #redis object
...
end
To:
#api v2
class RenamedModelB < ActiveRecord::Base
include Redis::Objects
...
attr_accessible :id, :text_body
...
set something_redis_ids #redis object
...
end
That is I want to RenamedModelB to have its id andtext_body from PSQL as well as the something_redis_ids from redis to point to the exact same items as in ModelA. How do we accomplish this? We prefer to basically rewrite the codebase from scratch so don't really want to build on top of our old, ugly api v1.
Thanks for taking the time to read.
Is your question about the existing table names not matching your new class name? IF so, you can simply override it:
class Product < ActiveRecord::Base
self.table_name = "PRODUCT"
end
http://guides.rubyonrails.org/active_record_basics.html#overriding-the-naming-conventions

Of STI, MTI, or CTI, which is the currently suggested Rails 4 solution?

I have the a basic events table, and want to have sub-tables for each event type (hiking, party, riverrun, etc).
I see a lot of old (2011/2012) posts regarding CTI, MTI and STI. Some solutions worked for Heroku, while others did not.
What is the "current" Rails way of doing this type of thing? Has this been added to Rails 4.x? Is there a magical Gem that handles this (with Postgres on Heroku)?
Some information if it helps:
In the future, there will be between 20-50 events, and each sub-table might be as many as 80 columns. The site is hosted on Heroku. Running Rails 4.0.2
STI - Single Table Inheritance is what you are looking for.
http://api.rubyonrails.org/classes/ActiveRecord/Base.html#class-ActiveRecord::Base-label-Single+table+inheritance
http://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html
You create your model as always but you add an attribute even_type as a string to your database. (default ist "type")
You let the EventModel know, that this will be the inheritance column.
class Event < ActiveRecord::Base
self.inheritance_column = :event_type
# dont forget your scopes to easy access them by type
# Event.party or Event.ultrafestival
scope :party, -> { where(event_type: 'Party') }
scope :ultrafestival, -> { where(event_type: 'UltraFestival') }
scope :tomorrowland, -> { where(event_type: 'TomorrowLand') }
def host
raise "needs to be implemented in your sub-class!"
end
end
Than you create some subclasses. Make sure those are inheriting from Event
class Party < Event
end
class UltraFestival < Event
end
class Tomorrowland < Event
end
Basically, all Objects are Events! So if you go Event.all you'll get them all! Technically an Object can be something else. All those "Events" will be stored in the same table, they will be differed by the event_type which will be in this example "party", "ultra_festival" or "tomorrowland".
Now you can add some special stuff to each of those classes for example
class Party < Event
def host
"private homeparty PTY"
end
def publish_photostream
end
def call_a_cleaning_woman
end
end
class UltraFestival < Event
attr_accessor :location
def host
"UMF Festival Corp."
end
def is_in_europe?
end
def is_in_asia?
end
end
class Tomorrowland < Event
def host
"ID&T"
end
def download_after_movie!
end
end
This is the standard Rails way - since years. Of course its working on every hoster and with postgres.
// edit:
if your sub-events need some special tables in the database, then you need to go MTI, multi-table-inheritance.

Rails :scope across databases

I have three models, Alarms, Sites, Networks. They are connected by belongs_to relationships, but they live in diferent databases
class Alarm < ActiveRecord::Base
establish_connection :remote
belongs_to :site, primary_key: :mac_address, foreign_key: :mac_address
end
class Site < ActiveRecord::Base
establish_connection :remote
belongs_to :network
end
class Network < ActiveRecord::Base
establish_connection :local
end
I wish to select all alarms belonging to a particular network. I can do this usng raw sql within a scope as follows:
scope :network_alarms, lambda { |net|
#need to ensure we are interrogating the right databases - network and site are local, alarm is remote
remote=Alarm.connection.current_database
local=Network.connection.current_database
Alarm.find_by_sql("SELECT network.* FROM #{local}.network INNER JOIN #{local}.site ON network.id = site.network_id INNER JOIN #{remote}.alarm on #{remote}.alarm.mac_address = site.mac_address WHERE site.network_id=#{net.id}")
}
and this works fine. However, this scope returns an array, so I can't chain it (for example, to use with_paginate's #page method). So
Is there a more intelligent way of doing this join. I have tried using join and where statements, for example (this is one of many variations I have tried):
scope :network_alarms, lambda { |net| joins(:site).where("alarm.mac_address = site.mac_address").where('site.network_id=?', net) }
but the problem seems to be that the rails #join is assuming that both tables are in the same database, without checking the connections that each table is using.
So the answer is simple when you know how...
ActiveRecord::Base has a table_name_prefix method which will add a specific prefix onto the table name whenever it is used in a query. If you redefine this method to add the database name onto the front of the table name, the SQL generated will be forced to use the correct database
so, in my original question we add the following method definition into tables Alarm, Site and Network (and anywhere else it was required)
def self.table_name_prefix
self.connection.current_database+'.'
end
we can then build the joins easily using scopes
scope :network_alarms, lambda { |net| joins(:site => :network).where('site.network_id=?', net) }
Thanks to srosenhammer for the original answer (here : Rails 3 - Multiple database with joins condition)
Steve

How to best handle per-Model database connections with ActiveRecord?

I'd like the canonical way to do this. My Google searches have come up short. I have one ActiveRecord model that should map to a different database than the rest of the application. I would like to store the new configurations in the database.yml file as well.
I understand that establish_connection should be called, but it's not clear where. Here's what I got so far, and it doesn't work:
class Foo < ActiveRecord::Base
establish_connection(('foo_' + ENV['RAILS_ENV']).intern)
end
Also, it is a good idea to subclass your model that uses different database, such as:
class AnotherBase < ActiveRecord::Base
self.abstract_class = true
establish_connection "anotherbase_#{RAILS_ENV}"
end
And in your model
class Foo < AnotherBase
end
It is useful when you need to add subsequent models that access the same, another database.
Heh. I was right! More cleanly:
class Foo < ActiveRecord::Base
establish_connection "foo_#{ENV['RAILS_ENV']}"
end
Great post at pragedave.pragprog.com.

Resources