Rails group_by with empty results - ruby-on-rails

I have following models:
class Task
belongs_to :task_category
end
class TaskCategory
has_many :tasks
end
I want to group tasks by task category and this works for me:
Task.all.group_by(&:task_category)
# =>
{
#<TaskCategory id: 1, name: "call", ... } =>[#<Task id: 1, ...>, #<Task id: 2, ...>],
#<TaskCategory id: 2, name: "event", ... } =>[#<Task id: 3, ...>, #<Task id: 4, ...>]
}
The problem is I want all task categories returned even if the task collection is empty. Therefore, something like this would work:
#<TaskCategory id: 3, name: "todo", ... } =>[]
In this case, the task category has no tasks, so the value is an empty array. Does the group_by support an option to allow this? If not, can this be done elegantly in a one-liner?

TaskCategory.all.includes(:task) would work wouldn't it? The data you get back would be in a slightly different format, but not significantly so.

If you just do TaskCategory.all, you can get the tasks grouped by the category that you need. The format isn't exactly the same but still grouped the way you want it:
TaskCategory.all
# Assuming the first TaskCategory has no tasks
TaskCategory.all.first.tasks
=> #<ActiveRecord::Relation []>
A TaskCategory with no tasks would yield #<ActiveRecord::Relation []> which is somewhat equivalent to [].

Related

has_many with unsaved default in collection

