Statically-typed, compile-time-checked programmer brain, here, struggling with a rails function call.
In a model when we specify has_many, we can specify the sort order like this has_many :requirements, -> { order(created_at: :asc) }, :dependent => :destroy.
But when we have a polymorphic association such as this
has_many :text_fields, as: :describable, :dependent => :destroy how can we add the sorting lambda?
I have tried every possible permutation of syntax except, apparently, the correct one.
You have clarified that it is the child records (text_fields) that you want ordered. You could use a default_scope, like so:
class ParentModel < ApplicationRecord
has_many :text_fields, as: :describable, :dependent => :destroy
end
class TextField < ApplicationRecord
default_scope { order(created_at: :asc) }
end
But think hard before doing so because default_scope is evil. Among other things, you can't override an order that is defined in a default_scope. I am not familiar with requirements, but you may not be able to override the order defined within requirements either.
I think you are much better served creating a scope like so:
class ParentModel < ApplicationRecord
has_many :text_fields, as: :describable, :dependent => :destroy
end
class TextField < ApplicationRecord
scope :default_order, -> { order(created_at: :asc) }
end
Then call it explicitly when you want your records ordered that way:
TextField.all.default_order
or
parent_record.text_fields.default_order
Related
I have a many-to-many / has-many-through relationship in my connecting my recipe model to my tag model such that:
class Tag < ActiveRecord::Base
has_many :taggings
has_many :recipes, through: :taggings
end
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :recipe
end
class Recipe < ActiveRecord::Base
has_many :taggings
has_many :tags, through: :taggings
end
...is there any way to filter for recipes with the same tag through a scope? I'm new to scopes, but I find them much more useful than methods, and I can only achieve searching and filtering by tag name through a method.
For example this will get me all recipes tagged with a given name:
def self.tagged_with(name)
Tag.find_by_name!(name).recipes
end
You can basically convert most association-method-chains (though not all*) into a scope
eg I'd try this (note: not tested for bugs) and see how it turned out
scope :tagged_with, ->(name) { find_by_name!(name).recipes }
If that doesn't work, I'd try something like:
scope :tagged_with, ->(name) { where(:name => name).first.recipes }
[*] The big issue with using scopes over method-chaining is that find/first can sometimes do weird things if it doesn't find one... the code in Rails literally defaults to the all scope in some cases (this is really weird behaviour that I think shouldn't happen) so for scopes that find just a single item, I will often not bother with a scope and use a class-method as you have originally.
I have three ActiveRecords.
Order, which has_many OrderItems. Also has a field named status.
OrderItem, which belongs_to an Order and a Script
Script, which has_many OrderItems
I want to set up the has_many in the Script model so that only OrderItems that belong to an Order which has a status of 'paid' are given.
I'm new to rails, and this isn't my codebase so I'm struggling a bit. I've seen other answers use the :include and :conditions keywords, but neither are valid in rails 4.2
Any help would be great.
I would try keeping things a bit pure by ensuring that the models do not get too involved in defining how other models behave.
Start by defining a scope for Order to define what you mean by "paid".
def self.has_been_paid
where(status: "Paid")
end
Then you can associate OrderItems with a paid Order.
belongs_to :paid_order, -> {merge(Order.has_been_paid)}, :class_name => "Order", :foreign_key => :order_id
... and a scope ...
def self.for_paid_orders
joins(:paid_order)
end
Then for the Script:
has_many :order_items, -> {merge(OrderItem.for_paid_orders)}
So now each model takes care of its own business.
Here's how to setup a has_many :order_items that only include items that belong to a paid order:
class Script < ActiveRecord::Base
has_many :order_items, -> {
includes(:order).where(orders: {status: 'paid'})
}
end
I'm working on my first rails project, I'm stuck trying to get two of my models to work together. Here are my models:
class Ecn < ActiveRecord::Base
has_many :revisions, :dependent => :destroy
has_many :drawings, through: :revisions
accepts_nested_attributes_for :revisions, :reject_if => lambda { |attrs| attrs.all? { |key, value| value.blank? }}, :allow_destroy => true
belongs_to :user
class Drawing < ActiveRecord::Base
has_many :revisions
class Revision < ActiveRecord::Base
belongs_to :drawing, foreign_key: :drawing_number
belongs_to :ecn
What I am trying to achieve is a search for Ecns that uses the revision foreign_key :drawing_number. When I create an Ecn, I add multiple revisions to the Ecn, which have a field for :drawing_number, which is a property of Drawings. I have an Ecn search form, which has multiple fields to search for, one of which is :drawing_number. I would like the query to find all Revisions that include the given :drawing_number, and find the Ecns that include those revisions.
The scope I have in my Ecn model looks like this:
scope :by_drawing_number, lambda { |drawing_number| Ecn.joins(:drawings).where("drawings.drawing_number LIKE ?", "%#{drawing_number}%") unless drawing_number.nil? }
This scope does not throw any errors, but the search comes up with zero results.
Following should be possible:
scope :by_drawing_number, lambda { |drawing_number| Revision.where("drawing_number LIKE ?", "%#{drawing_number}%").map(&:ecn)
This will return a collection of Ecns.
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
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