I have a multi-tenant rails app up and running.
Models that i want scoped to the current tenant (like this article model here) inherit the tenantscoped model like this
class Article < TenantScoped
end
this works great. i only recieve objects scoped to the current tenant.
but now im creating an admin interface where i want to be able to add articles to all tenants. but my admin interface is acting as a tenant and the models are being scoped to it.
Which ends with no entries being shown.
I am proposing that the best solution to this is to conditionally inherit from the tenant scoped model like this
class Article
unless SudoTenant.current?
< TenantScoped
else
< ActiveRecord::Base
end
end
i've been searching around to conditional inheritance for ruby classes and havent found anything yet. my syntax is wrong here or is this even possible?
Thanks in advance
You can define the class using the block syntax:
if SudoTenant.current?
Article = Class.new(ActiveRecord::Base) do
# your code
end
else
Article = Class.new(TenantScoped) do
# your code
end
end
I strongly recommend to use mixins instead of conditionally inheriting, it's cleaner, clearer and more obvious.
Not exactly what you're asking, but I happen to be doing the same thing (global articles on a tenant app), and I just created a Tenant for Admin for using it in my global Articles.
I've got something like this:
#article.rb
def self.global
unscoped.where(:company => Company.admin)
end
#company.rb
def self.admin
where(:name => 'admin').first # this can pretty much be anything that fits to you.
end
Related
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
I have a multi domain app talking to a legacy database.
In that DB I have two tables with different names, lets call them USER_A and USER_B. Their structure and data types are exactly the same, the only difference is that they get their data from different domains.
Now, I would like to have a single scaffold (model/controller/view) that, depending on the domain, maps to the right DB table.
Domain A would work with a model/controller called User which maps internally to the db table USER_A, and Domain B would work with the same model/controller User but maps to the table USER_B.
I would also like to use resource :user in my routes to access the model the rails way.
So somehow I need to overwrite the model on initialization but I am not quite sure how to go about it.
How would one go about this using Rails ActiveRecord?
I don't have a multitable DB ready to test with, so this is an educated guess at the solution:
# respective models
class User < ActiveRecord::Base
end
class DomainAUser < User
self.table_name = "USER_A"
end
class DomainBUser < User
self.table_name = "USER_B"
end
# controller
def set_user
#user = if request.subdomain(0) == "DomainA"
DomainAUser.find(params[:id])
else
DomainBUser.find(params[:id])
end
end
Edit: Here's an alternative bit of metaprogramming hackery which does the subclass instantization within the parent class itself. Tested and working.
I really wouldn't want to maintain something like this though.
# model
class User < ActiveRecord::Base
def self.for_domain(domain_suffix)
class_eval "class DomainUser < User; self.table_name='user_#{domain_suffix}'; end"
"User::DomainUser".constantize
end
end
# controller
User.for_domain("a").new
Some of my models has a column named "company_id".
I need that all querys in these models has a condition based in this column, so I can easily separate the companies rows.
Something like this:
Customer.where(state: x).`where(company_id: current_company)`...
How can I intercept this method to enforce this extra condition?
I would recommend using a concern to add this requirement as a default scope to all of your models.
module HasCompany
extend ActiveSupport::Concern
included do
default_scope { where(company_id: current_company) }
end
end
class Customer < ActiveRecord::Base
include HasCompany
...
end
Note: this approach will only work if you have access to current_company as a class method on your models.
Where does this code live? It looks like controller logic? If it's in a controller, then you can just set the current_company in a before_action in the application controller—probably like you're doing already. Presuming you have a has_many relationship between company and customers, you should just do current_company.customers.where(state: x).
If this code lives in a model, that's when things get tricky. You shouldn't have access to current_company in a model, since that deals with the current request.
I am building an Invoicing Application that basically follows the following pattern:
Users < Clients < Projects < Invoices
Now in order to generate autoincrementing invoice numbers for each User I put this in my Invoice model:
before_create :create_invoice_number
def create_invoice_number
val = #current_user.invoices.maximum(:number)
self.number = val + 1
end
However, it seems that the current_user variable cannot be accessed from within models in Rails?
What can I do to solve this problem?
This is due to separation of concerns in Rails and is a somewhat sticky issue to deal with. In the Rails paradigm, models should have no knowledge of any application state beyond what they're passed directly, so most Rails coders will tell you that any model needing to know about a current_user is code smell.
That said, there are three ways to do this, each "more correct" (or at least I would consider them so).
First, try creating an association to the user inside the invoice and link the invoice to the user in the controller:
class InvoicesController < ApplicationController
...
def create
#invoice = current_user.invoices.create(params[:invoice])
...
end
And in your model:
belongs_to :user
def create_invoice_number
self.user.invoices.maximum(:number) + 1
end
If that doesn't work, do this manually in the controller. It's true that controllers should always be as skinny as you can manage, but since this is clearly an application-level concern the controller is the place to put it:
class InvoicesController < ApplicationController
...
def create
#invoice = Invoice.create(params[:invoice])
#invoice.update_attribute(:number, current_user.invoices.maximum(:number))
...
end
Lastly, if you really, really want to bridge the controller and model, you can do so with ActionController::Sweepers. They are not intended for this purpose but will certainly get the job done for you.
there should not be any arise of such case still if you want then make use of observers in rails
One of mine rails projects has many models with the same flag: approved.
I don't like to manage the flag 'approved' for so many models, and I am seeking a DRY solution.
I have found some plugin like flag_shih_tzu or can_flag, but I think they work only with a model.
Do you know some plugin to flag many models at once ?
I think that I a good solution (without plugin) should use the polymorphic associations, do you agree ?
many thanks,
Alessandro
If all you're looking for is a way to store all the functions in a single place, but have them accessible from all your flagable models, I'd recommend writing a mixin for them. For example, in lib/approved.rb, you could have the module:
module Approved
# Any approval functions/constants that don't belong in a model go here...
module Mixin
def self.included(klass)
klass.class_eval do
# Class-levell model macros can be run here
named_scope :approved, {:conditions => {:approved => true}}
named_scope :unapproved, {:conditions => {:approved => false}}
end
end
def approved?
return (self.approved == true)
end
# Other shared model functions go here...
end
end
And then it's just a matter of including the mixin in all the models that need those functions:
class Approvable < ActiveRecord::Base
include Approved::Mixin
# etc.
end
Hope that helps!
I have a similar problem in my application, we have 10 or so models that all require approval and didn't want to copy the code everywhere. In our case we are using transitions as our workflow gem, so instead of having a flag approved we have a string column state.
A model that requires approval looks like this:
class Comment < A:RB
include ApprovalWorkflow
end
Then we have a workflow that looks like this:
# /app/workflows/approval_workflow.rb
module ApprovalWorkflow
def self.included(klass)
klass.class_eval do
state_machine do
.. workflow junk goes here ..
end
end
end
end
So what's going on here is that we've created a module, which you can think of like an anonymous piece of code which does't belong anywhere (read more about modules to understand why this is an awful description), which we then include in our classes which mixes in the functionality. Now our comment class has the approval workflow!
In you case, assuming you were to keep the approval flag, you might add default validations, some methods like approve!(user) or scopes for querying.
I hope this helps.
Using polymorphic associations is not the solution unless they are all the same base object. Keep in mind that with inheritance the parent should have an is-a relationship with the child.
What you could do is create an Approval model and have a one-to-one relationship with the approvable models.