Mongoid find by embedded document - ruby

I have the following embedded structure
class CbAuthor
include Mongoid::Document
embeds_many: cb_bylines
field :name, type: String
end
class CbByline
include Mongoid::Document
embedded_in :cb_author
has_many :cb_articles
field :byline, type: String
end
class CbArticle
include Mongoid::Document
belongs_to :cb_byline
end
This is because there are many bylines or pseudonyms the authors publish under and that is will be attached to their analytics reports. So when I have a byline, how do I find the author? This will be necessary because They will have dashboards that should list all the articles they wrote under all their respective bylines.
I tried CbAuthor.cb_bylines but that gives me a no method error. or CbAuthor.where(cb_bylines["byline"]: bylineInQuestion) but that also gives errors.
Essentially the goal is to have one author name to find all his bylines and the articles associated with those bylines

embeds_many :cb_bylines is just a fancy way of saying "add an array of hashes called cb_bylines" (at least as far as storage is concerned). That means that your CbAuthors look like this inside MongoDB:
{
_id: '...',
name: '...',
cb_bylines: [
{ _id: '...', byline: '...' },
...
]
}
MongoDB will unroll the array for simple queries for you so you can simply look for 'cb_bylines.byline' as though you were querying a hash inside the collection:
authors_by_lined_as_pancakes = CbAuthor.where('cb_bylines.byline' => 'Pancakes McGee')
or if you know there is just one:
pancakes_mcgee = CbAuthor.find_by('cb_bylines.byline' => 'Pancakes McGee')
Don't be afraid to bypass Rails and Mongoid to look at what your data really looks like inside MongoDB.

Related

Mongoid skip validation or set default value for document with nil embeds_many

Mongoid won't save document with no reference to embeds_many field, and I can't find any mention in documentation on how to make embeds_many default to [] if not present, or alternatively to skip that validation if not present.
Here's a simple reproduction
class ModelOne
include Mongoid::Document
include Mongoid::Timestamps
field :name, type: String
embeds_many :model_twos
end
class ModelTwo
include Mongoid::Document
embedded_in :model_one
end
mOne = ModelOne.new({name: "foo"})
if mOne.save
...
else
pp mOne.errors.full_messages
end
Which results in an error like;
["ModelTwos is invalid"]
"...by default, Mongoid will validate the children of any relation that are loaded into memory via a validates_associated... If you do not want this behavior, you may turn it off when defining the relation." (documentation)
So, the following should help hopefully:
embeds_many :model_twos, validate: false

Mongoid nesting with embeds_many two times of one class

