Archive

Posts Tagged ‘rails’

How to make rails migrations pretty

February 26th, 2010

I have seen a lot of migrations, most of them was just doing what it had to do. But I found a nice way to make my migrations more pretty.

So I had an Article model, which had field :published of type :boolean , as comparing to other solutions, I found that it should be rather :publish_date of type date.
First I have created migration using “script/generate migration article_update” and got empty migration:

class UpdateArticles < ActiveRecord::Migration
  def self.up
  end
  def self.down
  end
end

To be able to freely play with Fields we add our private implementation of article model, we change the fields and move data for them:

class UpdateArticles < ActiveRecord::Migration
  class Article < ActiveRecord::Base; end
  def self.up
    add_column :articles, :publish_date, :datetime, :null => true
    Article.reset_column_information
    Article.all.each { |article| article.update_attribute(:publish_date, :created_at) if article.publish }
    remove_column  :articles, :publish
  end
  def self.down
    add_column  :articles, :publish, :boolean
    Article.reset_column_information
    Article.all.each { |article| article.update_attribute(:publish, true) unless article.publish_date.nil? }
    remove_column :articles, :publish_date
  end
end

But this is not really what we wanted, if we have already a lot of data in Article tables then we will not know what is going on during update attribute action.
To make it display pretty we have to add say_with_time function:

class UpdateArticles < ActiveRecord::Migration
  class Article < ActiveRecord::Base; end

  def self.up
    add_column :articles, :publish_date, :datetime, :null => true
    say_with_time "Updating publish_date column" do
      Article.reset_column_information
      Article.all.each { |article| article.update_attribute(:publish_date, :created_at) if article.publish }
    end
    remove_column  :articles, :publish
  end

  def self.down
    add_column  :articles, :publish, :boolean
    say_with_time "Updating publish column" do
      Article.reset_column_information
      Article.all.each { |article| article.update_attribute(:publish, true) unless article.publish_date.nil? }
    end
    remove_column :articles, :publish_date
  end
end

And after running migration we will get he following result:

==  UpdateArticles: migrating =================================================
-- add_column(:articles, :publish_date, :datetime, {:null=>true})
-> 0.0007s
-- Updating publish_date column
-> 0.0034s
-- remove_column(:articles, :publish)
-> 0.0224s
==  UpdateArticles: migrated (0.0269s) ========================================

For me it was very small amount of data and the migration went fast, but if i would run it on production with thousands of records then it may take a bit more.

I know this exact code could be rewritten using SQL - and it would be almost always done in seconds, but even then It is nice to show progress, because person responsible for deployment might be confused seeing only blinking cursor.

Did You liked this post or maybe not, vote on it at dzone.

Development ,

complex associations in rails activerecord

January 30th, 2010

During playing with CanCan from ryanb show in last railscasts I did planned some models and find me in quite strange situation - I knew the connection, but I could not easily get the result.

My models look something like:

class User < ActiveRecord::Base
  has_many :assigments
  has_many :groups, :through => :assigments
end
class Assigment < ActiveRecord::Base
  belongs_to :user
  belongs_to :group
end
class Group < ActiveRecord::Base
  has_many :assigments
  has_many :users, :through => :assigments
  has_many :responsibilities
  has_many :roles, :through => :responsibilities
end
class Responsibility < ActiveRecord::Base
  belongs_to :role
  belongs_to :group
end
class Role < ActiveRecord::Base
  has_many :responsibilities
  has_many :groups, :through => :responsibilities
end

So my first try was to get roles through groups manually:

User.first.groups.map{|g|
  g.roles
}.flatten.map{|r|
  r.name.to_sym
}.uniq

But this solution is waste of resources, for system with a lot of possible roles it might be very inefficient, it takes first all groups of user and then ittereting through them gets all it’s roles. it will generate a lot of database queries.
So after some searching I got following code:

User.first.roles.map{|r|r.name.to_sym}.uniq

To make it working I used one quite nice feature of Active record - :finder_sql - so in user.rb I have added following line:

has_many :roles, :finder_sql => 'SELECT r.* FROM users u LEFT JOIN assigments a ON u.id=a.user_id LEFT JOIN responsibilities res ON a.group_id=res.group_id LEFT JOIN roles r ON res.role_id=r.id'

This makes it possible to get all the roles for a user just in one call, everything calculated on database side. Unfortunately there is one down side of this - the SQL code might be not portable to other databases, so use it only when You are sure You will stick to one database.
There is also other way around, it allows to get the same data in two sql calls, without using SQL queries, just on pure ActiveRecord usage:

Responsibility.find_all_by_group_id(
  u.assigments(:select=>'assigments.group_id').map{|a|a.group_id},
  :select=>'roles.name',
  :joins=>:role
).map{|r| r.name.to_sym }.uniq

There are many ways to archive the same goal, knowing them is only an part - knowing how they work, makes us aware how to chose the path.

on 2009-01-31 13:15 added:
Another way going out of the Group class:

Group.find(
  :all,
  :select=>'roles.name',
  :joins=>[:users,:roles],
  :conditions=>{:users=>{:id=>1}}
).map{|g|g.name.to_sym}.uniq

Did You liked this post or maybe not, vote on it at dzone.

Development , , ,

exception iteration in ruby

January 29th, 2010

There are many ways to iterate in ruby, many methods, but going back to assembler roots of mine I found another one:

>> a=[1,2,3]
=> [1, 2, 3]
>> begin puts a.shift; raise unless a.blank?; rescue; retry; end
1
2
3
=> nil

