Arquivo

Textos com Etiquetas ‘ActiveRecord’

Obtendo dados originais de atributos modificados no ActiveRecord

22 de outubro de 2010

Como todo bom framework de Mapeamento Objeto-Relacional (ORM), o ActiveRecord do Ruby on Rails mantém os dados originais dos atributos que são alterados. Com isso, você obter esses valores antes de salvar os novos.

Vamos ver alguns métodos que lhe ajudam a saber quais atributos foram alterados, bem como seus valores novos e originais.

Como exemplo usaremos um classe de modelo chamada Order com os seguintes atributos:

Imaginando que já temos pedidos cadastrados no banco de dados, através do console, vamos recuperar um pedido e atribuí-lo a uma variável chamada order:
> order = Order.first
=> #<Order id: 8567520, user_id: 506715721, number: "R222442710", total: #<BigDecimal:4607440,'0.3498E2',8(8)>, state: "new", completed_at: "2010-08-07 17:24:37">
> y order
--- !ruby/object:Order
attributes:
id: "8567520"
number: R222442710
total: "34.98"
state: new
completed_at: 2010-08-07 17:24:37
user_id: "506715721"
attributes_cache: {}
=> nil

Através do método changed?, sabemos se houve alguma modificação nessa instância de Order:
> order.changed?
=> false

Vamos modificar os atributos number e state:
> order.number = "NEW012345"
=> "NEW012345"
> order.state = "in_progress"
=> "in_progress"
> order.changed?
=> true

Agora o método changed? retorna true, mas não informa quais atributos foram alterados. Para obter uma lista de todos atributos alterados, seus valores originais e seus valores novos, usamos o método changes:
> order.changes
=> {"number"=>["R222442710", "NEW012345"], “state”=>["new", "in_progress"]}

O retorno é um hash onde as chaves são o nome dos atributos alterados e os valores são um array com dois itens: o valor original do atributo e o novo valor do atributo.

Por exemplo, você poderia fazer isso para recuperar o valor original do atributo state:
> order.changes["state"][0]
=> “new”

Existe também o método privado changed_attributes que retorna uma hash contendo somente os valores originais dos atributos modificados. Temos acesso a ele usamos o método Object#send:
> order.send :changed_attributes
=> {"number"=>"R222442710", "state"=>"new"}

E o método (também privado) attribute_was, que recebe como parâmetro o nome de um atributo como string:
> order.send :attribute_was, "state"
=> "new"

Então agora você está pensando em tornar esse último método público? Esqueça! O ActiveRecord já fez melhor.

O módulo ActiveRecord::AttributesMethods declara para cada atributo do modelo os métodos existentes na classe em que seu nome se inicia com “attribute”. Vamos listar os métodos privados da nossa instância da classe Order que atendem esse padrão:
> y order.private_methods.grep /^attribute_/
---
- attribute_change
- attribute_before_type_cast
- attribute_changed?
- attribute_was
- attribute_will_change!
=> nil

Para cada método acima, existe seu correspondente por atributo no modelo. Basta substituir “attribute” pelo nome do atributo. Veja os exemplos com o atributo state na classe Order:
> order.state
=> "in_progress"
> order.state_changed?
=> true
> order.state_change
=> ["new", "in_progress"]
> order.state_was
=> “new”

Você pode criar novos métodos que ficam disponíveis para todos os atributos. Basta registrar o sufixo do método na classe modelo usando o método de classe attribute_method_suffix e criar um método privado com o padrão attribute_[sufixo] que recebe como parâmetro o nome do atributo.

Com um exemplo fica mais fácil de entender:

class Order < ActiveRecord::Base
  attribute_method_suffix "_cool?"

  private
  def attribute_cool?(attr)
    puts "Yes, #{attr} with value #{read_attribute(attr)} is cool!"
  end
end

> order.total_cool?
Yes, total with value 34.98 is cool!
=> nil

No final das contas, o que você precisa fazer para obter o valor original de um atributo modificado é chamar o método [nome_do_atributo]_was:
> order.number_was
=> "R222442710"
> order.state_was
=> "new"

Lembrando que depois que salvamos as alterações no banco de dados, todo esse estado de alterações é removido:
> order.save
=> true
> order.changed?
=> false
> order.changes
=> {}

Ruby , , , ,

Múltiplos construtores em Ruby (minha versão)

10 de setembro de 2010

Ontem, através de uma mensagem no Twitter do André Moreira, estava lendo um post no blog do Rinaldi Fonseca falando sobre múltiplos construtores em Ruby. Comecei a escrever um comentário, que acabou se tornando muito grande. Então achei melhor escrever aqui para expressar minha opinião a respeito.

Na primeira parte do post é mostrado a utilização de métodos de classe para construir um novo objeto passando parâmetros diferentes dos recebidos no construtor, onde foi usado esse exemplo:

