Fernando Luizão

Desenvolvimento de software e nerdices em geral

Posts Tagged ‘single table inheritance

Rails: Otimizando STI com default_scope

with 8 comments

NOTA: se vc não faz idéia do que é STI, veja esse link.

Não dá pra negar que STI (Single Table Inheritance) é um recurso bacana do Rails. Claro que não é bom abusar, mas em alguns casos, ela se encaixa muito bem. No rails, quando queremos usar STI, basta ter uma coluna type do tipo string na tabela “mãe”, e nessa coluna o Rails se encarrega de gravar o nome da classe do registro, e a partir daí tudo funciona normalmente (para mais detalhes sobre como funciona STI no Rails, veja a api). Porém, apesar da implementação do Rails ser muito elegante e funcionar bem, ela tem alguns problemas:

  • Comparações entre strings são lentas. Mesmo que haja um índice na coluna “type”, comparações entre strings ainda são bem mais lentas que comparações entre inteiros.
  • Caso o model seja renomeado, os dados da coluna “type” terão que ser atualizados, ou ficaremos com dados incorretos na tabela. Para corrigir podemos criar uma migration, mas é mais uma coisa para se preocupar 😉

Vamos tentar melhorar isso!

Criando um projetinho de exemplo (NOTA: estou usando Rails 2.3.3. Se usa uma versão mais antiga, veja algumas considerações mais abaixo):

rails sti
cd sti
script/generate model pessoa nome:string documento:string tipo:integer

Estamos criando nossa tabela base, e em vez de usar uma coluna type do tipo string, vamos usar uma coluna chamada tipo (o nome da coluna poderia ser qualquer um), do tipo inteiro.

Vamos editar nossa migração e adicionar um índice na migration para melhorar a performance nas buscas:

class CreatePessoas < ActiveRecord::Migration
  def self.up
    create_table :pessoas do |t|
      t.string :nome
      t.string :documento
      t.integer :tipo
      t.timestamps
    end

    #adicionando o indice
    add_index :pessoas, &#91;:id, :tipo&#93;
  end

  def self.down
    drop_table :pessoas
  end
end
&#91;/sourcecode&#93;

Criando o banco e nossa tabela de testes:

&#91;sourcecode language='ruby'&#93;
rake db:create
rake db:migrate
&#91;/sourcecode&#93;

Alterando o model <strong>Pessoa</strong> para adicionar algumas constantes com os tipos de pessoas:


#app/models/pessoa.rb
class Pessoa < ActiveRecord::Base
  # tipos de pessoa
  JURIDICA = 1
  FISICA   = 2
end
&#91;/sourcecode&#93;

Agora, vamos criar dois models que herdam de <strong>Pessoa</strong>, <strong>PessoaFisica</strong> e <strong>PessoaJuridica</strong>, e definir os seus <em>default_scope</em>s:


#app/models/pessoa_fisica.rb
class PessoaFisica < Pessoa
  default_scope :conditions => {:tipo => FISICA}
end
#app/models/pessoa_juridica.rb
class PessoaJuridica < Pessoa
  default_scope :conditions => {:tipo => JURIDICA}
end

Só isso? Só :). Aparentemente, o Rails está bem espertinho, e quando instânciamos um objeto ele já se encarrega de colocar o valor correto na coluna tipo, da mesma forma que STI faz:

Pessoa.new
#<Pessoa id: nil, nome: nil, documento: nil, tipo: nil>
PessoaJuridica.new
#<PessoaJuridica id: nil, nome: nil, documento: nil, tipo: 1>
PessoaFisica.new
#<PessoaFisica id: nil, nome: nil, documento: nil, tipo: 2>

Criando algumas pessoas:

Pessoa.create(:nome => "Pessoa indefinida")
PessoaFisica.create(:nome => "João da Silva", :documento => "123.456.789-10")
PessoaFisica.create(:nome => "José Oliveira", :documento => "000.111.222-33")
PessoaJuridica.create(:nome => "Padaria pão quentinho", :documento => "26.721.163/0001-52")
PessoaJuridica.create(:nome => "Transportadora Leva e Traz", :documento => "70.562.197/0001-31")

Buscando:

>> Pessoa.all
# SELECT * FROM "pessoas"
=> [#<Pessoa id: 1, nome: "Pessoa indefinida", documento: nil, tipo: nil, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">, #<Pessoa id: 2, nome: "João da Silva", documento: "123.456.789-10", tipo: 2, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">, #<Pessoa id: 3, nome: "José Oliveira", documento: "000.111.222-33", tipo: 2, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">, #<Pessoa id: 4, nome: "Padaria pão quentinho", documento: "26.721.163/0001-52", tipo: 1, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">, #<Pessoa id: 5, nome: "Transportadora Leva e Traz", documento: "70.562.197/0001-31", tipo: 1, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">]

>> PessoaJuridica.all
# SELECT * FROM "pessoas" WHERE ("pessoas"."tipo" = 1)
=> [#<PessoaJuridica id: 4, nome: "Padaria pão quentinho", documento: "26.721.163/0001-52", tipo: 1, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">, #<PessoaJuridica id: 5, nome: "Transportadora Leva e Traz", documento: "70.562.197/0001-31", tipo: 1, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">]

>> PessoaFisica.all
# SELECT * FROM "pessoas" WHERE ("pessoas"."tipo" = 2)
=> [#<PessoaFisica id: 2, nome: "João da Silva", documento: "123.456.789-10", tipo: 2, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">, #<PessoaFisica id: 3, nome: "José Oliveira", documento: "000.111.222-33", tipo: 2, created_at: "2009-08-12 01:08:29", updated_at: "2009-08-12 01:08:29">]

Para quem ainda usa uma versão do Rails menor que 2.3 e maior que 2.0, uma solução é instalar o plugin com o backport do default_scope:

script/plugin install git://github.com/duncanbeevers/default_scope.git

Além disso, será necessário usar uma callback para setar a coluna tipo com o valor correto. Por exemplo:

class PessoaFisica < Pessoa
  default_scope :conditions => {:tipo => FISICA}
  before_save { |p| p.tipo = FISICA }
end

Para quem usa uma versão do mais antiga que a 2, já passou da hora de atualizar hem :).

Finalizando

Conseguimos o mesmo comportamento da STI, sem as desvantagens citadas anteriormente, usando apenas algumas linhas de código a mais (os default_scopes e as constantes que definimos, no total 4 linhas). Espero que essa dica seja útil, e lembre-se, aprecie STI com moderação :).

Advertisements

Written by fernandoluizao

August 12, 2009 at 1:28 am