Creating embedded documents, doesn't seem to create relationship - ruby-on-rails

I have the following models defined in Rails, with Mongoid:
Class Character
include Mongoid::Document
field :n, as: name, type: String
field :p, as: :positions, type: Array
field :o, as: :roles, type: Array
field :r, as: :resource, type: String
embeds_one :base_stat
end
class BaseStat
include Mongoid::Document
embedded_in :character
end
I'm trying to seed my database with documents that have these relationships 1) because I'd have to eventually and 2) so I can test I'm using Mongoid correctly. I've tried a couple of different ways to seed it, but every single time, I can create Character documents, then create BaseStat documents based off a Character document, but calling character.base_stat returns nil.
Here are the things I've tried in db/seeds.rb that didn't throw errors:
ch = Character.create!([etc])
ch.build_base_stat([etc])
Character.create!(name: value, name: value, base_stat: BaseStat.new(name: value, name:value))
ch = Character.create!([etc])
ch.create_base_stat([etc])
I've also tried using ch.base_stat.create! (which threw an error when I called rake db:setup).
I know that both the Character and BaseStat documents are created because I can search in the Rails console for the Character documents that were seeded (a = Character.where(name: value)[0] and b = BaseStat.where(name:value)[0]). But it looks like the relationship isn't being stored.
Calling a.metadata also throws a NoMethodError.
I don't have any controllers set up, just the models and the entries in db/seeds.rb. I feel like I must be missing something fundamental because, well, I've been hunting through StackOverflow and haven't seen anything that fixed this.
Versions:
Mongoid 4.0.0.alpha2
Rails 4.0.1

Have you tried a very basic test? Can you open the rails console create a Character, save it, then add a BaseStat to it and save that?
c = Character.new
b = BaseStat.new
b.name = "test"
c.base_stat = b
c.save
c
Does that print out your new record with the BaseStat embedded? If it does then there must be something wrong with the syntax or method in the seeds.

Quoting Mongoid docs, this could be why:
One core difference between Mongoid and Active Record from a behavior standpoint is that Mongoid does not automatically save child relations for relational associations. This is for performance reasons.
Try adding autosave: true to your base_stat relation:
embeds_one :base_stat, autosave: true

Related

Trying to build a one to one relationship between two preexisting models

I am new to rails and I am trying to build a one to one relationship between two preexisting models (test and test_type) in rails.
Here is my workflow.
has_one :test_type - to tests model
belongs_to :test - to test_types model
rails g migration Add_Test_Type_To_Test test_type:references
rake db:migrate
Now that looks like it has worked ok but when I try to verify that it looks like it hasn't.
rails console
#type = TestTypes.new :name => "My Type"
#type.save
#test = Tests.new :name => "My Test"
TestTypes.find(1) //returns record ok
#test.test_type //returns nil
#test.test_type = TestTypes.find(1) //NameError: uninitialized constant Tests::TestType
#test.test_type //still nil
According to the output it finds my type in the database but it doesn't seem to be able to add it to my test class which indicates to me that the relationship is not working.
Is anybody able to tell what I am doing wrong?
Does it work if you just change one line of code belongs_to :test -> has_one :test to make it one-on-one relationship. But I think your error comes from the fact that you have not added test_type instance to test_type attribute of test object. So before making #test.test_type request you need to add it #test.test_type = #test_type. If you wanna save the change to database then update_attribute method is very useful for that purpose update_attribute(name, value), #test.update_attribute('test_type', #test_type)
Updated answer: Rails Guide says:
4.2.1 Methods Added by has_one
When you declare a has_one association, the declaring class automatically gains five methods related to the association:
association(force_reload = false)
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {}
so try this #test.create_test_type(test_id: test.id), that bit of code can replace the following code I mentioned earlier #test.test_type (sorry I'm not Rails expert - still learning)

How to use `first_or_initialize` step with `accepts_nested_attributes_for` - Mongoid