class Carro
  attr_accessor :marca, :placa, :dono

  def initialize(marca, placa, dono)
    @marca, @placa, @dono =  marca, placa, dono
  end

  def Carro.carro_sem_dono(marca, placa)
    new marca, placa, "SEM DONO"
  end

  def Carro.carro_sem_placa(marca, dono)
    new marca, "SEM PLACA", dono
  end
end

carro = Carro.new "Ferrari", "ABC1234", "João"
carro_sem_dono  = Carro.carro_sem_dono "Vectra", "ABC5678"
carro_sem_placa = Carro.carro_sem_placa "Palio", "José"

puts carro.inspect           #<Carro: @dono="João", @placa="ABC1234", @marca="Ferrari">
puts carro_sem_dono.inspect  #<Carro: @dono="SEM DONO", @placa="ABC5678", @marca="Vectra">
puts carro_sem_placa.inspect #<Carro: @dono="José", @placa="SEM PLACA", @marca="Palio">

Os métodos carro_sem_dono e carro_sem_placa seguem o Factory Method Design Pattern (Padrão de Projeto Método Fábrica) e atuam como uma DSL na classe Carro. Desse modo, não há necessidade da redundância do nome da classe no ínicio de cada método fábrica:

class Carro
  attr_accessor :marca, :placa, :dono

  def initialize(marca, placa, dono)
    @marca, @placa, @dono =  marca, placa, dono
  end

  def self.sem_dono(marca, placa)
    new marca, placa, "SEM DONO"
  end

  def self.sem_placa(marca, dono)
    new marca, "SEM PLACA", dono
  end
end

carro = Carro.new "Ferrari", "ABC1234", "João"
carro_sem_dono  = Carro.sem_dono "Vectra", "ABC5678"
carro_sem_placa = Carro.sem_placa "Palio", "José"

puts carro.inspect           #<Carro: @dono="João", @placa="ABC1234", @marca="Ferrari">
puts carro_sem_dono.inspect  #<Carro: @dono="SEM DONO", @placa="ABC5678", @marca="Vectra">
puts carro_sem_placa.inspect #<Carro: @dono="José", @placa="SEM PLACA", @marca="Palio">

Já na segunda parte do post é mostrada uma maneira de se passar um bloco para o construtor da classe Carro e assim inicializar seus atributos:

class Carro
  attr_accessor :ano, :marca, :modelo, :dono, :cor, :tipo

  def initialize(&block)
    instance_eval &block
  end
end

carro = Carro.new do
  self.ano    = "2000"
  self.marca  = "Gol"
  self.modelo = "Exemplo"
  self.dono   = "Dono exemplo"
  self.cor    = "Vermelho"
  self.tipo   = "Tipo exemplo"
end

puts carro.inspect #<Carro: @ano="2000", @marca="Gol", @modelo="Exemplo", @dono="Dono exemplo", @cor="Vermelho", @tipo="Tipo exemplo">

Eu particularmente gosto do tipo de inicialização de atributos de um novo objeto permitada nas classes da camada Model de Ruby on Rails. O método new dessas classes (que herdam de ActiveRecord::Base) podem receber tanto um hash quanto um bloco com os valores dos atributos. Dessa maneira, a classe Carro poderia ser inicializada assim:

carro = Carro.new :ano => "2000",
                   :marca => "Gol",
                   :modelo => "Exemplo",
                   :dono => "Dono exemplo",
                   :cor => "Vermelho",
                   :tipo => "Tipo exemplo"

#ArgumentError: wrong number of arguments (1 for 0)

Eu disse poderia. Não pode, pois o construtor new espera um bloco e não um hash, e Carro não é uma classe Model do Rails.

Para solucionar isso, podemos modificar o construtor da classe Carro para receber um hash de atributos ao invés de um bloco. Então atribuímos cada valor presente no hash para seu respectivo atributo:

class Carro
  attr_accessor :ano, :marca, :modelo, :dono, :cor, :tipo

  def initialize(attributes = nil)
    attributes.each do |attr, value|
      self.send("#{attr}=", value)
    end unless attributes.nil?
  end
end

carro = Carro.new :ano => "2000",
                   :marca => "Gol",
                   :modelo => "Exemplo",
                   :dono => "Dono exemplo",
                   :cor => "Vermelho",
                   :tipo => "Tipo exemplo"

puts carro.inspect #<Carro: @ano="2000", @marca="Gol", @modelo="Exemplo", @dono="Dono exemplo", @cor="Vermelho", @tipo="Tipo exemplo">

Mas e se quisermos também ter a opção de inicializar a classe Carro passando um bloco? Sem problemas, usamos o comando yield passando como parâmetro self se um bloco foi fornecido:

