Rails on the Run

Rails experiments by Matt Aimonetti

Browsing Posts tagged activeRecord

ambition

By now, you should have heard about ambition if not read the latest post from the author.

Ambition has a simple goal: making you stop writing SQL in your queries and only stick to Ruby. (who cares if you use ActiveRecord, Sequel, DataMapper or another ORM)

I’m so used to the ActiveRecord way of querying the database that I was not fully convinced that Ambition would help me in my daily tasks. I still gave it a try:

Testing Ambition

$ sudo gem install ambition -y

Started my console

$ script/console

and required Ambition

require 'ambition'

I started by doing a query the AR way:

1
2
 Photo.find(:all, :conditions => "photos.title IS NULL AND photos.width > 250 
AND photos.height > 200 AND users.name = 'test'", :include => :user) 

And I converted it into an Ambition call:


Photo.select {|p| p.title == nil && p.width > 250 && p.height > 200  && p.user.name == 'test'}.entries

145 vs 102 keystrokes. 30% less typing with Ambition! I don’t know about you, but I REALLY prefer the Ruby only query, much cleaner and much “DRYer”. However, that’s not always true:


Photo.find_by_title(nil)

(24chars)


Photo.detect{|p| p.title == nil}

(32 chars)

But what’s going on behind the scene? Do we have the exact same SQL query sent to our DB?

Well, Ambition doesn’t generate any SQL, it uses AR to do so. You want to make sure Ambition is not messing with you, try that:

1
2
 >> Photo.select {|p| p.title == nil && p.width > 250 && p.height > 200  && p.user.name == 'test'}.to_hash
 => {:conditions=>"(photos.`title` IS NULL AND (photos.`width` > 250 AND (photos.`height` > 200 AND users.name = 'test')))", :include=>[:user]}

That’s pretty hot. Especially when you have to use eager loading!

Obviously you can still do stuff like that:

1
2
3
Photo.select {|p| p.title == nil && p.width > 250 && p.height > 200  && p.user.name == 'test'}.each do |photo|
 puts photo.filename
end

(note the query will only be made once)

Another cool thing, is to do simple sorting:


>> Photo.select {|p| p.title == nil && p.user.name == 'test'}.sort_by { |p| [p.created_at, -p.size] }

creates the following:


=> { :o rder=>"photos.created_at, photos.size DESC", :conditions=>"(photos.`title` IS NULL AND users.name = 'test')", :include=>[:user]}

or


=> "SELECT * FROM photos JOIN user WHERE (photos.`title` IS NULL AND users.name = 'test') ORDER BY photos.created_at, photos.size DESC"

That’s cool, and you can still sort on relationships:

1
2
3
4
5
6
7
>> Photo.select {|p| p.title == nil }.sort_by { |p| p.user.name }
=> "SELECT * FROM photos JOIN user WHERE photos.`title` IS NULL ORDER BY users.name"</macro:code >
    
Or directly on the model:

<macro:code lang="ruby">>> Photo.sort_by(&:title)
=> "SELECT * FROM photos ORDER BY photos.title"

To finish, another detail which makes Ambition a great library

1
2
3
>> Photo.any? {|p| p.title =~ /ambition/ }
=> "SELECT count(*) AS count_all FROM photos WHERE (photos.`title` REGEXP 'ambition')" 
=> true

And if you were worried that it wouldn’t work with utf8, check this out:

1
2
3
4
5
6
>> Photo.any? {|p| p.title == 'école'}
=> SET NAMES 'utf8'
=> SET SQL_AUTO_IS_NULL=0
=> SHOW FIELDS FROM photos
=> SELECT count(*) AS count_all FROM photos WHERE (photos.`title` = 'école') 
=> false

Limitations

The only limitation I found in Ambition is that Ruby code won’t work in the block, for instance:


>> Photo.select {|p| p.title == nil && p.created_at < 1.week.ago && p.user.name == 'test'}.entries

won’t work at the moment. To inspect what’s going simply try:

1
2
>> Photo.select {|p| p.title == nil && p.created_at < 1.week.ago && p.user.name == 'test'}.to_sql
=> "SELECT * FROM photos JOIN user WHERE (photos.`title` IS NULL AND (photos.`created_at` < 1.`week`.`ago` AND users.name = 'test'))"

You can see that photos.created_at < 1.week.ago is the problem.

The recommended way to achieve the same result is to use variables:

1
2
3
>> date = 1.week.ago
>> Photo.select {|p| p.title == nil && p.created_at < date && p.user.name == 'test'}.to_sql
=> "SELECT * FROM photos JOIN user WHERE (photos.`title` IS NULL AND (photos.`created_at` < '2007-09-08 19:38:48' AND users.name = 'test'))"

However, note that method calls will work just fine:

1
2
3
4
5
6
>> def time_now_please
>> Time.now
>> end
    
>> Photo.select {|p| p.title == nil && p.created_at < time_now_please && p.user.name == 'test'}.to_sql
=> "SELECT * FROM photos JOIN user WHERE (photos.`title` IS NULL AND (photos.`created_at` < '2007-09-15 19:41:37' AND users.name = 'test'))"   

Conclusion

For now, Ambition is still just wrapping ActiveRecord::Base#find but the plan is to actually generate SQL. Hopefully we’ll also be able to use Ruby code from within an Ambition block. Kickers methods are very interesting and could become a really nice way of speeding up your app and keep your code clean.

Ambition is a great query library, I think I’ll start using it whenever I have “find” calls with multiple conditions especially if my conditions are related to another model. However I still didn’t figure out how to use an inner join with Ambition.

I’m glad to announce a major update of Globalite.

First off I’d like to thank all the translators who helped with this release.

  • Globalite now support its first Asian language: Chinese!
    Ivan Chang did an awesome job creating a localization file in Chinese for Taiwan, Hong Kong and Main Land China. I’m really glad thinking that Globalite will make the Rails experience much nicer for a lot of Chinese people.

  • Ivan also pushed me to add a new feature that people had asked about: a better ActiveRecord error message support.

You know how Rails has a nice way of displaying your Model errors:

AR_error

Well, now that’s automatically translated in your locale. (as long as the new localization files are up to date. Feel free to contact me if you want to improve the locale file in your own language)

  • I also added support for pluralization directly in the translation file. (pluralization doesn’t always make sense in some languages) I’m planning on adding a better Inflector support later on.

for now, in your translation file simply use:

horse_count: we have pluralize{{count}, horse} in the ranch

In your view use the localization with arguments to pass the count:

<%= :horse_count.l_with_args({:count => @horse.count}) %>

See the wiki for more information about pluralization.

  • Finally the demo app has been updated with an example of how to grab the acceptable locale from the header. (thanks to Emmanuel Bouton)

I’m almost ready to update Globalite with 2 major changes:

  • Support for Chinese (Taiwan, Hong Kong and main land China) thanks to Ivan Chang
  • Better support for Active Record error messages in forms (based on contribution from Ivan)

I’m writing more tests and updating the sample app before releasing the updated version. It should be out in the next few days.

[Globalite page @ Google Code](http://code.google.com/p/globalite/