Mongoid association & null object pattern? - ruby

How would you implement the null object pattern on a Mongoid relation?
Class Owner
include Mongoid::Document
embeds_one :preference
end
Most owners won't have a preference, and thus I want them to have a NullPreference instead, as described in Ben Orenstein's excellent talk.
What I would like is something like this:
class NullPreference
def name
'no name'
end
end
owner = Owner.new
preference = owner.preference
preference.name
=> 'no name'
I found a related question regarding the same thing in ActiveRecord, no answers though.
Edit: I'm using Mongoid 2.6 otherwise I could've used autobuild: true and get a real Preference and use the defaults instead.

An obvious way is to build a layer of abstraction over that field.
class Owner
include Mongoid::Document
embeds_one :preference_field # internal field, don't use directly
def preference
preference_field || NullPreference.new
end
def preference= pref
self.preference_field = pref
end
end
Maybe there are simpler ways.

Related

Paper Trail Gem: How to use a custom item_type for specific model

Is there an option to configure the item_type for a version? I have a class Post, and the default item_type for that would be Post; is there an option to configure that be Foo?
UPDATE with example:
class Post < ActiveRecord::Base
has_paper_trail
end
post = Post.create # this creates a new post along with a version.
version = post.versions.first
version.item_type = 'Post' # Item type here is the name of the base model 'Post'.
Is there an option to configure the item_type for a version? I have a class Post, and the default item_type for that would be Post; is there an option to configure that be Foo?
No. When you call has_paper_trail, it adds a polymorphic association named item. Therefore, PaperTrail does not control the database column item_type, ActiveRecord does.
Here is the definition of the has_many association:
has_many(
versions_association_name, # Usually "versions"
-> { order(model.timestamp_sort_order) }, # Usually "created_at"
class_name: version_class_name, # Usually "PaperTrail::Version"
as: :item # Specifies a polymorphic interface
)
Just stumbled across this, realise its a bit old now but thought I'd share my solution as I had the same requirement. This is only a partial solution but it works me
I have extended the PaperTrail Version class and overriden the item_type getter
class AuditTrail < PaperTrail::Version
xss_foliate :except => [:object, :object_changes]
def item_type
if(self[:item_type] == "SomethingIWantToChange")
return "Different String"
end
return self[:item_type]
end
end
I then have set each of my models to use this class like so:
has_paper_trail :class_name => 'AuditTrail'
Then I can query the versions table and objects returned will run through the overridden getter and the item_type will be as I require:
audit_records = AuditTrail.where(someproperty: "something")
So this doesn't actually alter it in the DB when it is written, but is the best way I could find to present it differently to my frontend
Note that it doesn't alter the item_type if you fetch versions without using a query from your extended object i.e:
Someobject.find(1).versions.last
^ this still returns the item_type from the DB
You want the class_name option. This should work with your example:
class Post < ActiveRecord::Base
has_paper_trail :class_name => 'Foo'
end
This will version the Post class as Foo.
You can find more details in the Paper Trail documentation in the Custom Version Classes section.

Querying mongoid for value in attribute array

I need to search within Mongoid objects that have array attributes. Here are the relevant objects:
class Author
include Mongoid::Document
field :name, type: String
class Book
include Mongoid::Document
field :name, type: String
field :authors, type: Array
I can see that at least one book has a given author:
Book.all.sample.authors
=> [BSON::ObjectId('5363c73a4d61635257805e00'),
BSON::ObjectId('5363c73a4d61635257835e00'),
BSON::ObjectId('5363c73a4d61635257c75e00'),
BSON::ObjectId('5363c73b4d616352574a5f00')]
But I'm unable to find books that have that author.
Book.where(authors: '5363c73a4d61635257805e00').first
=> nil
I've tried the solution listed here: https://groups.google.com/forum/#!topic/mongoid/csNOcugYH0U but it didn't work for me:
Book.any_in(:author => ["5363c73b4d616352574a5f00"]).first
=> nil
I'm not sure what I'm doing wrong. Any ideas? I'd prefer to use Mongoid Origin commands.
This output:
Book.all.sample.authors
=> [BSON::ObjectId('5363c73a4d61635257805e00'),
BSON::ObjectId('5363c73a4d61635257835e00'),
BSON::ObjectId('5363c73a4d61635257c75e00'),
BSON::ObjectId('5363c73b4d616352574a5f00')]
tells us that authors contains BSON::ObjectIds. ObjectIds are often presented as Strings and sometimes you can use a String instead of a full blown ObjectId (such as with Model.find) but they're still not Strings. You are searching the array for a String:
Book.where(authors: '5363c73a4d61635257805e00')
but '5363c73a4d61635257805e00' and ObjectId('5363c73a4d61635257805e00') are not the same thing inside MongoDB. You need to search for the right thing:
Book.where(authors: BSON::ObjectId('5363c73a4d61635257805e00'))
You might want to monkey patch a to_bson_id method into various places. Something like this:
class String
def to_bson_id
BSON::ObjectId.from_string(self)
end
end
module Mongoid
module Document
def to_bson_id
id
end
end
end
module BSON
class ObjectId
def to_bson_id
self
end
end
end
class NilClass
def to_bson_id
self
end
end
Should do the trick. Then you can say things like:
Book.where(authors: '5363c73a4d61635257805e00'.to_bson_id)
Book.where(authors: some_string_or_object_id.to_bson_id)
and The Right Thing happens.
You might want to rename authors to author_ids to make its nature a little clearer.

