Rails order by in associated model - ruby-on-rails

I have two models in a has_many relationship such that Log has_many Items. Rails then nicely sets up things like: some_log.items which returns all of the associated items to some_log. If I wanted to order these items based on a different field in the Items model is there a way to do this through a similar construct, or does one have to break down into something like:
Item.find_by_log_id(:all,some_log.id => "some_col DESC")

There are multiple ways to do this:
If you want all calls to that association to be ordered that way, you can specify the ordering when you create the association, as follows:
class Log < ActiveRecord::Base
has_many :items, :order => "some_col DESC"
end
You could also do this with a named_scope, which would allow that ordering to be easily specified any time Item is accessed:
class Item < ActiveRecord::Base
named_scope :ordered, :order => "some_col DESC"
end
class Log < ActiveRecord::Base
has_many :items
end
log.items # uses the default ordering
log.items.ordered # uses the "some_col DESC" ordering
If you always want the items to be ordered in the same way by default, you can use the (new in Rails 2.3) default_scope method, as follows:
class Item < ActiveRecord::Base
default_scope :order => "some_col DESC"
end

rails 4.2.20 syntax requires calling with a block:
class Item < ActiveRecord::Base
default_scope { order('some_col DESC') }
end
This can also be written with an alternate syntax:
default_scope { order(some_col: :desc) }

Either of these should work:
Item.all(:conditions => {:log_id => some_log.id}, :order => "some_col DESC")
some_log.items.all(:order => "some_col DESC")

set default_scope in your model class
class Item < ActiveRecord::Base
default_scope :order => "some_col DESC"
end
This will work

order by direct relationship has_many :model
is answered here by Aaron
order by joined relationship has_many :modelable, through: :model
class Tournament
has_many :games # this is a join table
has_many :teams, through: :games
# order by :name, assuming team has this column
def teams
super.order(:name)
end
end
Tournament.first.teams # are returned ordered by name

For anyone coming across this question using more recent versions of Rails, the second argument to has_many has been an optional scope since Rails 4.0.2. Examples from the docs (see scopes and options examples) include:
has_many :comments, -> { where(author_id: 1) }
has_many :employees, -> { joins(:address) }
has_many :posts, ->(blog) { where("max_post_length > ?", blog.max_post_length) }
has_many :comments, -> { order("posted_on") }
has_many :comments, -> { includes(:author) }
has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
has_many :tracks, -> { order("position") }, dependent: :destroy
As previously answered, you can also pass a block to has_many. "This is useful for adding new finders, creators and other factory-type methods to be used as part of the association." (same reference - see Extensions).
The example given there is:
has_many :employees do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by(first_name: first_name, last_name: last_name)
end
end
In more modern Rails versions the OP's example could be written:
class Log < ApplicationRecord
has_many :items, -> { order(some_col: :desc) }
end
Keep in mind this has all the downsides of default scopes so you may prefer to add this as a separate method:
class Log < ApplicationRecord
has_many :items
def reverse_chronological_items
self.items.order(date: :desc)
end
end

Related

rails polymorphic with includes based on type of class

