Mongoid: can I embed many and reference one of the embedded? - ruby

I have a list of games. Each one has an embedded list of scores. I'd like to keep a reference to the best score outside of the scores list.
class Game
include Mongoid::Document
field :best_score_id, type: Moped::BSON::ObjectId
...
embeds_many :scores
class Score
include Mongoid::Document
field :user, type: String
field :score, type: Int
I tried doing an belongs_to and a has_one but got an error message: "Referencing a Score document from the Game document via a relational association is not allowed since the price history is embedded." I suppose I can store the relevant bits of the Score in a hash called "best_score" but it makes more sense to me to embed many scores and then reference one of them as "Best". Is this possible?

You could do something like this -
Write a method to pick the best score in the game model class -
def best_score
score = scores.order_by(:score, :desc).limit(1)
if score.nil?
nil
else
score.first
end
end
And since scores are embedded in the game, there won't be any +1 query to the database also.

Related

Neo4jRB: Searching through all associations?

Consider this simple setup:
class Person
include Neo4j::ActiveNode
property :name, type: String
has_many :out, :follows, model_class: Person, rel_class: Friendship
has_many :in, :followed_by, model_class: Person, rel_class: Friendship
end
class Friendship
include Neo4j::ActiveRel
property :key, type: String
type 'friendship'
from_class Person
to_class Person
end
How would I search through all Friendships for those matching a condition? (e.g. Friendships of a certain key).
In an email, Brian Underwood points me to this snippet:
ModelClass.association_name(:node_var, :rel_var).where("rel_var = 'some_condition'")
I've tried playing around with it, but don't understand. Is ModelClass an ActiveNode or ActiveRel instance? What is :node_var and :rel_var?
If you want to search for every friendship that has a specific key property, you'd do that like this:
Person.all.follows.rel_where(key: your_key_var)
# OR
Person.all.follows(:f, :r).where('r.key = {key}').params(key: your_key_var)
These will both generate MATCH (p:Person)-[r:friends]->(f:Person), more or less, with the first example using auto-defined node identifiers and the second using f for the destination Friend node and r for the relationship, as given by the :f, :r arguments. After that, a to_a will return the friend at the END of the chain or you can call pluck with either :f or :r to return the given objects.
The model_class option always describes the NODE class on the other side of the association. In Brian's example, node_var and rel_var are generic names for the identifiers Cypher will use in the statement that it creates.

How to validate a mongoid field based on another mongoid field?

I made the switch to nosql and am playing with Mongoid with Sinatra. I'm still new to this, and I"m stumped on this problem.
class Item
field :name, type: String
field :category, type: Moped::BSON::ObjectId # holds _id of the category object
field :cover_type, type: String
validates_presence_of :category
validates_inclusion_of :cover_type, in:["Hardcover", "Softcover"]
end
class Category
field :name
validates_inclusion_of :name, in:["Books", "Movies", "Ebooks"]
end
Imagine a store that sells Books, Movies, and Ebooks and each item sold belongs to one of the three categories. If an Item is listed under the category "Books", then the Item is required to have a field called cover_type. Furthermore, cover_type can only be either "Hardcover" or "Softcover".
When saving an Item, how do I piece together the validate in the Item class for when the Item is in the Book category and therefore requires the presence of the field cover_type, which is also validated as being "Hardcover" or "Softcover"?
If the Item isn't a Book, then cover_type can be null.
Mongoid models inherit ActiveModel validations. You can just write a custom validator method:
class Item
validates :cover_type_must_be_valid, if: :book?
def book?
Category.find(category).name == 'Books'
end
def cover_type_must_be_valid
errors.add(:cover_type, 'must be Hardcover or Softcover') unless %w{Hardcover Softcover}.include? cover_type
end
end
The book? method is unpleasant; why not use belongs_to :category on Item and has_many :items on Category?
EDIT: here’s what book? could look like if you used the has_many and belongs_to:
def book?
category.name == 'Books'
end
Not that different, but you’ll surely be accessing item.category all over your application, I can’t see why you wouldn’t want to make it easier.

Mongoid: Retrieves all embedded documents

Suppose we have these models:
class Person
include Mongoid::Document
embeds_many :albums
end
class Album
include Mongoid::Document
embeds_many :photos
end
class Photo
include Mongoid::Document
end
What I want is to retrieves all Photo of a particular Person. Is there a mongoid/mongodb shortcuts or the only way is to iterate over person.albums and store all album.photos in a new array?
Thanks.
You have 2 ways to do this, one is through Mongoid, which, AFAIK, will inflate all objects.
Something like:
Person.only("albums.photos").where(id: '1').albums.map(&:photos).flatten
Or you can do it in Moped(driver) which will return only an array of photos.
Person.collection.find(id: "1").select("albums.photos" => 1).
first["albums"].map { |a| a["photos"] }.flatten
On the DB load, both dont make any difference, as they will yield the same query, only difference is that the first one will create way more objects than the second one.

How to sort by nested field value with Mongoid?

