Dirty Tracking of embedded document on the parent doc in Mongoid - mongoid

I had to track the dirty objects. And it works fine with the parent
doc. But when I change the embedded or referenced in doc, the dirty
has to be accessed via the embedded/referenced in doc itself.
How can I track the dirty on the parent doc itself??

I have put together mongoid extension to solve this problem https://github.com/versative/mongoid_relations_dirty_tracking. It simply tracks changes on document relations in the similar way as mongoid do it for attributes so the timestamps and versions are updated correctly.

I don't think Mongoid's own dirty attributes feature tracks changes to embedded documents. But it's not hard to implement basic dirty tracking of embedded documents yourself with something like this:
class Bar
include Mongoid::Document
embeds_many :foos
attr_accessor :foos_changed
alias_method :foos_changed?, :foos_changed
# ...
end
class Foo
include Mongoid::Document
embedded_in :bar, :inverse_of => :foos
after_save :notify_parent
def notify_parent
bar.foos_changed = true
end
# ...
end
b = Bar.create
b.foos_changed? # => nil
b.foos.create
b.foos_changed? # => true
If you need more options, like including embedded documents changes in Bar.changes or tracking the specific changes, you'll have to override the changes and changed? methods, which I wouldn't do unless you absolutely need those features.
Here's a way to track what changed in the embedded documents:
class Bar
include Mongoid::Document
embeds_many :foos
attr_writer :embedded_changes
def embedded_changes
#embedded_changes ||= begin
self._children.inject({}) do |memo, child|
memo.merge(
{child.collection_name => {child.id => child.changes}}
) if child.changed?
end
end
end
def changes
original_value = super
if original_value.blank?
embedded_changes
else
original_value.merge(embedded_changes)
end
end
def changed?
super || self._children.any?(&:changed?)
end
# ...
end
class Foo
include Mongoid::Document
embedded_in :bar, :inverse_of => :foos
# ...
field :hat
end
b = Bar.create
f = b.foos.create
b.changes # => nil
b.changed? # => false
f.hat = 1
f.changes # => {"hat"=>[nil, "1"]}
b.changes # => {"foos"=>{BSON::ObjectId('4cf...')=>{"hat"=>[nil, "1"]}}}
b.changed? # => true

Related

Can't get my strong parameters nested correct in Ruby on Rails