class Carro
  attr_accessor :ano, :marca, :modelo, :dono, :cor, :tipo

  def initialize(attributes = nil)
    attributes.each do |attr, value|
      self.send("#{attr}=", value)
    end unless attributes.nil?

    yield self if block_given?
  end
end

carro = Carro.new do |c|
  c.ano    = "2000"
  c.marca  = "Gol"
  c.modelo = "Exemplo"
  c.dono   = "Dono exemplo"
  c.cor    = "Vermelho"
  c.tipo   = "Tipo exemplo"
end

puts carro.inspect #<Carro: @ano="2000", @marca="Gol", @modelo="Exemplo", @dono="Dono exemplo", @cor="Vermelho", @tipo="Tipo exemplo">

Nessa implementação sempre irá prevalecer o que vier no bloco. Então se você passar um hash e um bloco para o construtor ao mesmo tempo (o que é uma bizarrice), os atributos que coincidirem terão o valor que foi passado no bloco:

carro = Carro.new(:ano => "2000", :marca => "Gol", :modelo => "Exemplo") do |c|
  c.modelo = "MODELO DO BLOCO"
  c.dono   = "Dono exemplo"
  c.cor    = "Vermelho"
  c.tipo   = "Tipo exemplo"
end

puts carro.inspect #<Carro: @ano="2000", @marca="Gol", @modelo="MODELO DO BLOCO", @dono="Dono exemplo", @cor="Vermelho", @tipo="Tipo exemplo">

Esses eram meus comentários sobre o interessante assunto de construtores em Ruby.

Ruby , , , , , , ,

“Pretty print” para seus objetos Ruby

8 de abril de 2010

Vira e mexe, seja no irb ou no script/console, temos que exibir o conteúdo de objetos para examinar seus valores. A maneira mais comum para fazer isso é utilizando os método puts ou o método p, como no exemplo abaixo:

Esse tipo de exibição não é muito legível, principalmente se você estiver no script/console visualizando os dados de  um ActiveRecord:

No caso de objetos ActiveRecord, há a opção da exibição em YAML, chamando o método y no script/console:

Agora existe uma gem chamada awesome_print que permite exibir os dados dos objetos com identação e cores de acordo com o tipo de dado.

Primeiro vamos instalá-la:
$ sudo gem install awesome_print

E para exemplificar, vamos usar o script/console para exibir os valores de um hash. Note que é necessário fazer uma referência para o arquivo “ap”. Depois bastar chamar o método ap:

Uma coisa legal é que dependendo do tipo do valor, as cores são diferentes.

Agora veja um exemplo utilizando um ActiveRecord:

Uma ressalva negativa é a não exibição dos valores decimais, como acontece quando se exibe o ActiveRecord em YAML. Talvez para uma próxima versão isso melhore.

Há também algumas opções para customizar a forma como os dados são exibidos pela awesome_print, como por exemplo o tamanho da identação e as cores. Para mais informações, veja a documentação da gem: http://github.com/michaeldv/awesome_print.

Ruby , , , , , ,

Instalando ImageMagick no Mac OS X 10.5

4 de março de 2010

PaperClip é um plugin de upload de arquivos para Ruby on Rails que cria atributos dos arquivos nas classes ActiveRecord funcionando da mesma maneira como se estivesse utilizando campos do banco de dados.

Para utilizar esse plugin, é necessário instalar o ImageMagick, que pode ser feito via MacPorts:
$ sudo port install ImageMagick

Eu fiz isso e obtive o seguinte erro:

On Mac OS X 10.5, tiff 3.8.2 requires Xcode 3.1 or later but you have Xcode 3.0.
Error: Target org.macports.extract returned: incompatible Xcode version
Error: The following dependencies failed to build: tiff xorg-libXext xorg-libX11 autoconf help2man p5-locale-gettext m4 automake libtool xorg-bigreqsproto xorg-inputproto xorg-kbproto xorg-libXau xorg-xproto xorg-libXdmcp xorg-util-macros xorg-xcmiscproto xorg-xextproto xorg-xf86bigfontproto xorg-xtrans xorg-libXt xorg-libsm xorg-libice
Error: Status 1 encountered during processing.

.
Para instalar o ImageMagick no Mac OS X 10.5 é preciso ter o Xcode 3.1 ou superior. Eu tinha o Xcode 3.0 instalado, então fui até a página de desenvolvedores da Apple e baixei a últma versão do Xcode para Mac OS X 10.5.

O endereço é https://connect.apple.com. Você precisa se logar para ter acesso aos downloads. Se você não tem cadastro, pode criar uma nova conta.

Depois de logado, clique no link Downloads, depois no menu da direita em Developer Tools, localize a seção Xcode 3.1.4 Developer Tools e faça o download dos 993 MB do arquivo Xcode 3.1.4 Developer DVD (Disk Image).

Após instalado o Xcode 3.1.4, você já pode instalar o ImageMagick via MacPorts e depois utilizar o PaperClip.