This code is only prof of concept, it should not be used for iterations, by example if it would be used without prior initialization of “a” variable it would be an endless loop as on exception “NameError” it will retry in endless loop.

There are other places where this could be used, but even possible retry should not be used without some kind of endless loop protection like:

>> i=10; begin puts IO.read('some.file'); rescue; if i>0 then puts "#{i} ..."; i-=3; sleep 1; retry; end; end
10 ...
9 ...
8 ...
7 ...
some file created
=> nil

In this example a file was created during run of the command - on second console by just running “echo some file created > some.file”.

There are other ways to do the same, maybe not so cool like that above but probably most of developers will prefer something like:

>> 10.downto(1){|i| begin puts IO.read('some.file'); break; rescue; puts "#{i} ..."; sleep 3; end }
10 ...
9 ...
8 ...
some file created
=> nil

Did You liked this post or maybe not, vote on it at dzone.

Development , , ,

string to class in ruby on rails

January 26th, 2010

There are few ways to have a class from a string, most know are:

  • Kernel.const_get(’User’)
  • eval(’User’)
  • ‘User’.constantize

The order in which I have named them is important - a bit important, Kernel.const_get is 10 times faster then constantize and 5 times faster then eval.

The reason of such speed for Kernel.const_get is from it has to maintain list of all constants in the application, tests show it may even behave faster then storing names and Class mapping in a hash.

The difference is not big on simple calls to create just one or two classes, but on heavy loaded systems this might give some more percents of the hardware.

Did You liked this post or maybe not, vote on it at dzone.

Development , , , , ,

Rails auto_complete nested list

June 9th, 2009

Yesterday I was implementing auto completion for categories. The idea was to get possibility to write by hand categories but keep them as has_many_through association in database.

After six hours of coding I got this working - that is why I’m against rails, with such big amount of plugins, gems and blogs you have to spent a lot of time to do small things.

This code works with rails 2.3

So first we have to install auto_complete plugin which was quite handy:

script/plugin install auto_complete
script/plugin discover

Second we need to define migration:

  create_table :book_categories do |t|
    t.references :books
    t.references :categories
  end
  create_table :books do |t|
    t.string :title
  end
  create_table :categories do |t|
    t.string :name
  end

At third step we define models:

class Book < ActiveRecord::Base
  has_many :book_categories, :dependent => :destroy
  has_many :categories, :through => :book_categories
end
class BookCategory < ActiveRecord::Base
  belongs_to :book
  belongs_to :category
end
class Category < ActiveRecord::Base
  has_many :book_categories, :dependent => :destroy
  has_many :books, :through => :book_categories
end

Time for forth step - faking Book properties:

class Book < ActiveRecord::Base
has_many :book_categories, :dependent => :destroy
has_many :categories, :through => :book_categories
def categories_list
self.categories.map{|c| c.name}*”, ”
end

def categories_list=(list)
list_names = list.split(’,').map{ |e| e.strip }.uniq
list_existings = Category.find(:all, :conditions => [ 'name IN (?)', list_names ], :select => ‘id,name’ )
list_existings_names = list_existings.map { |e| e.name }
list_add = list_names-list_existings_names
list_new = list_existings.map { |e| e.id }
list_old = self.categories.map { |e| e.id }.uniq
list_add.each do |name|
self.categories << Category.new(:name => name)
end
self.categories << Category.find(:all, :conditions => [ 'id IN (?)', list_new-list_old ], :select => ‘id’ )
self.categories.delete Category.find(:all, :conditions => [ 'id IN (?)', list_old-list_new ], :select => ‘id’ )
end
end

Now we can have some rest from coding and play a bit with new models using “script/console”:

>> Category.new(:name => 'a').save
>> Category.new(:name => 'b').save
>> Category.new(:name => 'c').save
>> Category.all.map{|c| [c.id,c.name] }
=> [[1, "a"], [2. "b"], [3, "c"]]
>> b = Book.new(:title=>'some book')
>> b.categories_list='a,b,c'
>> b.categories
=> [#<Category id: 1>, #<Category id: 2>, #<Category id: 3>]
>> b.categories_list='a,c,d'
>> b.categories
=> [#<Category id: 1>, #<Category id: 3>, #<Category id: nil, name: "d">]
>> b.save
>> Category.all.map{|c| [c.id,c.name] }
=> [[1, "a"], [2. "b"], [3, "c"], [4, "d"]]

Fifth step is to write edit/new view:

<% form_for(@book) do |f| %>
  <div>
    <%= f.label :title, t(:book_label_title) %><br />
    <%= f.text_field :title %>
  </div>
  <div>
    <%= f.label :categories_list, t(:book_label_category) %><br />
    <%= text_field_with_auto_complete :book, :categories_list, {}, { :method => :get } %>
  </div>
<% end %>

Six step is to define action in controller:

class BooksController < ApplicationController
  def auto_complete_for_book_categories_list
    list = params['book']['categories_list'].split(',').map{ |e| e.strip }

    name = list.pop

    find_options = {
      :conditions => [ "name LIKE ?", '%' + name + '%' ],
      :order => "name ASC",
      :limit => 10,
      :select => 'id, name',
    }

    @items = Category.find(:all, find_options).select { |e| !list.include?(e.name) }.
      map { |e| list.push(e.name); e.name=(list*', '); list.pop(); e }

    render :inline => "<%= auto_complete_result @items, 'name' %>"
  end
  #other actions go here ...
end

The seventh and last step is to add routing:

map.resources :books, :collection => { :auto_complete_for_book_categories_list => :get }

Hope that this will help someone.

Development , ,

Get Adobe Flash playerPlugin by wpburn.com wordpress themes