Rails CollectionProxy randomly inserts in wrong order - ruby-on-rails

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)

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 group_by with empty results

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 [].

Making a has_many relation avoiding childs to store paretn's id

class User
include Mongoid::Document
has_many :favorites, class_name: "Item"
end
class Item
include Mongoid::Document
belongs_to :user, dependent: :nullify
end
I want that the users have an array of favorites but in Item collection, the user_id is not stored. Is the approach I followed correct?
If I try to access a user favorites as User.last.favourites or try to add a favorite to a user, it takes for ever. Why is this?
Thanks
I believe you are missing the embedded_in :user on your Item class
class User
include Mongoid::Document
has_many :favorites, class_name: "Item"
end
class Item
# class is all lower case
include Mongoid::Document
# remove the relation to the user form the item
# so that it cannot save the user_id
end
the previous code should work, and won't allow you to save the user_id in a favorite.
so this code should work user.last.favorites #=> [Array of favorites]
but this code will through an exception user.last.favorites.last.user #=> method user not found
why it takes forever ?! I cannot judge unless i've seen the logs.
also don't go for the embedded solution, the embedded collection is only accessible from the parent... and i think this is not what you want to achieve...
in a simpler words: if you have a favorite that is called 'rails',
using the embedded solution described in the other answer would result in the following behaviour
p Favorites.all.to_a
#=> []
p user.first.favorites.first
#=> <Favorite id: 1, name: rails>
p user.last.favorites.first
#=> <Favorite id: 100, name: rails>
if you noticed:
the collections is not query-able except from the parent that embeds it.
the favorites collections is unique per user. 2 different users cannot share the same embedded document ( as in the previous code, both first and last user has the same favorite (what you intended) but they are actually 2 completely different objects.
the same code using the has_many relation would result in the following
p Favorites.all.to_a
#=> [<<Favorite id: 1, name: rails>]
p user.first.favorites.first
#=> <Favorite id: 1, name: rails>
p user.last.favorites.first
#=> <Favorite id: 1, name: rails>
# this is pseudocode but you will get the idea
user.last.favorites.first.name = `rails4` # then save
p user.first.favorites.first
#=> <Favorite id: 1, name: rails4>
p user.last.favorites.first
#=> <Favorite id: 1, name: rails4>

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)}

Mongoid - two update modifier in single call to update a document in mongodb

I am trying to experiment with mongodb, mongoid and rails. I have a simple Task and Comment model in Rails, where Comments are embedded into Tasks. Now Task has attribute called comment_count. Is there a way of increment the count as well as push a new comment together in a single call.
Task model:
class Task
include Mongoid::Document
field :name
field :desc
field :comment_count, type: Integer, default: 0
embeds_many :comments
end
Comment Model:
class Comment
include Mongoid::Document
field :entry
embedded_in :task
end
Below is the operation which I want to do in a single call.
1.9.3p194 :025 > task.comments.push(Comment.new(entry: "This is a comment"))
=> [#<Comment _id: 509e1708a490b3deed000003, _type: nil, entry: "First comment">, #<Comment _id: 509e1716a490b3deed000004, _type: nil, entry: "Second comment">, #<Comment _id: 509e1aa3a490b3deed000005, _type: nil, entry: "This is a comment">]
1.9.3p194 :026 > task.inc(:comment_count, 1)
=> 3
I actually intend to get a way of using multiple update modifiers like $inc, $push, $pop etc in a single update call. Similar to what we can do directly in mongo shell.
Please help.
Thanks
Unfortunately, Mongoid does not seem to support counter_cache as ActiveRecord does.
You could use an after_save and an after_destroy callback on your Comment model to implement this, respectively incrementing / decrementing the parent's counter.

Resources