Mongoid Association Creating (unwanted) Records - ruby-on-rails

I'm at a loss to why Mongoid is creating a new record in an association. I'm stepping closely through the code, but I've never seen anything like this. I've made a test and slimmed down the code. I left the VCR in just in case it might be related.
it "should not create a duplicate entry for MT" do
state = PolcoGroup.create(type: :state, name: 'MT', active: true)
s = state.get_senators
state.junior_senator = s[:state_junior_senator] # !!!!! this creates a new record
state.senior_senator = s[:state_senior_senator] # !!!!! so does this line
expect(Legislator.all.size).to eql(2) # actually equals 4 -- each association creates a new record
end
result is:
Legislator.all.map(&:sortname)
=> ["Tester, Jon (Sen.) [D-MT]", "Walsh, John (Sen.) [D-MT]", "Walsh, John (Sen.) [D-MT]", "Tester, Jon (Sen.) [D-MT]"]
## models
class PolcoGroup
include Mongoid::Document
include Mongoid::Timestamps
include VotingMethods
include DistrictMethods
extend DistrictClassMethods
include StateMethods
field :name, :type => String
...
# STATE RELATIONSHIPS -----------------------------
has_one :junior_senator, class_name: "Legislator", inverse_of: :jr_legislator_state
has_one :senior_senator, class_name: "Legislator", inverse_of: :sr_legislator_state
...
end
class Legislator
include Mongoid::Document
include Mongoid::Timestamps
# the following fields are directly from govtrack
field :govtrack_id, type: Integer
field :bioguideid, type: String
...
belongs_to :jr_legislator_state, class_name: "PolcoGroup", inverse_of: :junior_senator
belongs_to :sr_legislator_state, class_name: "PolcoGroup", inverse_of: :senior_senator
...
end
module StateMethods
def get_senators
...
# just returns the following
{state_senior_senator: senators.first, state_junior_senator: senators.last}
end
end
You can see more code here: https://gist.github.com/tbbooher/d892f5c234053990da70

OK -- never do what I did. I was pulling in an old version of mongo as a test database and then conducting the above. Of course it wasn't working correctly.

Related

Model embeds_many and embedded_in relationship using factorygirls

I want to write FactoryGirl class for creating company. The models are as follows:
module Company
class Contact
include Mongoid::Document
include ActiveModel::Validations
embedded_in :company, class_name: "::Company::Contact"
end
end
module Company
class Company
require 'autoinc'
embeds_many :contacts, class_name: "::Company::Contact"
end
end
FactoryGirl.define do
factory :company, :class => 'Company::Company' do
name { Faker::Company.name }
after(:create) do |company|
# company.contacts << create(:company_contact)
create_list(:company_contact, 1, company: company)
end
# contacts { [ build(:company_contact) ] }
end
end
The error received is
Failure/Error: create_list(:company_contact, 1, company: company)
Mongoid::Errors::InvalidPath:
message:
Having a root path assigned for Company::Contact is invalid.
summary:
Mongoid has two different path objects for determining the location of a document in the database, Root and Embedded. This error is raised when an embedded document somehow gets a root path assigned.
resolution:
Most likely your embedded model, Company::Contact is also referenced via a has_many from a root document in another collection. Double check the relation definitions and fix any instances where embedded documents are improperly referenced from other collections
How should I handle this? I cannot change the models.
You have the association class name wrong:
module Company
class Contact
include Mongoid::Document
include ActiveModel::Validations
embedded_in :company, class_name: 'Company::Company'
end
end

Mongoid Polymorphic Association Rails

