Tigraine

Daniel Hoelbling-Inzko talks about programming

Mongoid gotcha: Id equality comparisons

One quick thing I just ran into in my Rails app with Mongoid:

Ids in model instances are not Strings objects and thus their == method doesn't work against normal Strings.

This means:

@post.id == '51abcb7b421aa90cdf000146'   # always false

will never be true unless you explicitly cast the ObjectId to a string:

@post.id.to_s == '51abcb7b421aa90cdf000146'  # correct comparison

Oh and yes: you shouldn't do things like that anyways in your code - but if you have to - be aware.

Filed under mongoid, rails, mongodb

Mongoid .length on query ignores limit

I just ran into this and wanted to share this quite puzzling behavior:

20.times { Fabricate(:post) }
Post.scoped.limit(10).length.should eq(10)
# => Test failed
#    expected: 12
#         got: 20

Weird right? But once you look at MongoDB's capabilities and the Mongoid implementation you'll quickly discover that this is only kind of a semi-bug :). MongoDB supports an .count() command that takes a filter and returns the number of matches documents. This command is being issued by Mongoid whenever you call .length on a criterion (since it's much cheaper than executing the actual query and counting the returned objects).

So don't write tests like this:

# method under test:
def self.most_played
  self.order_by(:play_count.desc).limit(12)  
end

# test
it 'returns the 12 most played items' do
  Video.most_played.length.should eq(12)
end

This test will fail, even though the method is doing everything perfectly right. I'd even say that in a regular ActiveRecord setting this would be a passing test. But since I am utilizing the delayed execution functionality of Mongoid most_played method is only returning a Criteria object that represents the query and can be composed into a more complicated query.

This causes .length to be executed on Criteria where it causes Mongoid to call the .count() method instead of running .length on Array. Since limit is no query parameter it will not be passed to MongoDB and the returning count will correctly be equal to the total number of Documents in the collection.

The more correct way to write that test is to actually force Mongoid to execute it's query and count the result:

it 'returns the 12 most played items' do
  Video.most_played.to_a.length.should eq(12)
end
Filed under mongoid, rails, ruby

Mongoid i18n and fallback locales

I may have hinted at it once or twice that I really love how Rails makes Internationalization (I18n) really simple (in stark contrast to what I had to do back then in .NET). But one thing Rails I18n will not do for you is translating the models in your database, which is to me one of the major problems I often face at work when doing I18n. (Of course there are gems that alleviate that pain)

So today I was sitting down to write a system design for a new application at work that has to be localized in a lot of different languages while still being fast. After thinking a bit about the problem I decided that doing so in ActiveRecord would be cumbersome to say the least, and I decided for a Document-Database approach. Since I know MongoDB best I went out and looked at the Mongoid documentation on how to best integrate transparent localization into the system (fully expecting that I'd have to implement this myself).

Well, Mongoid blew my mind:

class Post
  include Mongoid::Document

  field :title, localized: true
end

That's it, Mongoid will now transparently store the title field in a hash and return the correct value depending on I18n.locale. We are talking literally zero cost here. Mongoid even accounts for the reality of translations not always being present so you might want to fallback to another locale instead of not presenting anything. Wow..

So here is how to set this up with fallbacks inside your config/application.rb:

config.i18n.fallbacks = true
config.i18n.default_locale = :en

That's all, and here is how it works:

post = Post.new(:title => 'Hello World')

puts post.title # => "Hello World"
I18n.locale = :de
puts post.title # => "Hello World"

post.title = "Hallo Welt"
puts post.title # => "Hallo Welt"

I18n.locale = :en
puts post.title # => "Hello World"

Unfortunately this at the moment only works for regular fields, not for relations so I still have to do some work in my current project - but even the field i18n saves me a lot of headache.

Multiple fallbacks: Something that is not really covered in the documentation but becomes apparent when you look at the tests is that you can define a callback chain per locale. Meaning you can tell Mongoid to do go through a list of locales while searching for an existing value.

::I18n.fallbacks[:de] = [ :de, :en, :es ]

Mongoid will now first look for a german value, then fall back to the english one, and if that's not present either it will try the spanish one. Very handy especially if you run the locale schema down to country codes like 'de-AT', 'de-CH'. So for example this is the fallback chain for switzerland:

::I18n.fallbacks['de-CH'] = ['de-DE', 'de', 'en']
::I18n.fallbacks['fr-CH'] = ['fr-FR', 'fr', 'en']
::I18n.fallbacks['it-CH'] = ['it-IT', 'it', 'en']

Yes, Switzerland is a mess - but at least it gives you a good example for crazy fallbacks.

Filed under mongoid, ruby

My Photography business

Projects

dynamic css for .NET

Archives

more