In our multi-tenant, shared db application, I am looking for a way to extend models to always include the account scope as part of their SQL conditions when running a query. Ideally, i'd extend ActiveRecord so i can include something like scoped_by_account in all relevant models.
I've searched around the internet and found one plugin that claims to do just this. Unfortunately, the scoping searches part of the plugin is not working as far as I can tell. The plugin (which can be found here: https://github.com/mconnell/multi_tenant) extends ActiveRecord and uses the following code to 'inject' the additional search condition in every search:
def self.unscoped
...
super.apply_finder_options(:conditions => { account_id => Account.current.id })
end
From what I can tell, this does not succeed in applying the condition to all searches. If I query Project.all it will return all results, ignoring the current account. Only if I use Project.unscoped it will work correctly.
My question:
How can I extend ActiveRecord to include my additional condition in every query it runs for that model?
Thx for your time,
Erwin
Are you looking for default_scope?
Related
In my Ruby on Rails project, I have a mailer that basically prepares a daily digest of things that happened in the system for a given user. In the mailer controller, I am gathering all the relevant records from the various models according to some common pattern (within a certain date, not authored by this user, not flagged, etc) and with minor differences from model to model.
There are half a dozen of models involved here (and counting), and most of them have unified column names for certain things (like date of publishing, or whether an item is flagged by admin or not). Hence, the 'where's that go into query are mostly the same. There are minor differences in conditions, but at least 2 or 3 conditions are exactly the same. I easily assume there may be even more similar conditions between models, since we are just starting the feature and haven't figured out the eventual shape of the data yet.
I basically chain the 'where' calls upon each model. It irritates me to have 6 lines of code so close to each other, spanning so far to the right of my code editor, and yet so similar. I am dreaded by the idea that at some point we will have to change one of the 'core' conditions, munging with that many lines of code all at once.
What I'd love to do is to move a core set of conditions that goes into each query into some sort of Proc or whatever, then simply call it upon each model like a scope, and after that continue the 'where' chain with model-specific conditions. Much like a scope on each model.
What I am struggling with is how exactly to do that, while keeping the code inside mailer. I certainly know that I can declare a complex scope inside a concern, then mix it into my models and start each of queries with that scope. However, this way the logic will go away from the mailer into an uncharted territory of model concerns, and also it will complicate each model with a scope that is currently only needed for one little mailer in a huge system. Also, for some queries, a set of details from User model is required for a query, and I don't want each of my models to handle User.
I like the way scopes are defined in the Active Record models via lambdas (like scope :pending, -> { where(approved: [nil, false]) }), and was looking for a way to use similar syntax outside model class and inside my mailer method (possibly with a tap or something like that), but I haven't found any good examples of such an approach.
So, is it possible to achieve? Can I collect the core 'where' calls inside some variable in my mailer method and apply them to many models, while still being able to continue the where chain after that?
The beauty of Arel, the technology behind ActiveRecord query-building, is it's all completely composable, using ordinary ruby.
Do I understand your question right that this is what you want to do?
def add_on_something(arel_scope)
arel_scope.where("magic = true").where("something = 1")
end
add_on_something(User).where("more").order("whatever").limit(10)
add_on_something( Project.where("whatever") ).order("something")
Just ordinary ruby method will do it, you don't need a special AR feature. Because AR scopes are already composable.
You could do something like:
#report_a = default_scope(ModelA)
#report_b = default_scope(ModelB)
private
def default_scope(model)
model.
where(approved: [nil, false]).
order(:created_at)
# ...
end
I'm building an API wrapper that will query objects from a third-party API and build them into objects to be used in my Rails environment. To do that, I'm building a set of models that use ActiveRecord (for some of its functionality) but are not database backed. I would like to be able to make a call like this:
obj = MyModel.find(1)
And have the code be something like this:
def MyModel.find id
# check for object in cache
# check for object in db
# grab object from API
# return object
end
Am I going to do something horribly wrong if I override the default find method? Am I approaching this in totally the wrong way?
If you are not using a database, then you do not need ActiveRecord. The entire purpose of ActiveRecord is to give you a mapping to a relational database.
I think what you want is for a class to implement certain pieces of what ActiveRecord provides, and Rails 3 has made those pieces into classes that you can include into regular 'ol classes on an as-needed basis. Look at this article for more details: http://www.rubyinside.com/rails-3-0s-activemodel-how-to-give-ruby-classes-some-activerecord-magic-2937.html
For instance, if you only want validations on a class, you can use include ActiveModel::Validations and then you'll get all of the nice error handling and .valid? and validates presence: true kind of behavior you're used to.
I would also suggest the railscast by Ryan Bates: http://railscasts.com/episodes/219-active-model which goes into more detail.
I've got three nested models: user has many plates and plate has many fruits. I also have a current_user helper method that runs in the before filter to provide authentication. So when I get to my controller, I already have my user object. How can I load all the user's plates and fruits at once?
In other words, I'd like to do something like:
#plates = current_user.plates(include: :fruits)
How can I achieve this?
I'm using Rails 3.1.3.
You will probably want to use the provided #includes method on your relation. DO NOT USE #all unless you intend to immediately work through the records, it will immediately defeat many forms of caching.
Perhaps something like: #plates = current_user.plates.includes(:fruits)
Unfortunately, there are portions of the Rails API that are not as well documented as they should be. I would recommend checking out the following resources if you have any further questions about the Rails query interface:
Query Interface Guide
ActiveRecord::Relation Walkthrough (screencast)
The query interface is possibly the most difficult part of the Rails stack to keep up with, especially with the changes made with Rails 3.0 and 3.1.
You can do
ActiveRecord::Associations::Preloader.new([current_user], :plates => :fruit).run
To eager load associations after current_user was loased. The second argument can be anything you would normally pass to includes: a symbol, an array of symbols, a hash etc
#plates = current_user.plates.all(:include => :fruits)
should do it.
I have a number of custom find_by_sql queries in Rails. I would like to use eager loading with them but there doesn't seem to be a good way to do this.
I have seen the eager_custom.rb file floating around and it doesn't seem to work with Rails now. It appear Rails does eager loading differently now, using 2 queries (the regular query plus a query where the 'id IN' the ids from the first query), instead of the single join query used in the past.
My question is if I do a custom SQL query, then do 'id IN' query, is there a way to add back associated objects into the initial query results?
For example I have topics loaded with find_by_sql, then I find topic images where the topic id is in the topics ids, is there a way to add the images manually back to the topics?
Thanks
As you noticed, in Rails 2.1 a new kind of eager/pre-loading was introduced which uses multiple queries with id IN (...). This method is usually faster, especially when there are multiple associations being pre-loaded. You can use this functionality manually with find_by_sql by using the preload_associations class method inherited from ActiveRecord (not recommended). For example:
class Person
def self.find_a_special_group
people = find_by_sql("...")
preload_associations(people, [:jobs, :addresses])
return people
end
end
The preload_associations method is protected, so you must call it from within the class, and it takes (1) an array of objects, (2) an array, hash, or symbol of associations (same format as find's :include option), and (3) an options hash. See the documentation for the ActiveRecord::AssociationPreload::ClassMethods module for more details.
However, having said all of that, this technique is certainly undesirable as the Rails documentation discourages programmers from using preload_associations directly. Are you sure you have to use find_by_sql? Are you sure you know all of the options find takes? (:select, :from, :joins, :group, :having, etc) I'm not saying you don't need find_by_sql, but it might be worth a few minutes to make sure.
I'm working on a Rails (currently 2.3.4) app that makes use of subdomains to isolate independent account sites. To be clear, what I mean is foo.mysite.com should show the foo account' content and bar.mysite.com should show bar's content.
What's the best way to ensure that all model queries are scoped to the current subdomain?
For example, one of my controllers looks something like:
#page = #global_organization.pages.find_by_id(params[:id])
(Note #global_organization is set in the application_controller via subdomain-fu.)
When what I would prefer is something like:
#page = Page.find_by_id(params[:id])
where the Page model finds are automatically scoped to the right organization. I've tried using the default_scope directive like this: (in the Page model)
class Page < ActiveRecord::Base
default_scope :conditions => "organization_id = #{Thread.current[:organization]}"
# yadda yadda
end
(Again, just to note, the same application_controller sets Thread.current[:organization] to the organization's id for global access.) The problem with this approach is that the default scope gets set on the first request and never changes on subsequent requests to different subdomains.
Three apparent solutions thus far:
1 Use separate vhosts for each subdomain and just run different instances of the app per subdomain (using mod_rails). This approach isn't scalable for this app.
2 Use the original controller approach above. Unfortunately there are quite a number of models in the app and many of the models are a few joins removed from the organization, so this notation quickly becomes cumbersome. What's worse is that this actively requires developers to remember and apply the limitation or risk a significant security problem.
3 Use a before_filter to reset the default scope of the models on each request. Not sure about the performance hit here or how best to select which models to update per-reqeust.
Thoughts? Any other solutions I'm missing? This would seem to be a common enough problem that there's gotta be a best practice. All input appreciated, thanks!
Be careful going with default scope here, as it will lead you into a false sense of security, particularly when creating records.
I've always used your first example to keep this clear:
#page = #go.pages.find(params[:id])
The biggest reason is because you also want to ensure this association is applied to new records, so your new/create actions will look like the following, ensuring that they are properly scoped to the parent association:
# New
#page = #go.pages.new
# Create
#page = #go.pages.create(params[:page])
Have you tried defining the default_scope a lambda? The lambda bit that defines the options get evaluated every time the scope is used.
class Page < ActiveRecord::Base
default_scope lambda do
{:conditions => "organization_id = #{Thread.current[:organization]}"}
end
# yadda yadda
end
It's essentially doing your third option, by working in tandem with your before filter magic. But it's a little more aggressive than that, kicking in on every single find used on the Page model.
If you want this behaviour for all models you could add the default_scope to ActiveRecord::Base, but you mention a few being a couple of joins away. So if you go this route, you'll have to override the default scopes in those models to address the joins.
We've been using https://github.com/penguincoder/acts_as_restricted_subdomain for the last 2 years, but it only works with Rails 2.3.
We are currently trying to upgrade (and gemmify) the plugin to work with Rails 3. I'm curious on how you worked your problem out.
You might be better of having a database per account and switching the database connection based on the subdomain.
In addition to the above link if you have a model (in your case Account) that you want to use the default database just include establish connection in the model.
class Account < ActiveRecord::Base
# Always use shared database
establish_connection "shared_#{RAILS_ENV}".to_sym
I've found the acts_as_tenant gem [github] works great for this feature. It does the scoping for you with minimal extra effort, and also makes bypassing the scoping difficult.
Here's the initial blog post about it: http://www.rollcallapp.com/blog/2011/10/03/adding-multi-tenancy-to-your-rails-app-acts-as-tenant