Archive

Posts Tagged ‘nesting’

Deep associations in rails activerecord

May 22nd, 2010

Some time ago I wrote about complex associations, now time to add another method and corrections.

First the finder_by_sql, in that particular case It was necessary to add

:readonly => true

So the code looks now like this:

  has_many :roles,
    :readonly => true,
    :finder_sql => '
SELECT roles.name FROM roles
INNER JOIN responsibilities ON roles.id = responsibilities.role_id
INNER JOIN assigments ON responsibilities.group_id = assigments.group_id
WHERE assigments.user_id = #{id}
GROUP BY roles.id
  '

There is one realy big downside of using finder_sql - it does not work with find_by_… or named scopes, so this forced me to continue searching and this is the result:

  def roles
    Role.scoped(
     {
       :joins => { :responsibilities => { :group => { :assigments => :user } } },
       :conditions => {"users.id" => id},
       :group => "roles.id"
     }
   )
  end

and now I can write:

user.roles.by_name(:admin).count

where by_name is an named scope

  named_scope :by_name, lambda { |type| {:conditions => [ "roles.name = ", type.to_s ] } }

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 , , ,

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