Para mais informações sobre o PaperClip, acesse esse link:
http://delicious.com/prodis.net/paperclip

Unix , , , , , ,

Serialização de objetos em JSON com Ruby on Rails

13 de dezembro de 2009

Em um post anterior mostrei como serializar objetos em JSON utilizando .NET. Agora vamos fazer a mesma coisa com Ruby on Rails.

Esse é o Jason, não JSON.

Vamos utilizar como exemplo uma classe de modelo chamada SomeFake:

class SomeFake < ActiveRecord::Base

end

Utilizando essa migration:

class CreateSomeFakes < ActiveRecord::Migration
  def self.up
    create_table :some_fakes do |t|
      t.string :text
      t.float :value
      t.timestamps
    end
  end

  def self.down
    drop_table :some_fakes
  end
end

No script/console vamos criar uma instância do modelo SomeFake com os seguintes dados:
>> fake = SomeFake.create :text => "I am a sample text.", :value => 150.85
=> #<SomeFake id: 1, text: "I am a sample text.", value: #<BigDecimal:18ac9f0,'0.15085E3',8(12)>, created_at: "2009-12-13 19:43:28", updated_at: "2009-12-13 19:43:28">

Então queremos serializar a variável fake em JSON para obter o seguinte resultado:
{"id":1,"text":"I am a sample text.","value":150.85}

Para fazer isso, vamos chamar o método to_json na variável fake (estou usando o comando print para uma exibição melhor no console do JSON gerado):
>> print fake_json = fake.to_json
"{"some_fake": {"updated_at": "2009-12-13T19:43:28Z", "text": "I am a sample text.", "id": 1, "value": 150.85, "created_at": "2009-12-13T19:43:28Z"}}"

O resultado que obtemos não é exatamente igual ao que estávamos querendo.

Primeiro, o nome do nosso modelo foi serializado como raiz do objeto em JSON. Isso aconteceu porque por padrão em uma aplicação Rails, a opção ActiveRecord::Base.include_root_in_json é configurada para true no arquivo config/initializers/new_rails_defaults.rb. Nós podemos alterar essa opção para false nesse arquivo, o que afeta a serialização em JSON de toda a aplicação, ou podemos alterá-lo no próprio script/console para nossos testes:
>> ActiveRecord::Base.include_root_in_json = false
=> false

Agora nosso objeto serializado fica assim:
>> print fake_json = fake.to_json
"{"updated_at": "2009-12-13T19:43:28Z", "text": "I am a sample text.", "id": 1, "value": 150.85, "created_at": "2009-12-13T19:43:28Z"}"

A segunda diferença é que não queremos que os atributos de timestamps (created_at, updated_at) sejam serializados. Então vamos dizer para o método to_json não serializar esses atributos, utilizando a opção except:
>> print fake_json = fake.to_json(:except => [:created_at, :updated_at])
“{”text”: “I am a sample text.”, “id”: 1, “value”: 150.85}”

Para fazer o inverso, transformar dados em JSON para um objeto, criamos uma nova instância da classe SomeFake e chamamos o método from_json passando a variável fake_json como parâmetro:
>> other_fake = SomeFake.new
=> #<SomeFake id: nil, text: nil, value: nil, created_at: nil, updated_at: nil>

>> other_fake.from_json fake_json
=> #<SomeFake id: nil, text: "I am a sample text.", value: #<BigDecimal:1708a04,'0.15085E3',8(12)>, created_at: nil, updated_at: nil>

Caso você precise serializar objetos em JSON sem os atributos timestamps com frequência, ao invés de sempre passar a opção except para o método to_json, podemos incluir um novo método na classe ActiveRecord::Base que faça a serialização sem esses atributos. Dessa forma, todos os nossos modelos terão essa funcionalidade.

Vamos chamar esse método de to_json_no_timestamps, o qual sua implementação é mostrada abaixo:

class ActiveRecord::Base
  def to_json_no_timestamps(options = {})
    timestamps_options = [:created_at, :updated_at]

    if (options.has_key? :except)
      if (options[:except].class == Array)
        timestamps_options = options[:except] | timestamps_options
      else
        timestamps_options &amp;lt;&amp;lt; options[:except].to_sym unless options[:except].nil?
      end
    end

    options[:except] = timestamps_options

    to_json options
  end
end

E então basta chamar nosso novo método em uma instância de qualquer modelo:
>> fake = SomeFake.first
=> #<SomeFake id: 1, text: "I am a sample text.", value: #<BigDecimal:1712798,'0.15085E3',8(12)>, created_at: "2009-12-13 19:43:28", updated_at: "2009-12-13 19:43:28">

>> print fake_json = fake.to_json_no_timestamps
"{"text": "I am a sample text.", "id": 1, "value": 150.85}"

Arquitetura, Ruby , , , ,