Follow association without loading parent object - ruby-on-rails

I have a class like this:
class Project < ActiveRecord::Base
has_many :versions, :order => :position
end
In a controller action, I need to query for a project's versions. I have the project ID. I could simply do a
versions = Project.find(params[:project_id]).versions
However, this will first do a query to the "projects" table, which is unnecessary in my case. I could change it to
versions = Version.find( :conditions => { :project_id => params[:project_id } )
(or use a dynamic finder with the same results)
This doesn't use the additional attributes of the association (in this case, the ":order => :position"), so if I wanted to get exactly the same result as in the same example, I will have to write:
versions = Version.find( :conditions => { :project_id => params[:project_id },
:order => :position )
And here I'm no longer DRY since I'm repeating something (the ":order" option) that is already defined in the model.
Is there a way to use and follow the association defined in the model without having to load the parent object? I'm currently still using Rails 2.3.8.

I would recommend to use one default_scope for ordering and another scope to find versions by project in Version model.
Project
class Project < ActiveRecord::Base
has_many :versions
end
Version
class Version < ActiveRecord::Base
belongs_to :project
named_scope :order_by_position, {:order => "position"}
named_scope :by_project_id, (lambda do |project_id|
{:conditions => ['project_id = ?', project_id]}
end)
end
Then you can use:
versions = Version.by_project_id(params[:project_id])

Sorry but I'm pretty sure you cannot do that - at least not following the public ActiveRecord API.
You could write extension that uses internal ActiveRecord API to inspect association options, but I wouldn't recommend this.
You could reuse some conditions through scopes but since you're still on Rails 2.3 that is not helping you.
I think your best bet is manually fetching version instances as you have written in the last example:
versions = Version.find( :conditions => { :project_id => params[:project_id },
:order => :position )
EDIT: What you could do in Rails4:
You can reuse and share scopes between models and associations like this:
class Project < ActiveRecord::Base
has_many :versions, -> { ordered }
end
class Version
def self.ordered
order(:position)
end
end
versions = Version.find_by(:project_id, params[:project_id]).ordered
The more scopes and the more complicated they are, the more useful this approach is.

Related

Is there any rails syntactic sugar that could make this prettier?

I've written a model for "Category". The requirements here are that each category can fall into one category "type". I'm learning rails at the same time as doing this project, and managed to get the above working with the following class method (where_category_type);
class Category < ActiveRecord::Base
#associations
belongs_to :category_type
has_and_belongs_to_many :recipes
def self.where_category_type category_type
Category.find(:all, :include => :category_type, :conditions => { :category_types => {:name => category_type }})
end
end
All works etc. but I am very keen to make sure I'm doing things "the rails way", so I was wondering if I'm missing some syntactic sugar somewhere that would make this a little more readable / less verbose?
class Category < ActiveRecord::Base
#associations
belongs_to :category_type
has_and_belongs_to_many :recipes
end
Then instead of defining *where_category_type* Category's class static method, you can call just:
Category.joins(:category_type).where('category_types.name' => 'name of your category').all

How do you concatenate two active record results to return a new result that can further be filtered?

Imagine the scenario...
#models/user.rb
class User < ActiveRecord::Base
has_many :accounts, :conditions => { :active => 1 }
end
#models/account.rb
class Account < ActiveRecord::Base
belongs_to :user
def public_accounts
Account.all :conditions => { public => true }
end
end
Now imagine I want to concatenate User(:id).accounts with Account.public_accounts to show a list of all accounts available to a user.
So you'd think I'd be able to update the User model to look like this.
#models/user.rb
class User < ActiveRecord::Base
has_many :accounts, :conditions => { :active => 1 }
def all_accounts
self.accounts + Account.public
end
end
However, now I won't be able to use the all() method since it's no longer of that type of object.
In the controller I'd like to do this...
#controllers/accounts_controller.rb
def search_all
User.find(params[:user_id]).all_accounts.all(
:offset => params[:offset],
:limit => params[:limit]
)
end
Thoughts?
Update #1:
Scope's won't work for my scenario. I simplified my scenario to try and get my point across. As stated I need a way to combine two active record results and retain the ability to further filter them in my controller.
So the question is, "Why?" The reason is, I am trying to combine two sets of records to form a complete collection and one of the collections is not associated with the user at all.
I have refactored the above scenario to try and show a more precise example without getting overly complicated.
This might be a good scenario to use scopes.
You can define active and inactive scopes in the Account model and then use the following:
User.accounts
User.accounts.active
User.accounts.inactive
You can even chain scopes together, so you could do something like:
User.accounts.active.paid_up
jklina's answer is correct, it's best to use scopes in this situation. Scopes provide a sugary syntax and are more readable. I'll elaborate on the setup:
class User < AR::Base
has_many :accounts
end
class Account < AR::Base
belongs_to :user
scope :active, where(:active => true)
scope :inactive, where(:active => false)
end
You would then access the account scopes as jklina showed: User.find(1).accounts.active, etc.
Accessing all of a user's accounts like: User.find(1).accounts.
UPDATE:
I fixed some mistakes and added more below.
Based on the updates to your question, I think you need to make the public method a method on the class:
class Accounts < AR::Base
...
# This is essentially a scope anyways
def self.public
where(:public => true)
end
end
class User < AR::Base
...
# This should return all the users accounts
# and any public accounts
def all_accounts
Account.where("user_id = ? OR public is true", self.id)
end
end
Lets look at the return values in the chain:
User.find(params[:user_id]) # returns an instance of User class
User.find(params[:user_id]).all_accounts # returns an array
The Array class doesn't have an instance method called all that's why you are seeing this error. This is not a bug.
Why don't you try this:
class User
has_many :accounts, :conditions => { :active => 1 }
has_many :all_accounts :conditions => ["(active = ? OR public = ?)",
true, true]
end
Now you can:
User.find(params[:user_id]).all_accounts.all(:limit => 10, :offset => 2)
You're try to access two distinct tables and apply LIMIT/OFFSET to them as a combined union. That aint gonna happen unless you logically combine them at the SQL layer, not at the ActiveRecord layer.
Sounds like writing out the SQL, maybe using a UNION and then using find_by_sql might be your best best.

activerecord find through association

I am trying to retrieve an activerecord object from my db. My models are
class User < ActiveRecord::Base
belongs_to :account
has_many :domains, :through => :account
end
And
class Account < ActiveRecord::Base
has_many :domains
has_many :users
end
And
class Domain < ActiveRecord::Base
belongs_to :account
end
Now I would like to retrieve a user based on the username and a domain name (lets assume that these are attributes of the User and the Domain classes respectively). i.e. something along the lines of
User.find(:first, :conditions =>{:username => "Paul", :domains => { :name => "pauls-domain"}})
I know that the above piece of code will not work since I do have to mention something about the domains table. Also, the association between users and domains is a one-to-many (which probably further complicates things).
Any ideas on how should this query be formed?
If you're using Rails 3.x, the following code would get the query result:
User.where(:username => "Paul").includes(:domains).where("domains.name" => "paul-domain").limit(1)
To inspect what happen, you can append .to_sql to above code.
If you're using Rails 2.x, you'd better write the raw sql query.
The following piece of code did the trick:
User.joins(:account).joins('INNER JOIN "domains" ON "accounts"."id" = \
"domains"."account_id"').where(:users => {"username" => "Paul"}).
where(:domains => {"name" => "paul-domain"})
Sorry about the formatting of this long line of code

Accessing rails helper methods in models

In Rails 3, I am having a problem accessing a helper method from within a model
In my ApplicationController I have a helper method called current_account which returns the account associated with the currently logged in user. I have a project model which contains projects that are associated with that account. I have specified 'belongs_to :account' in my project model and 'has_many' in my account model. All of this seems to be working correctly and my projects are being associated with the right accounts.
However at the moment when I use 'Project.all', all of the project records are being returned as you would expect. I would like it to automatically filter the projects so that only those associated with the specified account are returned and I would like this to be the default behaviour
So I tried using 'default_scope'. The line in my model looks something like this
default_scope :order => :name, :conditions => ['account_id = ?', current_account.id]
This throws an error saying
Undefined local variable or method current_account for #
If I swap the call to current_account.id for an integer id - eg
default_scope :order => :name, :conditions => ['account_id = ?', 1]
Everything works correctly. How do I make my current_account method accessible to my model
Many Thanks
You can't access the session from models. Instead, pass the account as a parameter to a named scope.
#controller
Model.my_custom_find current_account
#model.rb
named_scope :my_custom_find, lambda { |account| { :order => :name, :conditions => ['account_id = ?', account.id]}}
I haven't used rails 3 yet so maybe named_scopes have changed.
The association setup is enough to deal with scoping on controller and view levels. I think the problem is to scope, for instance, finds in models.
In controller:
#products = current_account.products.all
Fine for view scoping, but...
In model:
class Inventory < ActiveRecord::Base
belongs_to :account # has fk account_id
has_many :inventory_items, :dependent => :destroy
has_many :products, :through => :inventory_items
end
class Product < ActiveRecord::Base
has_many :inventory_items
def level_in(inventory_id)
inventory_items.where(:inventory_id => inventory_id).size
end
def total_level
# Inventory.where(:account_id => ?????) <<<<<< ISSUE HERE!!!!
Inventory.sum { |inventory| level_in(inventory.id) }
end
end
How can this be scoped?
+1 for mark's answer. This should still work in Rails 3.
Just showing you the rails 3 way with scope and new query api:
scope :my_custom_find, lambda { |account| where(:account_id=>account.id).order(:name) }
You've got the association set up, so couldn't you just use:
#projects = current_account.projects.all
...in your controller?
I've not adjusted the syntax to be Rails 3 style as I'm still learning it myself.

Rails, ActiveRecord: how do I get the results of an association plus some condition?

I have two models, user and group. I also have a joining table groups_users.
I have an association in the group model:
has_many :groups_users
has_many :users, :through=> :groups_users
I would like to add pending_users which would be the same as the users association but contain some conditions. I wish to set it up as an association so that all the conditions are handled in the sql call. I know there's a way to have multiple accessors for the same model, even if the name is not related to what the table names actually are. Is it class_name?
Any help would be appreciated, thanks
Use named_scopes, they're your friend
Have you tried using a named_scope on the Group model?
Because everything is actually a proxy until you actually need the data,
you'll end up with a single query anyway if you do this:
class User < ActiveRecord::Base
named_scope :pending, :conditions => { :status => 'pending' }
and then:
a_group.users.pending
Confirmation
I ran the following code with an existing app of mine:
Feature.find(6).comments.published
It results in this query (ignoring the first query to get feature 6):
SELECT *
FROM `comments`
WHERE (`comments`.feature_id = 6)
AND ((`comments`.`status` = 'published') AND (`comments`.feature_id = 6))
ORDER BY created_at
And here's the relevant model code:
class Feature < ActiveRecord::Base
has_many :comments
class Comment < ActiveRecord::Base
belongs_to :feature
named_scope :published, :conditions => { :status => 'published' }
This should be pretty close - more on has_many.
has_many :pending_users,
:through => :groups_users,
:source => :users,
:conditions => {:pending => true}
:pending is probably called something else - however you determine your pending users. As a side note - usually when you see a user/group model the association is called membership.
In the User model:
named_scope :pending, :include => :groups_users, :conditions => ["group_users.pending = ?", true]
That's if you have a bool column named "pending" in the join table group_users.
Edit:
Btw, with this you can do stuff like:
Group.find(id).users.pending(:conditions => ["insert_sql_where_clause", arguments])

Resources