Work env: Rails 4.2 mongoid 5.1
Below are my models:
class Tag
include Mongoid::Document
include Mongoid::Timestamps
field :name, type: String
belongs_to :entity_tags, :polymorphic => true
end
class EntityTag
include Mongoid::Document
include Mongoid::Timestamps
field :tag_id, type: String
field :entity_id, type: String // Entity could be Look or Article
field :entity_type, type: String // Entity could be Look or Article
field :score, type: Float
end
class Look
include Mongoid::Document
include Mongoid::Timestamps
has_many :tags, :as => :entity_tags
end
class Article
include Mongoid::Document
include Mongoid::Timestamps
has_many :tags, :as => :entity_tags
end
We are trying to implement polymorphic functionality between Looks and Articles to Tags.
i.e. Let's say we have a Tag named "politics", and we would like to add the tag to an Article with the score '0.9' and to a Look with the score '0.6'. The Score should be saved at the EntityTags Model.
The problem:
The first assign of the tag works, but then when I try to assign the same tag to another entity, it removes it and reassigns it from the first one to the latter.
The assignment looks like the following:
entity.tags << tag
Does anybody know the proper way to save associations and create the EntityTag Object with the correct polymorphism and assignment properly?
Thanks!
I've managed to implement a non-elegant working solution based on the following answer in this link

Skip default scope for relation in mongoid

How can I skip the default scope for relations in mongoid?
The trashable concern implements a soft-delete on the model, also it adds the following
field :d_at, type: DateTime
default_scope -> { where(d_at: nil) }
If a brand gets trashed I still want to have it available when I load a product that has a reference to that brand
These are the model definitions
class Product
include Mongoid::Document
field :title, type: String
belongs_to :brand, class_name: 'Brand'
end
class Brand
include Mongoid::Document
include Concerns::Trashable
field :title, type: String
end
Example:
product = Product.find([id])
puts product.brand.inspect #This brand is soft-deleted and not fetched because of the default scope
This works, but it breaks more then it fixes
class Product
include Mongoid::Document
field :title, type: String
belongs_to :brand, class_name: 'Brand'
#unscope relation to brand
def brand
Brand.unscoped.find(self.brand_id)
end
end
According to the the fix Support unscoping default_scope in eager_loaded associations, you can skip the default scope manually by specifying the columns to be ignored in your association.
-> { unscope(where: :column_name) }
Or you may use unscoped_associations.

Unusual Rails Mongoid case

Previously we had has_one and belongs_to relation with our models:
class Task
include Mongoid::Document
include Mongoid::Timestamps
has_one :output
end
class Output
include Mongoid::Document
include Mongoid::Timestamps
belongs_to :task
end
But we now plan to embed output inside task.
class Task
include Mongoid::Document
include Mongoid::Timestamps
embeds_one :output
end
class Output
include Mongoid::Document
include Mongoid::Timestamps
embedded_in :task
end
Everything works fine but we want to make backward compatible. ie. we want those output also which were created before embed.
Then, we did this method in task.rb:
def output
Task.collection.find(_id: Moped::BSON::ObjectId(self.id)).first.output || Output.collection.find(task_id: Moped::BSON::ObjectId(self.id)).first
end
The problem with this is now task.output will give json instead of output object.
so we cannot do
task = Task.new
output = task.create_output
output.task #=> not possible
Anyone having this scenario or any directions for this case.
Well, instead of making a workaround, why dont we migrate the old ones?
First, change both models to embed by replacing has_one with embeds_one and replacing belongs_to with embedded_in. Save the code.
Then use your rails console (>> rails console)
Then
Output.each do |o|
if !o.task_id.nil?
#change to embedded format
t=Task.find(o.task_id)
t.output=o
t.output.task_id=nil
t.save
end
end

How to migrate from belongs_to, to embedded_in in Mongoid?