Let's say we have these models
class Message
belongs_to :messageable, polymorphic: true
end
class Ticket
has_many :messages, as: :messageable
has_many :comments
end
class User
has_many :messages, as: :messageable
has_many :ratings
end
class Rating
belongs_to :user
end
class Comment
belongs_to :ticket
end
Now I want to load all messages (which have associated tickets or users), and eager load depending on the type of class, either comments for tickets and ratings for users
Of course Message.includes(:messageable).order("created_at desc") will only include the directly associated object, but the question would be how to include the different association types that derive from each model type (i.e. in this example, how to eager load comments for tickets and ratings for users)?
This is just a simple example, but what about even more complicated cases, where I'd like to include something else for the user, another association, and what if that association needs more includes?
The only way I can think of to do this is to duplicate the associations on each model with a common name:
class Ticket
has_many :messages, as: :messageable
has_many :comments
has_many :messageable_includes, class_name: "Comment"
end
class User
has_many :messages, as: :messageable
has_many :ratings
has_many :messageable_includes, class_name: "Rating"
end
Message.includes(:messageable => :messageable_includes) ...
I'm not sure I would use this strategy as a widespread solution, but if this is a complicated as your case gets, it may work for you.
I've used the following helper methods in my own project:
def polymorphic_association_includes(association, includes_association_name, includes_by_type)
includes_by_type.each_pair do |includes_association_type, includes|
polymorphic_association_includes_for_type(association, includes_association_name, includes_association_type, includes)
end
end
def polymorphic_association_includes_for_type(association, includes_association_name, includes_association_type, includes)
id_attr = "#{includes_association_name}_id"
type_attr = "#{includes_association_name}_type"
items = association.select {|item| item[type_attr] == includes_association_type.to_s }
item_ids = items.map {|item| item[id_attr] }
items_with_includes = includes_association_type.where(id: item_ids).includes(includes).index_by(&:id)
items.each do |parent|
parent.send("#{includes_association_name}=", items_with_includes[parent[id_attr]])
end
end
These would allow you to say:
messages = Message.all
polymorhpic_association_includes messages, :messageable, {
Ticket => :comments,
User => :ratings
}
Not a particularly fluent interface but it works in general.
Place the includes on a default scope for each model:
class Ticket
has_many :messages, as: :messageable
has_many :comments
default_scope -> { includes(:comments).order('id DESC') }
end
class User
has_many :messages, as: :messageable
has_many :ratings
default_scope -> { includes(:ratings).order('id DESC') }
end
Then whenever you call Message.all each polymorphic association will include it's own resources.
Also if you need to call the class without the scope just use unscoped or create a different scope:
class Ticket
has_many :messages, as: :messageable
has_many :comments
has_many :watchers
default_scope -> { includes(:comments).order('id DESC') }
scope :watched -> {includes(:watchers)}
end
Ticket.unscoped.all # without comments or watchers (or order)
Ticket.watched.all # includes watchers only

Relation Conditions with Variables

I have the following models:
class Publication < ActiveRecord::Base
has_many :reviews
has_many :users, :through => :owned_publications
has_many :owned_publications
end
class User < ActiveRecord::Base
has_many :publications, :through => :owned_publications
has_many :owned_publications
end
class OwnedPublication < ActiveRecord::Base
belongs_to :publication
belongs_to :user
has_one :review, :conditions => "user_id = #{self.user.id} AND publication_id = #{self.publication.id}"
end
In the third model, I'm trying to set a condition with a pair of variables. It seems like the syntax works, except that self is not an instance of OwnedPublication. Is it possible to get the current instance of OwnedPublication and place it into a condition?
The solution requires the use of :through and :source options, as well as a proc call:
has_one :review, :through => :publication, :source => :reviews,
:conditions => proc { ["user_id = ?", self.user_id] }
Proc is the trick to passing in dynamic variables to ActiveRecord association conditions, at least as of Rails 3.0. Simply calling:
has_one :conditions => proc { ["publication_id = ? AND user_id = ?",
self.publication_id, self.user_id] }
will not work, though. This is because the association will end up searching the reviews table for a 'reviews.owned_publication_id' column, which does not exist. Instead, you can find the proper review through publication, using publication's :reviews association as the source.
I think your best bet is to just have the Review record belong_to an OwnedPublication, and setup your Publication model to get the reviews via a method:
def reviews
review_objects = []
owned_publications.each do |op|
review_objects << op
end
review_objects
end
Might be a more efficient way if you use a subquery to get the information, but it removes the concept of having unnecessary associations.

Reusing activerecord scopes on has many through associations