Rails Include Module on Condition

I'm trying to include a module only when a condition is met.
module PremiumServer
def is_premium
true
end
end
class Server
include Mongoid::Document
include PremiumServer if self.premium
field :premium, :type => Boolean, :default => false
end
This isn't working, and I can't figure out why. Can someone please tell me how I'm supposed to include modules based upon a condition being met, like above?
Thanks!
EDIT:
I found the answer to my problem here: Mongoid and carrierwave
However, I'm awarding the question to the top answer as it is probably the more useful way.
includes happen on the class level. Your premium attribute is at instance level.
There are ways to do the include on per instance level, but I would not recommend them.
Here you are better of using inheritance
class Server; .. ; end
class PremiumServer < Server; ..; end
Or, in your case, if the only method is is_premium add it to the Server class and have it return the premium variable
def is_premium
self.premium
end
oh, and you should use "question" method in ruby... Although Mongoid provides these for boolean values.
def premium?
self.premium
end
Use class inheritance and the scope mechanism of Mongoid:
class Server
include Mongoid::Document
field :premium, type: Boolean, default: false
# ... basic server methods
end
class PremiumServer < Server
default_scope :premium_servers, where(premium: true)
# ... additional premium server methods
end
p_server = PremiumServer.first
p_server.<access to PremiumServer methods>
The default_scope will be used every time you do a query on PremiumServer, you do not need to call .premium_servers manually.
That is "conditional based" in another way - in a mongoid way.
Further information:
Scopes: http://mongoid.org/docs/querying/scopes.html
Inheritance: http://mongoid.org/docs/documents/inheritance.html

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.

Implementing an ActiveRecord before_find

I am building a search with the keywords cached in a table. Before a user-inputted keyword is looked up in the table, it is normalized. For example, some punctuation like '-' is removed and the casing is standardized. The normalized keyword is then used to find fetch the search results.
I am currently handling the normalization in the controller with a before_filter. I was wondering if there was a way to do this in the model instead. Something conceptually like a "before_find" callback would work although that wouldn't make sense on for an instance level.
You should be using named scopes:
class Whatever < ActiveRecord::Base
named_scope :search, lambda {|*keywords|
{:conditions => {:keyword => normalize_keywords(keywords)}}}
def self.normalize_keywords(keywords)
# Work your magic here
end
end
Using named scopes will allow you to chain with other scopes, and is really the way to go using Rails 3.
You probably don't want to implement this by overriding find. Overriding something like find will probably be a headache down the line.
You could create a class method that does what you need however, something like:
class MyTable < ActiveRecord::Base
def self.find_using_dirty_keywords(*args)
#Cleanup input
#Call to actual find
end
end
If you really want to overload find you can do it this way:
As an example:
class MyTable < ActiveRecord::Base
def self.find(*args)
#work your magic here
super(args,you,want,to,pass)
end
end
For more info on subclassing checkout this link: Ruby Tips
much like the above, you can also use an alias_method_chain.
class YourModel < ActiveRecord::Base
class << self
def find_with_condition_cleansing(*args)
#modify your args
find_without_condition_cleansing(*args)
end
alias_method_chain :find, :condition_cleansing
end
end

Resources