Recently I came across issue I cannot resolve (or google it properly). First, here are the files:
#Counter.rb
class Counter
include Mongoid::Document
embeds_many :pointing, as: :goodvs, store_as: "goodvs"
embeds_many :pointing, as: :badvs, store_as: "badvs"
accepts_nested_attributes_for :pointing
field :name, type: String
field :champId, type: Integer
end
#Pointing.rb
class Pointing
include Mongoid::Document
belongs_to :counter
accepts_nested_attributes_for :counter
field :name, type: String
field :votes, type: Integer
field :lane, type: String
end
Description
I want to nest Pointing class in Counter class double to make structure like this:
{
name: 'sth',
champId: 1,
goodvs: [{
name: 'sthsth'
votes: 1
lane: 'top'
},
{
name: 'sthsth2'
votes: 4
lane: 'bot'
}],
badvs: [{
name: 'sthsth'
votes: 1
lane: 'mid'
}]
}
Anyone have any solution how to do this? I can make normal structure for nested attributes used once only but I have no clue how to do this properly for this situation.
I have only just started messing around with mongo/mongoid myself but it looks like your class definition is a bit awry. The details are referenced from the Mongoid Relations docs
Use the embedded_in relationship for embeds_many. belongs_to goes with has_many.
class Pointing
include Mongoid::Document
embedded_in :counter
field :name, type: String
field :votes, type: Integer
field :lane, type: String
end
If that doesn't fix it... I setup custom relation names slightly differently by using class_name to point back to the actual class. The as: option is documented to be used when the the child document can belong to many parents but I've not used it enough to say if this is an actual difference or just style.
class Counter
include Mongoid::Document
embeds_many :goodvs, class_name: "Pointing"
embeds_many :badvs, class_name: "Pointing"
accepts_nested_attributes_for :goodvs, :badvs
field :name, type: String
field :champId, type: Integer
end
Then retrieving the objects I've created with:
Counter.each do |c|
log 'counter name', c.name, c.id
log 'goodv', c.goodvs
log 'goodv first', c.goodvs.first.name, c.goodvs.first.id
log 'badvs', c.badvs
log 'badvs first', c.badvs.first.name, c.badvs.first.id
end
Results in:
counter name [sth] [53cfcee66fcb2d2db5000001]
goodv [#<Pointing:0x00000601a395b0>] [#<Pointing:0x00000601a393f8>]
goodv first [mee] [53cfcee66fcb2d2db5000002]
badvs [#<Pointing:0x00000601a37468>] [#<Pointing:0x00000601a372b0>]
badvs first [mee] [53cfcee66fcb2d2db5000002]
So different Pointing object references but both goodvs and badvs contain the same mongo document underneath.

How do I autocreate associated records in MongoDB using Mongoid?

I'm still getting my head around MongoDB and Mongoid in particlar.
Let's say I have a User and each User has one Thingamajig. When I create the User
I want the system to autmatically also create a blank Thingamajig for that User.
Each Thingamajig has a whatsit field that must be unique if it has a value, but is allowed to have no value when created.
So I define the following classes.
class Thingamajig
include Mongoid::Document
field :whatsit, type: String
index({whatsit: 1}, {unique: true, name: 'whatsit_index'})
end
class User
include Mongoid::Document
field :name, type: String
index({name: 1}, {unique: true, name: 'user_name_index'})
embeds_one :thingamajig, dependent: :nullify, autobuild: true
end
However what I find when I
User.create!(name: 'some name')
is that User.find(name: 'some name').thingamajig is nil.
Questions:
How can I ensure that each User gets an associated Thingamajig? and
How do I specify that the name field of a User is required?
FYI I am using Sintara not Rails (if that matters to anyone).
1 - The autobuild: true option normally should have done the trick. I think the problem is that you forgot to add the other side of the relation to the Thingamajig model:
class Thingamajig
include Mongoid::Document
embedded_in :user
...
end
2 - To specify required fields, use validations:
class User
include Mongoid::Document
field :name, type: String
validates_presence_of :name
...
end
Mongoid uses ActiveModel validations.

Rails Generate Nested JSON

I have two objects, an #article and a #profile. Article is a model and #profile is a Struct. I'd like to end up with some JSON that looks like this:
{
"article": {
"title": "this is a title",
"author": "Author McAuthor",
"profile": {
"first_name": "Bobby",
"last_name": "Fisher"
}
}
}
As of now, I can just manually create this by doing something like:
#json = { article: { title: #article.title, author: #article.author, profile: { first_name: #profile.first_name, last_name: #profile.last_name } }}
I feel like building the json object this way is sorta crude, also, every time I change the author model, I might have to change this code. It would be great if I could find an easier way to build these json objects without having to do so manually... Any help? Thanks!
In addition to shioyama's correct answer, you can use rabl to craft your JSON objects, similar to how ERB works for views.
For example, you would create a 'view', say, index.rabl. It would look like:
collection #articles
attributes :author, :title
child(:profile) { attributes :first_name, :last_name }
Rails serializes objects in two steps, first by calling as_json to create the object to be serialized, then by calling to_json to actually create the JSON string.
Generally, if you want to customize how your models are represented in JSON, it's best to override as_json. Assuming your profile struct is a virtual attribute (i.e. defined with attr_accessor, not saved in the db), you could do this in your Article model:
def as_json(options = {})
super((options || {}).merge({
:methods => :profile
}))
end
Hope that helps. See also:
as_json documentation
Add virtual attribute to json output

Class methods on Mongoid embedded documents

I'd like to keep all authentication specific code in the file that defines the Auth "model" like this:
class User
include Mongoid::Document
embeds_one :auth
field :username, type: String
end
class Auth
include Mongoid::Document
embedded_in :user, inverse_of: :auth
field :password
def self.login(user, pass)
User.first(conditions: { username: user, password: pass })
end
end
The problem? It's not possible to call class methods of embedded documents:
> Auth.login('user', 'pass')
Mongoid::Errors::InvalidCollection: Access to the collection for Auth is not allowed since it is an embedded document, please access a collection from the root document.
> User.auth.login('user', 'pass')
NoMethodError: undefined method `auth' for User:Class
Singleton methods in embedded Mongoid::Document models is not a good idea?
You can't access embedded documents directly like you tried first time Auth.loggin('user','pass'). You should be having only instance methods in embedded document models like this
def self.login(user, pass)
User.first(conditions: { username: user, password: pass })
end
and can access it by user object like this
#user.auth.login('user','pass')

Resources