I'd like to incorporate a step to check for an existing relation object as part of my model creation/form submission process. For example, say I have a Paper model that has_and_belongs_to_many :authors. On my "Create Paper" form, I'd like to have a authors_attributes field for :name, and then, in my create method, I'd like to first look up whether this author exists in the "database"; if so, then add that author to the paper's authors, if not, perform the normal authors_attributes steps of initializing a new author.
Basically, I'd like to do something like:
# override authors_attributes
def authors_attributes(attrs)
attrs.map!{ |attr| Author.where(attr).first_or_initialize.attributes }
super(attrs)
end
But this doesn't work for a number of reasons (it messes up Mongoid's definition of the method, and you can't include an id in the _attributes unless it's already registered with the model).
I know a preferred way of handling these types of situations is to use a "Form Object" (e.g., with Virtus). However, I'm somewhat opposed to this pattern because it requires duplicating field definitions and validations (at least as I understand it).
Is there a simple way to handle this kind of behavior? I feel like it must be a common situation, so I must be missing something...
The way I've approached this problem in the past is to allow existing records to be selected from some sort of pick list (either a search dialog for large reference tables or a select box for smaller ones). Included in the dialog or dropdown is a way to create a new reference instead of picking one of the existing items.
With that approach, you can detect whether the record already exists or needs to be created. It avoids the need for the first_or_initialize since the user's intent should be clear from what is submitted to the controller.
This approach struggles when users don't want to take the time to find what they want in the list though. If a validation error occurs, you can display something friendly for the user like, "Did you mean to pick [already existing record]?" That might help some as well.
If I have a model Paper:
class Paper
include Mongoid::Document
embeds_many :authors
accepts_nested_attributes_for :authors
field :title, type: String
end
And a model Author embedded in Paper:
class Author
include Mongoid::Document
embedded_in :paper, inverse_of: :authors
field :name, type: String
end
I can do this in the console:
> paper = Paper.create(title: "My Paper")
> paper.authors_attributes = [ {name: "Raviolicode"} ]
> paper.authors #=> [#<Author _id: 531cd73331302ea603000000, name: "Raviolicode">]
> paper.authors_attributes = [ {id: paper.authors.first, name: "Lucia"}, {name: "Kardeiz"} ]
> paper.authors #=> [#<Author _id: 531cd73331302ea603000000, name: "Lucia">, #<Author _id: 531cd95931302ea603010000, name: "Kardeiz">]
As you can see, I can update and add authors in the same authors_attributes hash.
For more information see Mongoid nested_attributes article
I followed the suggestion of the accepted answer for this question and implemented a reject_if guard on the accepts_nested_attributes_for statement like:
accepts_nested_attributes_for :authors, reject_if: :check_author
def check_author(attrs)
if existing = Author.where(label: attrs['label']).first
self.authors << existing
true
else
false
end
end
This still seems like a hack, but it works in Mongoid as well...

Mongoid: finding documents that are referencing another one (referenced 1-N)

I have a MongoDB database that uses mongoid referencing according to the documentation. Here are my models:
class Mydoc
include Mongoid::Document
# ...
has_and_belongs_to_many :editors, class_name: 'User', inverse_of: nil
# ...
end
and
class User
# actually, it's based on devise with some changes
# ...
# it does not reference back to Mydoc, see inverse_of there!
end
Now if I make a reference to a user (to grant her/him editor role), mongoid creates an array field called editor_ids that contains object ids of the documents in the users table. Works nice.
I'd like to have a query that shows all Mydoc documents where a certain user is editor. Can't figure out how to do that in mongoid. It's very simple in the mongo console:
db.mydocs.find({ editor_ids: { $in: [ ObjectId("52c97e58b878bcf156000001") ] } })
It works like charm. But I need this in rails, not in the console. Based on mongoid docs, I've tried this (and a hell lot of variations of it) but it does not work:
#docs_where_editor = Mydoc.where(:editors.in => [#user._id])
the result is an empty dataset. I would appreciate any hint.
I suppose it's not the where method, since in mongoid, find only accepts object ids you want to find.
If you have an :editor_ids array and you're only looking for a single value then you should be able to use multi-keys thusly:
Mydoc.where(:editor_ids => #user.id)
and if you have several editor IDs to search for then:
Mydoc.where(:editors_ids.in => array_of_user_ids)
would be the transliteration of your MongoDB console query to Mongoid.

Correct way to create or update with multiple belongs_to in Rails

New to Rails and Ruby and trying to do things correctly.
Here are my models. Everything works fine, but I want to do things the "right" way so to speak.
I have an import process that takes a CSV and tries to either create a new record or update an existing one.
So the process is 1.) parse csv row 2.) find or create record 3.) save record
I have this working perfectly, but the code seems like it could be improved. If ParcelType wasn't involved it would be fine, since I'm creating/retrieving a parcel FROM the Manufacturer, that foreign key is pre-populated for me. But the ParcelType isn't. Anyway to have both Type and Manufacturer pre-populated since I'm using them both in the search?
CSV row can have multiple manufacturers per row (results in 2 almost identical rows, just with diff mfr_id) so that's what the .each is about
manufacturer_id.split(";").each do |mfr_string|
mfr = Manufacturer.find_by_name(mfr_string)
# If it's a mfr we don't care about, don't put it in the db
next if mfr.nil?
# Unique parcel is defined by it's manufacturer, it's type, it's model number, and it's reference_number
parcel = mfr.parcels.of_type('FR').find_or_initialize_by_model_number_and_reference_number(attributes[:model_number], attributes[:reference_number])
parcel.assign_attributes(attributes)
# this line in particular is a bummer. if it finds a parcel and I'm updating, this line is superfulous, only necessary when it's a new parcel
parcel.parcel_type = ParcelType.find_by_code('FR')
parcel.save!
end
class Parcel < ActiveRecord::Base
belongs_to :parcel_type
belongs_to :manufacturer
def self.of_type(type)
joins(:parcel_type).where(:parcel_types => {:code => type.upcase}).readonly(false) unless type.nil?
end
end
class Manufacturer < ActiveRecord::Base
has_many :parcels
end
class ParcelType < ActiveRecord::Base
has_many :parcels
end
It sounds like the new_record? method is what you're looking for.
new_record?() public
Returns true if this object hasn’t been saved yet — that is, a record
for the object doesn’t exist yet; otherwise, returns false.
The following will only execute if the parcel object is indeed a new record:
parcel.parcel_type = ParcelType.find_by_code('FR') if parcel.new_record?
What about 'find_or_create'?
I have wanted to use this from a long time, check these links.
Usage:
http://rubyquicktips.com/post/344181578/find-or-create-an-object-in-one-command
Several attributes:
Rails find_or_create by more than one attribute?
Extra:
How can I pass multiple attributes to find_or_create_by in Rails 3?