Say I have a few activerecord models in my rails 3.1 project that look like this:
class Component < ActiveRecord::Base
has_many :bugs
end
class Bug < ActiveRecord::Base
belongs_to :component
belongs_to :project
scope :open, where(:open => true)
scope :closed, where(:open => false)
end
class Project < ActiveRecord::Base
has_many :bugs
has_many :components_with_bugs, :through => :bugs, :conditions => ["bugs.open = ?", true]
end
In Short: I have a has_many through association (components_with_bugs) where I want to scope the "through" model. At present I'm doing this by duplicating the code for the scope.
Is there any way to define this has many through association (components_with_bugs) such that I can reuse the Bug.open scope on the through model, while still loading the components in a single database query? (I'm imagining something like :conditions => Bug.open)
Rails 4 answer
Given you have:
class Component < ActiveRecord::Base
has_many :bugs
end
class Bug < ActiveRecord::Base
belongs_to :component
belongs_to :project
scope :open, ->{ where( open: true) }
scope :closed, ->{ where( open: false) }
end
You have two possibilities:
class Project < ActiveRecord::Base
has_many :bugs
# you can use an explicitly named scope
has_many :components_with_bugs, -> { merge( Bug.open ) }, through: :bugs, source: 'component'
# or you can define an association extension method
has_many :components, through: :bugs do
def with_open_bugs
merge( Bug.open )
end
end
end
Calling projet.components_with_bugs or project.components.with_open_bugs will fire the same sql query:
SELECT "components".* FROM "components"
INNER JOIN "bugs" ON "components"."id" = "bugs"."component_id"
WHERE "bugs"."project_id" = ? AND "bugs"."open" = 't' [["project_id", 1]]
Which one is better to use depends on your application. But if you need to use many scopes on the same association, I guess association extensions could be clearer.
The real magic is done with merge which allows you to, as the name says, merge conditions of another ActiveRecord::Relation. In this case, it is responsible for adding AND "bugs"."open" = 't' in the sql query.
Apart from your scopes , write the default scope as:
default_scope where(:open => true) in your "through" model Bug.
class Bug < ActiveRecord::Base
belongs_to :component
belongs_to :project
default_scope where(:open => true)
scope :open, where(:open => true)
scope :closed, where(:open => false)
end
And in the Project model remove :conditions => ["bugs.open = ?", true]
class Project < ActiveRecord::Base
has_many :bugs
has_many :components_with_bugs, :through => :bugs
end
I think the above will work for you.
Try using the following.
has_many :components_with_bugs, :through => :bugs do
Bug.open
end
Can't you use something like this ?
has_many :components_with_bugs, :through => :bugs, :conditions => Bug.open.where_values
I haven't tested it, just proposing an path for investigation
The http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html specifies
:conditions Specify the conditions that the associated object must meet in order to be included as a WHERE SQL fragment, such as authorized = 1.
Hence you can do it as:
class Project < ActiveRecord::Base
has_many :bugs
has_many :components_with_bugs, :through => :bugs do
def open
where("bugs.open = ?", true)
end
end
end
EDIT:
You can't specify another model's scope as a condition. In your case, they way you have it implemented is right. You can implement it another way as
has_many :components_with_bugs, :through => :bugs # in this case, no need to use the relation.
def open_bugs
self.bugs.openn # openn is the scope in bug. Don't use name 'open'. It's a private method of Array.
end

Using default_scope in a model, to filter by the correct instanceID

I have two tables:
books (id, name, desc, instance_id)
instances (id, domain)
A user should ONLY be able to see data that is assigned to their instance_id in records...
For the books, model, to accomplish this, I'm thinking about using a default scope.. Something like:
class Books < ActiveRecord::Base
attr_accessible :name, :description
belongs_to :user
default_scope :order => 'books.created_at DESC'
AND books.instance_id == current.user.instance_id
end
Any thoughts on that idea? Also how can I write that 2nd to last line for Rails 3? 'AND books.instance_id == current.user.instance_id'
Thanks
It's not a good idea to access the current user inside the model. I would implement this as follows:
class Instance < ActiveRecord::Base
has_many :users
has_many :books
end
class User < ActiveRecord::Base
belongs_to :instance
has_many :books, :order => "created_at DESC"
has_many :instance_books, :through => :instance, :source => :books,
:order => "created_at DESC"
end
class Book < ActiveRecord::Base
belongs_to :user
belongs_to :instance
end
List of Books associated with the user instance:
current_user.instance_books
List of Books created by the user:
current_user.books
Creating a new book:
current_user.books.create(:instance => current_user.instance, ..)
Note:
Your book creation syntax is wrong. The build method takes hash as parameter. You are passing two arguments instead of one.
user.books.build(params[:book].merge(:instance => current_user.instance}))
OR
user.books.build(params[:book].merge(:instance_id => current_user.instance_id}))