Let's say I have a User with a field name, and which has_many teams, and a Team that belongs_to a user, and belongs_to a sport. A Sport has a field name and has_many teams.
I want to walk through the sports, do some stuff, and collect an array of the teams sorted by the name of the user.
result = []
Sport.asc(:name).each do |spt|
# some other stuff not relevant to this question but that
# justifies my looping through each sport.
spt.teams.asc(:'user.name').each { |t| result << t }
end
This runs but and the sorting of the sports is as expected, but the order of the teams in result is not sorted as I'd expect.
What is the correct way, using Mongoid to sort a collection by the value of a relation?
I don't think there is a way to do this with Mongoid. It might work if the field you were sorting by was part of an embedded document, but not in this case where you're using a referenced document.
I guess you probably have two options here, the less efficient way would be to just sort the collection of teams in ruby:
sport.teams.sort{|t1, t2| t1.user.name <=> t2.user.name}.each{ |team| result << team }
The better, and arguably more 'MongoDB-y' solution, would be to cache the user name inside each team, using a before_save callback, and then use that to sort the teams:
# app/models/team.rb
class Team
include Mongoid::Document
field :user_name, :type => String
before_save :update_user_name
protected
def update_user_name
self.user_name = self.user.name if self.user
end
end
Then you can just do:
spt.teams.asc(:user_name).each { |t| result << t }
Obviously, if the user's name field is mutable, then you'll to trigger it to save each child-team whenever the user's name field is changed.
class User
after_save :update_teams_if_name_changed
def update_teams_if_name_changed
if self.name_changed?
self.teams.each { |team| team.save }
end
end
end
Given that is not fantastically simple to maintain, this could arguably be a good candidate to use an observer, rather than callbacks, but you get the idea.

How to reference an embedded document in Mongoid?

Using Mongoid, let's say I have the following classes:
class Map
include Mongoid::Document
embeds_many :locations
end
class Location
include Mongoid::Document
field :x_coord, :type => Integer
field :y_coord, :type => Integer
embedded_in :map, :inverse_of => :locations
end
class Player
include Mongoid::Document
references_one :location
end
As you can see, I'm trying to model a simple game world environment where a map embeds locations, and a player references a single location as their current spot.
Using this approach, I'm getting the following error when I try to reference the "location" attribute of the Player class:
Mongoid::Errors::DocumentNotFound: Document not found for class Location with id(s) xxxxxxxxxxxxxxxxxxx.
My understanding is that this is because the Location document is embedded making it difficult to reference outside the scope of its embedding document (the Map). This makes sense, but how do I model a direct reference to an embedded document?
Because Maps are their own collection, you would need to iterate over every Map collection searching within for the Location your Player is referenced.
You can't access embedded documents directly. You have to enter through the collection and work your way down.
To avoid iterating all of the Maps you can store both the Location reference AND the Map reference in your Player document. This allows you to chain criteria that selects your Map and then the Location within it. You have to code a method on your Player class to handle this.
def location
self.map.locations.find(self.location_id)
end
So, similar to how you answered yourself except you could still store the location_id in your player document instead of using the coord attribs.
Another way would be to put Maps, Locations, and Players in their own collections instead of embedding the Location in your Map collection. Then you could use reference relationships without doing anything fancy... however your really just using a hierarchical database likes it's a relational database at this point...
PLEASE go and VOTE for the "virtual collections" feature on MongoDB's issue tracker:
http://jira.mongodb.org/browse/SERVER-142
It's the 2nd most requested feature, but it still hasn't been scheduled for release. Maybe if enough people vote for it and move it to number one, the MongoDB team will finally implement it.
In my use case, there is no need for the outside object to reference the embedded document. From the mongoid user group, I found the solution: Use referenced_in on the embedded document and NO reference on the outside document.
class Page
include Mongoid::Document
field :title
embeds_many :page_objects
end
class PageObject
include Mongoid::Document
field :type
embedded_in :page, :inverse_of => :page_objects
referenced_in :sprite
end
class Sprite
include Mongoid::Document
field :path, :default => "/images/something.png"
end
header_sprite = Sprite.create(:path => "/images/header.png")
picture_sprte = Sprite.create(:path => "/images/picture.png")
p = Page.create(:title => "Home")
p.page_objects.create(:type => "header", :sprite => header_sprite)
p.page_objects.first.sprite == header_sprite
My current workaround is to do the following:
class Map
include Mongoid::Document
embeds_many :locations
references_many :players, :inverse_of => :map
end
class Player
referenced_in :map
field :x_coord
field :y_coord
def location=(loc)
loc.map.users << self
self.x_coord = loc.x_coord
self.y_coord = loc.y_coord
self.save!
end
def location
self.map.locations.where(:x_coord => self.x_coord).and(:y_coord => self.y_coord).first
end
end
This works, but feels like a kluge.
Thinking outside the box, you could make Location its own document and use Mongoid Alize to automatically generate embedded data in your Map document from your Location documents.
https://github.com/dzello/mongoid_alize
The advantage of this method is that you get efficient queries when conditions are suitable, and slower reference based queries on the original document when there's no other way.

Resources