how to force a has_many to use a specific :foreign_key?

I'm having trouble with a model not honoring the :foreign_key policy.
Character model has the following fields:
name:string
level:int
realm:string
realm_id:integer
class Character < ActiveRecord::Base
belongs_to :realm
end
My Realms model looks like this:
class Realm < ActiveRecord::Base
has_many :characters, :foreign_key => "realm_id"
end
However, it seems like it's forcing the character model to use the :realm column as the foreign_key rather than :realm_id. I don't have any clue as to why or how to fix it. Is there any other way to make it ignore the :realm field and go for the :realm_id without having to change the name of the column?
[Edit for clarity]
The character model does have a realm_id:integer field. I have tried not having the foreign_key but the results with both is identical.
ruby-1.9.2-p136 :012 > c = Character.new
=> #
ruby-1.9.2-p136 :013 > c.realm = "Sargeras"
ActiveRecord::AssociationTypeMismatch: Realm(#2154038240) expected, got String(#2151988680)
Despite even having the foreign_key, it just refuses to let go of the realm column.
[Edit 2]
The realm column will just take over due to the has_many and belongs_to association. There is no way so far to break this, so the solution is to either remove the column (the approach i will take), or rename it to something different.
Did you make sure that your Character table has a realm_id column? Please make sure of that, and then get rid of foreign_key => 'realm_id, it is not necessary at all. Your program should work if you get both these things done.
You should not need the :foreign_key part here at all, since you're following the standard Rails naming convention, the realm_id column should be inferred from the model name.
EDIT
I see. I don't think you can have a column and an association by the same name in one model. The easiest solution would probably be to rename the "realm" column to "realmname" or something like that.
belongs_to :realm creates (among other things) methods called realm and realm= used as getters and setters. That means the method_missing magic that ActiveRecord uses to expose database columns as attributes is never triggered when you do Character#realm, as that method isn't in fact missing.
If you want to avoid renaming the realm column in your database, you could create attribute accessors for the field manually under another name:
class Character < ActiveRecord::Base
def realm_name
self['realm']
end
def realm_name=(value)
self['realm'] = value
end
end
This way you'll still have the realm column in your database and be able to access the attribute in Ruby, albeit under a different name. This isn't a great idea, though, as you'll be duplicating the realm name as both Character#realm_name and Character.realm.name.
I would ditch the realm column and instead make sure I use Realm objects when importing from the datasource:
character.realm = Realm.find_by_name('Sargeras')
That way you'd only realm data available where it makes sense; in the Realm model.

Resources