How do I automatically sort a has_many relationship in Rails?

This seems like a really simple question but I haven't seen it answered anywhere.
In rails if you have:
class Article < ActiveRecord::Base
has_many :comments
end
class Comments < ActiveRecord::Base
belongs_to :article
end
Why can't you order the comments with something like this:
#article.comments(:order=>"created_at DESC")
Named scope works if you need to reference it a lot and even people do stuff like this:
#article.comments.sort { |x,y| x.created_at <=> y.created_at }
But something tells me it should be simpler. What am I missing?
You can specify the sort order for the bare collection with an option on has_many itself:
class Article < ActiveRecord::Base
has_many :comments, :order => 'created_at DESC'
end
class Comment < ActiveRecord::Base
belongs_to :article
end
Or, if you want a simple, non-database method of sorting, use sort_by:
article.comments.sort_by &:created_at
Collecting this with the ActiveRecord-added methods of ordering:
article.comments.find(:all, :order => 'created_at DESC')
article.comments.all(:order => 'created_at DESC')
Your mileage may vary: the performance characteristics of the above solutions will change wildly depending on how you're fetching data in the first place and which Ruby you're using to run your app.
As of Rails 4, you would do:
class Article < ActiveRecord::Base
has_many :comments, -> { order(created_at: :desc) }
end
class Comment < ActiveRecord::Base
belongs_to :article
end
For a has_many :through relationship the argument order matters (it has to be second):
class Article
has_many :comments, -> { order('postables.sort' :desc) },
:through => :postable
end
If you will always want to access comments in the same order no matter the context you could also do this via default_scope within Comment like:
class Comment < ActiveRecord::Base
belongs_to :article
default_scope { order(created_at: :desc) }
end
However this can be problematic for the reasons discussed in this question.
Before Rails 4 you could specify order as a key on the relationship, like:
class Article < ActiveRecord::Base
has_many :comments, :order => 'created_at DESC'
end
As Jim mentioned you can also use sort_by after you have fetched results although in any result sets of size this will be significantly slower (and use a lot more memory) than doing your ordering through SQL/ActiveRecord.
If you are doing something where adding a default order is cumbersome for some reason or you want to override your default in certain cases, it is trivial to specify it in the fetching action itself:
sorted = article.comments.order('created_at').all
If you are using Rails 2.3 and want to use the same default ordering for all collections of this object you can use default_scope to order your collection.
class Student < ActiveRecord::Base
belongs_to :class
default_scope :order => 'name'
end
Then if you call
#students = #class.students
They will be ordered as per your default_scope. TBH in a very general sense ordering is the only really good use of default scopes.
You can use ActiveRecord's find method to get your objects and sort them too.
#article.comments.find(:all, :order => "created_at DESC")
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
And if you need to pass some additional arguments like dependent: :destroy or whatever, you should append the ones after a lambda, like this:
class Article < ActiveRecord::Base
has_many :comments, -> { order(created_at: :desc) }, dependent: :destroy
end

Resources