How to sort by nested field value with Mongoid? - ruby

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.

Related

Refactor with Strategy Pattern. In Ruby

Heads up! In the below example, using a pattern is probably overkill... however, if I were extending this to count genres, count the members in a given band, count the number of fans, count the number of venues played, count the number of records sold, count the number of downloads for a specific song etc... it seems like there could be a ton of stuff to count.
The Goal:
To create a new function that chooses the correct counting function based on the input.
The Example:
class Genre < ActiveRecord::Base
has_many :songs
has_many :artists, through: :songs
def song_count
self.songs.length
end
def artist_count
self.artists.length
end
end
P.S. If you are also a curious about this question, you may find this other question (unfortunately answered in C#) to be helpful as a supplemental context. Strategy or Command pattern? ...
In Ruby you can implement a strategy pattern quite easily using an (optional) block (assuming it's still unused).
class Genre < ActiveRecord::Base
has_many :songs
has_many :artists, through: :songs
def song_count(&strategy)
count_using_strategy(songs, &strategy)
end
def artist_count(&strategy)
count_using_strategy(artists, &strategy)
end
private
def count_using_strategy(collection, &strategy)
strategy ||= ->(collection) { collection.size }
strategy.call(collection)
end
end
The above code defaults to using the size strategy. If you ever want to use a specific strategy in a specific scenario you can simply provide the strategy alongside the call.
genre = Genre.last
genre.song_count # get the song_count using the default #size strategy
# or provide a custom stratigy
genre.song_count { |songs| songs.count } # get the song_count using #count
genre.song_count { |songs| songs.length } # get the song_count using #length
If you need to re-use some strategies more often you could save them in a constant or variable:
LENGTH_STRATEGY = ->(collection) { collection.length }
genre.artist_count(&LENGTH_STRATEGY)
Or create a specific class for them if they are more complicated (currently overkill):
class CollectionStrategy
def self.to_proc # called when providing the class as a block argument
->(collection) { new(collection).call }
end
attr_reader :collection
def initialize(collection)
#collection = collection
end
end
class LengthStrategy < CollectionStrategy
def call
collection.length
end
end
genre.artist_count(&LengthStrategy)

How to define method names and object references before actually having to use them (Ruby)

Say I am keeping track of email correspondances. An enquiry (from a customer) or a reply (from a supporter) is embedded in the order the two parties are corresponding about. They share the exact same logic when put into the database.
My problem is that even though I use the same logic, the object classes are different, the model fields I need to call are different, and the method names are different as well.
How do I put methods and objects references in before I actually have to use them? Does a "string_to_method" method exists or something like that?
Sample code with commentaries:
class Email
include Mongoid::Document
field :from, type: String
field :to, type: String
field :subject, type: String
belongs_to :order, :inverse_of => :emails
def start
email = Email.create!(:from => "sender#example.com", :to => "recipient#example.com", :subject => "Hello")
from_or_to = from # This represents the database field from where I later on will fetch the customers email address. It is either from or to.
enquiries_or_replies = enquiries # This represents a method that should later be called. It is either enquiries or replies.
self.test_if_enquiry_or_reply(from_or_to, enquiries_or_replies)
end
def test_if_enquiry_or_reply(from_or_to, enquiries_or_replies)
order = Order.add_enquiry_or_reply(self, from_or_to, enquiries_or_replies)
self.order = order
self.save
end
end
class Order
include Mongoid::Document
field :email_address, type: String
has_many :emails, :inverse_of => :order
embeds_many :enquiries, :inverse_of => :order
embeds_many :replies, :inverse_of => :order
def self.add_enquiry_or_reply(email, from_or_to, enquiries_or_replies)
order = Order.where(:email_address => email.from_or_to).first # from_or_to could either be from or to.
order.enquiries_or_replies.create!(subject: email.subject) # enquiries_or_replies could either be enquiries or replies.
order
end
end
Judging by the question and the code sample, it sounds like you are mixing concerns too much. My first suggestion would be to re-evaluate your method names and object structure. Ambiguous names like test_if_thing1_or_thing2 and from_or_to (it should just be one thing) will make it very hard for others, and your future self, to understand the code laster.
However, without diverging into a debate on separation of concerns, you can change the methods you call by using public_send (or the private aware send). So you can do
order.public_send(:replies).create!
order.public_send(:enquiries).create!
string to method does exist, it's called eval
so, you could do
method_name = "name"
eval(method_name) #calls the name method

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

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.

Is there a way in MongoMapper to achieve similar behavior as AR's includes method?

Is there a feature equivalent in MongoMapper to this:
class Model < ActiveRecord::Base
belongs_to :x
scope :with_x, includes(:x)
end
When running Model.with_x, this avoids N queries to X.
Is there a similar feature in MongoMapper?
When it's a belongs_to relationship, you can turn on the identity map and run two queries, once for your main documents and then one for all the associated documents. That's the best you can do since Mongo doesn't support joins.
class Comment
include MongoMapper::Document
belongs_to :user
end
class User
include MongoMapper::Document
plugin MongoMapper::Plugins::IdentityMap
end
#comments = my_post.comments # query 1
users = User.find(#comments.map(&:user_id)) # query 2
#comments.each do |comment|
comment.user.name # user pulled from identity map, no query fired
end
(Mongoid has a syntax for eager loading, but it works basically the same way.)

Rails 3 scope only select certain attributes for a has_many relationship

This looks like it should be something pretty easy but I can't seem to get it to work. I have a model with a has_many relationship and I'd like a scope on the parent that allows me to select only certain attributes for each.
An example:
class Bakery < ActiveRecord::Base
has_many :pastries
scope :summary, select([:id, :name, 'some option calling pastries.summary'])
class Pastry < ActiveRecord::Base
belongs_to :bakery
scope :summary, select([:id, :image_url])
I'd like to be able to call something like Bakery.first.summary and get a Bakery model with only the id and name populated and for each pastry in it's pastries array to only have the id and image_url attributes populated.
You could do this, but it won't affect the SQL queries that are made as a result (assuming you're trying to optimise the underlying query?):
class Pastry
...
def summary
{
:id => self.id,
:image_url => self.image_url
}
end
end
class Bakery
...
def summary
pastries.collect {|i| i.summary }
end
end
This would then give you an array of hashes, not model instances.
ActiveRecord doesn't behave how you're expecting with models - it will fetch whatever data it thinks you need. You could look at using the Sequel gem instead, or executing a raw SQL query such as:
Pastry.find_by_sql("SELECT id, name from ...")
But this could give you unexpected behaviour.

Resources