I thought it would be a simple thing, but unfortunately I can't find a way to create a collection with a default entry.
There is the model Entry which can be filtered by one or more filter_entries.
There should be a fallback, when there is no association. Cause I dont't want to create a default association in the database for 100000s of entries.
Currently I made an array, which returns a default entry.
class Entry
belongs_to :object
has_many :entry_filters, dependent: :destroy
def entry_filters
super.presence || [EntryFilter.new(filter: default_filter)]
end
private
def default_entry
object.filters.find_by(default: true)
end
end
An entry with an association
Entry.first.entry_filters
=> [#<EntryFilter:0x00007f4b39cb16d8
id: 1,
entry_type: "Entry",
entry_id: 1,
filter_id: 1>]
Entry.first.entry_filters.class
=> EntrySegment::ActiveRecord_Associations_CollectionProxy
An entry without an association
Entry.second.entry_filters
=> [#<EntryFilter:0x000055e9060ac608
id: nil,
entry_type: nil,
entry_id: nil,
filter_id: 1>]
Entry.second.entry_filters.class
=> Array
It works, but I want to work with a collection, like when there is a real association linked to the entry.
Is there a way, using the ActiveRecord::Associations::HasManyAssociation to fake that collection?
I've tried several methods, but each will create thaht association instead of building it.
Entry.second.association(:entry_filters).writer([EntryFilter.new(filter: Filter.first)])
=> [#<EntryFilter:0x00007f90d0b38d88 id: 2, entry_type: "Entry", entry_id: 2, filter_id: 1>]
Entry.second.association(:entry_filters).replace([EntryFilter.new(filter: Filter.first)])
=> [#<EntryFilter:0x00007f90d0a457c8 id: 3, entry_type: "Entry", entry_id: 2, filter_id: 1>]
Entry.second.association(:entry_filters).concat([EntryFilter.new(filter: Filter.first)])
=> [#<EntryFilter:0x00007f90d0a637a5 id: 4, entry_type: "Entry", entry_id: 2, filter_id: 1>]
You can use the .build collection method to do what you want:
def entry_filters
filters = super
filters.build(filter: default_filter) if filters.blank?
filters
end
The .build method instantiates a new associated object (without saving), sets the foreign key, and adds it to the object's collection in memory. In ordinary usage you would call it like <entry_obj>.entry_filters.build(filter: ...). But, you can't do that inside .entry_filters itself or you end up with infinite recursion. Hence, we call super first to get the collection and then call .build if the collection is currently empty.
> Entry.new.entry_filters
=> #<ActiveRecord::Associations::CollectionProxy [#<EntryFilter id: nil,
filter_id: ... ]
When you call .save on the parent entry, it will also save the new entry filter at that point.

Rails - how to automatically uniq joins method?

This question is based on this: Rails, why joins returns array with non-uniq values?
Let say I get non uniq array by .joins() method:
City.joins(:locations)
# => [#<City id: 5, name: "moscow", created_at: "2010-07-02 15:09:16", updated_at: "2010-07-02 15:09:16">, #<City id: 5, name: "moscow", created_at: "2010-07-02 15:09:16", updated_at: "2010-07-02 15:09:16">, #<City id: 5, name: "moscow", created_at: "2010-07-02 15:09:16", updated_at: "2010-07-02 15:09:16">, #<City id: 5, name: "moscow", created_at: "2010-07-02 15:09:16", updated_at: "2010-07-02 15:09:16">]
I can make records uniq by using
City.joins(:locations).group('cities.id') # or simpler
City.joins(:locations).uniq
# => [#<City id: 5, name: "moscow", created_at: "2010-07-02 15:09:16", updated_at: "2010-07-02 15:09:16">]
How can I make .joins() method returns uniq records by default?
You could try overriding the .joins method for the models you need, but I would suggest just writing a scope, e.g.
scope :unique_locations, -> { joins(:locations).uniq }
Then just call City.unique_locations. It's cleaner and more readable that way.
Generally overwriting methods should be done only when you're sure you won't need it 'the old way', and it makes sense. Plus, when you say City.joins(:locations) the reader expects default behaviour, and returning something else will cause chaos and confusion.
You can define has_many macro, with the stubby lambda as an argument:
has_many :locations, -> { joins(:locations).uniq }
Also you can define own AR relation method, it stil use a simple has_many macro.
has_many :locations do
def only_uniq
joins(:locations).uniq
end
end
Now use it:
c = City.find(123)
c.locations.only_uniq
It does the same thing as scope or lambda in has_many.

Rails CollectionProxy randomly inserts in wrong order

I'm seeing some weird behaviour in my models, and was hoping someone could shed some light on the issue.
# user model
class User < ActiveRecord::Base
has_many :events
has_and_belongs_to_many :attended_events
def attend(event)
self.attended_events << event
end
end
# helper method in /spec-dir
def attend_events(host, guest)
host.events.each do |event|
guest.attend(event)
end
end
This, for some reason inserts the event with id 2 before the event with id 1, like so:
#<ActiveRecord::Associations::CollectionProxy [#<Event id: 2, name: "dummy-event", user_id: 1>, #<Event id: 1, name: "dummy-event", user_id: 1>
But, when I do something seemlingly random - like for instance change the attend_event method like so:
def attend_event(event)
self.attended_events << event
p self.attended_events # random puts statement
end
It gets inserted in the correct order.
#<ActiveRecord::Associations::CollectionProxy [#<Event id: 1, name: "dummy-event", user_id: 1>, #<Event id: 2, name: "dummy-event", user_id: 1>
What am I not getting here?
Unless you specify an order on the association, associations are unordered when they are retrieved from the database (the generated sql won't have an order clause so the database is free to return things in whatever order it wants)
You can specify an order by doing (rails 4.x upwards)
has_and_belongs_to_many :attended_events, scope: -> {order("something")}
or, on earlier versions
has_and_belongs_to_many :attended_events, :order => "something"
When you've just inserted the object you may see a different object - here you are probably seeing the loaded version of the association, which is just an array (wrapped by the proxy)

Deleting records from HABTM association

I'm trying to do something fairly simple. I have two models, User and Group. For simplicity's sake, let's say they look like this:
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
end
and
class Group < ActiveRecord::Base
has_and_belongs_to_many :users
end
Now, for some reason, I have a user that has the same group twice. In the Rails Console:
user = User.find(1000)
=> #<User id: 1000, first_name: "John", last_name: "Doe", active: true, created_at:
"2013-01-02 16:52:36", updated_at: "2013-06-17 16:21:09">
groups = user.groups
=> [#<Group id: 1, name: "student", is_active: true, created_at: "2012-12-24 15:08:59",
updated_at: "2012-12-24 15:08:59">, #<Group id: 1, name: "student", is_active: true,
created_at: "2012-12-24 15:08:59", updated_at: "2012-12-24 15:08:59">]
user.groups = groups.uniq
=> [#<Group id: 1, name: "student", is_active: true, created_at: "2012-12-24 15:08:59",
updated_at: "2012-12-24 15:08:59">]
user.save
=> true
And there is some SQL output that I've silenced. I would think that everything should be all set, but it's not. The groups aren't updated, and that user still has both. I could go into the join table and manually remove the duplicates, but that seems cludgy and gross and unnecessary. What am I doing wrong here?
I'm running Rails 3.2.11 and Ruby 1.9.3p392
Additional note: I've tried this many different ways, including using user.update_attributes, and using group_ids instead of the groups themselves, to no avail.
The reason this doesn't work is because ActiveRecord isn't handling the invalid state of duplicates in the habtm association (or any CollectionAssociation for that matter). Any ids not included in the newly assigned array are deleted - but there aren't any in this case. The relevant code:
# From lib/active_record/associations/collection_association.rb
def replace_records(new_target, original_target)
delete(target - new_target)
unless concat(new_target - target)
#target = original_target
raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
"new records could not be saved."
end
target
end
The 'targets' being passed around are Arrays of assigned records. Note the call to delete(target - new_target) is equivalent in your case to delete(user.groups - user.groups.uniq) which results in an empty Array passed (since comparison is based on the id attribute of each record).
Instead, you'll need to clear out the association and then reassign the single group again:
group = user.groups.first
user.groups.clear
user.groups << group
This might be a way to cleanup those duplicates (it handles any number of groups of duplicate associations):
user = User.find(1000)
user.groups << user.groups.group_by(&:id).values.find_all {|v| v.size > 1}.each {|duplicates| duplicates.uniq_by! {|obj| obj.id}}.flatten.each {|duplicate| user.groups.delete(duplicate)}

Ruby/Rails - Check If Child Id Exists Within Record of HABTM Relationship

I have a set of resources called Tasks and Posts and there are in a has_and_belongs_to_many (HABTM) relationship with each other.
There is also a join table connecting their values.
create_table 'posts_tasks', :id => false do |t|
t.column :post_id, :integer
t.column :task_id, :integer
end
So my question is how do I check to see if the id of a specific task exists within the array created from #post.tasks?
irb(main):011:0> #post = Post.find(1)
=> #<Post id: 2, comment: "blah blah", created_at: "2011-10-18 03:40:30", updated_at:
irb(main):012:0> #post.tasks
=> [#<Task id: 1, description: "Test 1", created_at: "2011-10-18 03:
22:05", updated_at: "2011-10-18 03:22:05">, #<Task id: 3, description: "Test 3",
created_at: "2011-10-18 03:22:21", updated_at: "2011-10-18 03:22:21
">]
So my question is whats the ruby way for writing does "#task = Task.find(2)" exist within #post.tasks and if so return true or false?
#post.tasks.where(:id => task_id).present?
Is much lighter compared to what Gabelsman has suggested.
#post.tasks.map(&:id).include?(task_id)
Where task_id is the id of the task you want to check. To explain this a little bit, you're taking the list of tasks, mapping them to an array of their ids, and then checking to see if the id in question is in that array. If you're not familiar with map you should check it out, it's awesome.
ruby-docs
Assuming you named your has_and_belongs_to_many to :posts then doing something like this in Rails is very optimized:
#post.task_ids.include?(task.id)
I noticed that the two answer provided here result in n select statements. But doing it with task_ids caches the first query and the rest does not require a SELECT statement from the DB.
You can use exists? method for that:
#post.tasks.exists?(id: task_id)

Resources