I'm trying to add an array of objects (tasks in this case) as a return type in my controller for the root object (project). One project has many tasks and I'd like to save it all at once, but I keep getting the following error indicating that the return type isn't correct.
ActiveRecord::AssociationTypeMismatch (Task(#47457277775360) expected, got {"name"=>"Some task", "start"=>"2019-12-05T03:38:48.555Z", "end"=>"2019-12-14T03:38:48.555Z"} which is an instance of ActiveSupport::HashWithIndifferentAccess(#47457266882220))
The whitelisted parameters I have are like this
# whitelist params
params.permit(:name, :description, tasks: [:name, :start, :end])
The data being returned for the above example:
{"name"=>"asdf", "description"=>"zxvccxvzzxcvxcvcxvzxcvz", "tasks"=>[{"start"=>"2019-12-05T03:38:48.555Z", "end"=>"2019-12-14T03:38:48.555Z", "name"=>"Some task"}]}
[Edit] - Here are the models we're working with
# app/models/task.rb
class Task < ApplicationRecord
# model association
belongs_to :project
# validation
validates_presence_of :name
end
# app/models/project.rb
class Project < ApplicationRecord
# model association
has_many :tasks, dependent: :destroy
accepts_nested_attributes_for :tasks
# validations
validates_presence_of :name
end
As per rails documentation Nested attributes allow you to save attributes on associated records through the parent. So in your strong params you need to pass attributes like this.
params.permit(:name, :description, task_attributes: [:name, :start, :end])
I suggest you to bind all params under one attribute like this
params.require(:project).permit(:name, :description, task_attributes: [:name, :start, :end])
so you must send params from frontend like
{"project": {"name"=>"asdf", "description"=>"zxvccxvzzxcvxcvcxvzxcvz", "task_attributes"=>[{"start"=>"2019-12-05T03:38:48.555Z", "end"=>"2019-12-14T03:38:48.555Z", "name"=>"Some task"}]}}
You can read the documentation from https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

How to find all document by association Mongoid 4

I have a model Tag which potentially belongs to several other models, but at the moment only one model Todo which in turn belongs to User like so:
class User
include Mongoid::Document
field: name, type: String
has_many :todos
end
class Todo
include Mongoid::Document
field: name, type: String
belongs_to :user
end
class Tag
include Mongoid::Document
field: name, type: String
belongs_to :todos
end
How can I query all Tags that belongs to a particular user? I've written the following:
todo_ids = Todo.where(user_id: '86876876787')
and then:
tags = Tag.where('todo_id.in': todo_ids)
But those didn't work. What am I missing?
You're missing two things:
Mongoid isn't ActiveRecord so it won't know what to do with todo_ids in the Tag query.
'todo_id.in' is a field path that is trying to look at the in field inside a todo_id hash, this isn't a use of MongoDB's $in operator.
You can only work with one collection at a time so to fix the first one, you need to pull an array of IDs out of MongoDB:
todo_ids = Todo.where(user_id: '86876876787').pluck(:id)
# -------------------------------------------^^^^^^^^^^^
To fix the second one, use the $in operator:
tags = Tag.where(todo_id: { '$in': todo_ids })
tags = Tag.where(:todo_id.in => todo_ids)
tags = Tag.in(todo_id: todo_ids)
#...

Check for non empty array using Rails 4+ and PostgreSQL arrays

I'm referring to this blog on how to use postgresql arrays in Rails 4. To be more precise, I'm using Rails 4.1.8.
I would like to enforce that the array should not be empty(should have at least one or more values) wrt the below table definition for the phones attribute.
Setting null: false as done below prevents me from doing a Contacts.create(phones: nil), but Contacts.create(phones: []) saves to the database.
class CreateContacts < ActiveRecord::Migration
def change
create_table :contacts do |t|
t.string :name
t.text :phones, array: true, null: false
end
end
end
What constraint can I use to ensure that Contacts.create(phones: []) raises an error saying that there should be at least one entry within the array?
A regular presence validation should meet your requirement, since the empty array is not present?.
class Contact < ActiveRecord::Base
validates :phones, presence: true
end
I suggest you use a serialized attribute instead.
class CreateContacts < ActiveRecord::Migration
def change
create_table :contacts do |t|
t.string :name
t.text :phones, null: false
end
end
end
And in your model:
class Contact < ActiveRecord::Base
serialize :phones
validate :phones, presence: true
before_validation :clean_empty_array
private
def clean_empty_array
self.phones = phones.presence
end
end
Note that I added a before_validation cleaning method, to turn empty arrays into nil using presence ([].presence => nil; ["1"].presence => ["1"]). As an alternative, you could use attribute_normalizer gem to handle this kind of data normalization if you have this requirement in more places of your application.
EDIT: after discussion in the comments, serialize seems to be not the best way to go, since it carries important caveats such as querying issues. Thanks to #muistooshort for pointing these out. Also, the before_validation is not needed, since a presence: true validation is equivalent to what was proposed in the before_validation.
So the final solution to the question would involve keeping Postgresql array support and just validating against phones presence.
In short: keep array: true in your migration file and add this validation to your model:
class Contact < ActiveRecord::Base
validate :phones, presence: true
end

Mongoid 3.1 eager loading, json, and field names

Recently updated to Mongoid 3.1 from 3.0.3 and this resulted in some broken code and confusion on my side.
Say you have a pair of classes with a belongs_to/has_many relationship, like so:
class Band
include Mongoid::Document
field :name, type: String
has_many :members, :autosave => true
end
class Member
include Mongoid::Document
field :name, type: String
belongs_to :band
end
Saving all this to the database like so:
b = Band.new
b.name = "Sonny and Cher"
b.members << Member.new(name: "Sonny")
b.members << Member.new(name: "Cher")
b.save
I would in my API, be able to return a 'member' object like so:
m = Member.where(name: "Sonny").first
m.to_json
which yields the following, as expected:
{"_id":"<removed>","band_id":"5151d89f5dd99dd9ec000002","name":"Sonny"}
My client can request the full band object with a subsequent call if it wants to. However, in some cases I DO want to include the referenced item directly. With 3.0.3, I would just do the following:
m = Member.where(name: "Sonny").first
m[:band] = m.band
m.to_json
and this would add a new field with the full band information to it. With 3.1, however (it may have started in earlier versions, but I didn't test), I now get this:
{"_id":"<removed>","band_id":{"_id":"5151dc025dd99d579e000002","name":"Sonny and Cher"},"name":"Sonny"}
So, it looks like the band info has been eager-loaded into the field? Why is it stored under the key ':band_id' and not ':band'? I guess ':band' is protected, but I still don't think the data should be stored under the ':band_id' key. I suspect I am missing something here. Any ideas?
You can specify an :include option for to_json like so:
m.to_json(include: :band)
The JSON will then have a key band with the Band object converted to JSON and band_id will still be present.

nested form, embedded docs, mongoid 2.2.0 problem

I cant seem to find answer for this here or with the google, any help would be awesome.
The Building saves correctly, but the embedded doc PriorityArea doesnt get updated...
I want to eventually have it ajax a new form for new priority areas evenutally, but need it to update first.
Mongoid::Errors::InvalidFind in BuildingsController#update
Calling Document#find with nil is invalid
class Building
include Mongoid::Document
embeds_many :priority_areas
accepts_nested_attributes_for :priority_areas, :allow_destroy => true, :autosave => true
end
class PriorityArea
include Mongoid::Document
embedded_in :building, :inverse_of => :priority_areas
end
#view
= form_for [#customer, #building] do |f|
...
...
= f.fields_for :priority_areas do |pa|
= f.name
...
...
#controller
#building.update_attributes(params[:building])
It correctly yeilds the correct data from the db, but fails to error above on building#update. Any help is greatly appreciated.
update
in the building#update im
puts params[:building][:priority_areas_attributes].to_yaml
which yeilds
--- !map:ActiveSupport::HashWithIndifferentAccess
"0": !map:ActiveSupport::HashWithIndifferentAccess
name: area 51
location: near front door
notes: ""
priority: "1"
id: ""
"1": !map:ActiveSupport::HashWithIndifferentAccess
name: area 52
location: near rear door
notes: ""
priority: "2"
id: ""
im guessing the problem is the null id:""
The problem was the null id
it needed to have an ObjectId to work correctly. stupid error on my part.
I encountered the exact problem. simple_form was automatically passing an id parameter to my controller, but it was blank.
Why was the id for my embedded document blank? I'm guessing it's because I imported the parent document via mongoimport. If I manually generate a parent document via web forms, then the embedded documents have IDs as expected.
Here was my workaround:
class Foo
include Mongoid::Document
embeds_many :bars
accepts_nested_attributes_for :bars
####
# simple_form_for / embedded document workaround
#
# Because simple_form wants to provide the ID for an existing object,
# it will output a blank ID because imported embedded documents
# have an ID of nil.
#
# Intercept it to avoid
# Mongoid::Errors::InvalidFind in FoosController#update
def bars_attributes=(attribs)
attribs.each do |key, value|
index = key.to_i
fixed_attrib = value.delete_if { |k,v| k=="id" and v=="" }
self.bars[index].update_attributes(fixed_attrib)
end
end
end
class Bar
include Mongoid::Document
embedded_in :foo
end

Resources