If one first build their models with a belong_to and has_many association and then realized they need to move to a embedded_in and embeds_many association, how would one do this without invalidating thousands of records? Need to migrate them somehow.
I am not so sure my solution is right or not. This is something you might try to accomplish it.
Suppose You have models - like this
#User Model
class User
include Mongoid::Document
has_many :books
end
#Book Model
class Book
include Mongoid::Document
field :title
belongs_to :user
end
At first step I will create another model that is similar to the Book model above but it's embedded instead of referenced.
#EmbedBook Model
class EmbedBook
include Mongoid::Document
field :title
embedded_in :user
end
#User Model (Update with EmbedBook Model)
class User
include Mongoid::Document
embeds_many :embed_books
has_many :books
end
Then create a Mongoid Migration with something like this for the above example
class ReferenceToEmbed < Mongoid::Migration
def up
User.all.each do |user|
user.books.each do |book|
embed_book = user.embed_books.new
embed_book.title = book.title
embed_book.save
book.destroy
end
end
end
def down
# I am not so sure How to reverse this migration so I am skipping it here
end
end
After running the migration. From here you can see that reference books are embedded, but the name for the embedded model is EmbedBook and model Book is still there
So the next step would be to make model book as embed instead.
class Book
include Mongoid::Document
embedded_in :user
field :title
end
class User
include Mongoid::Document
embeds_many :books
embeds_many :embed_books
end
So the next would be to migrate embedbook type to book type
class EmbedBookToBook < Mongoid::Migration
def up
User.all.each do |user|
user.embed_books.each do |embed_book|
book = user.books.new
book.title = embed_book.title
book.save
embed_book.destroy
end
end
def down
# I am skipping this portion. Since I am not so sure how to migrate back.
end
end
Now If you see Book is changed from referenced to embedded.
You can remove EmbedBook model to make the changing complete.
This is just the suggestion. Try this on your development before trying on production. Since, I think there might be something wrong in my suggestion.
10gen has a couple of articles on data modeling which could be useful:
Data Modeling Considerations for MongoDB Applications
Embedded One-to-Many Relationships
Referenced One-to-Many Relationships
MongoDB Data Modeling and Rails
Remember that there are two limitations in MongoDB when it comes to embedding:
the document size-limit is 16MB - this implies a max number of embedded documents, even if you just embed their object-id
if you ever want to search across all embedded documents from the top-level, then don't embed, but use referenced documents instead!
Try these steps:
In User model leave the has_many :books relation, and add the
embedded relation with a different name to not override the books
method.
class User
include Mongoid::Document
has_many :books
embeds_many :embedded_books, :class_name => "Book"
end
Now if you call the embedded_books method from a User instance
mongoid should return an empty array.
Without adding any embedded relation to Book model, write your own
migration script:
class Book
include Mongoid::Document
field :title, type: String
field :price, type: Integer
belongs_to :user
def self.migrate
attributes_to_migrate = ["title","price"] # Use strings not symbols,
# we keep only what we need.
# We skip :user_id field because
# is a field related to belongs_to association.
Book.all.each do |book|
attrs = book.attributes.slice(*attributes_to_migrate)
user = book.user // through belong_to association
user.embedded_book.create!(attrs)
end
end
end
Calling Book.migrate you should have all the Books copied inside each user who was
associated with belongs_to relation.
Now you can remove the has_many and belongs_to relations, and
finally switch to clean embedded solution.
class User
include Mongoid::Document
embeds_many :books
end
class Book
include Mongoid::Document
field :title, type: String
field :price, type: Integer
embedded_in :user
end
I have not tested this solution, but theoretically should work, let me know.
I have a much shorter concise answer:
Let's assume that you have the same models:
#User Model
class User
include Mongoid::Document
has_many :books
end
#Book Model
class Book
include Mongoid::Document
field :title
belongs_to :user
end
So change it to embeds:
#User Model
class User
include Mongoid::Document
embeds_many :books
end
#Book Model
class Book
include Mongoid::Document
field :title
embedded_in :user
end
And generate a mongoid migration like this:
class EmbedBooks < Mongoid::Migration
##attributes_to_migrate = [:title]
def self.up
Book.unscoped.where(:user_id.ne => nil).all.each do |book|
user = User.find book[:user_id]
if user
attrs = book.attributes.slice(*##attributes_to_migrate)
user.books.create! attrs
end
end
end
def self.down
User.unscoped.all.each do |user|
user.books.each do |book|
attrs = ##attributes_to_migrate.reduce({}) do |sym,attr|
sym[attr] = book[attr]
sym
end
attrs[:user] = user
Book.find_or_create_by(**attrs)
end
end
end
end
This works because when you query from class level, it is looking for the top level collection (which still exists even if you change your relations), and the book[:user_id] is a trick to access the document attribute instead of autogenerated methods which also exists as you have not done anything to delete them.
So there you have it, a simple migration from relational to embedded

Resources