Textos categorizados 'Ruby'

O poder do Ruby

Muito se fala das vantagens do Ruby sobre muitas das linguagens atuais, por ser uma linguagem de altíssimo nível. Mas muitas vezes não percebemos grandes diferenças entre as linguagens, além das usuais diferenças de sintaxe - se as linhas de comando precisam de ponto-e-vírgula no final, se as variáveis precisam de $ no início do nome, se estas são tipadas ou não, se precisam ser declaradas ou não, etc. Porém, em algumas situações específicas, enxergamos o verdadeiro poder do Ruby.

Hoje precisei implementar uma paginação de resultados de busca. Eu sei que existem plugins para Rails que simplificam esta tarefa, porém como esta busca não é feita no banco de dados, e sim através de um indexador que já retorna resultados paginados, optamos por fazer manualmente a lista de links para as páginas.

Por exemplo: ao realizar uma busca, são exibidos os 10 primeiros resultados, com um link para a próxima página e a lista de links para cada página. Como o número de páginas, teoricamente, não tem limite, fiz o seguinte:

  • caso o resultado tenha até 10 páginas, todas são exibidas
  • caso o resultado tenha mais de 10 páginas, são exibidas apenas 10, sendo que:
    • caso a página atual seja uma das 6 primeiras, exibe os links para as páginas 1 a 10
    • caso a página atual seja maior que 6, exibe da página atual - 5 até a página atual + 4
    • caso a página atual seja uma das 10 últimas, exibe as 10 últimas

Pela descrição acima, percebemos que é uma lógica bem simples, porém meio chata para ser implementada - na maioria das linguagens atuais, isto exigiria um grande número de if’s aninhados, para verificarmos se as condições descritas acima são atendidas. Porém, em Ruby o código ficou muito simples e enxuto:


if (num_pages > 1)
  page_start = [1, page-5].max
  page_end = [num_pages, page+4].min
  if num_pages > 10
    page_start = [page_start, num_pages-9].min
    page_end = [page_end, 10].max
  end
  page_start.upto(page_end) {|p|
    # Exibe os links
  }
end

Definição de nomes de atributos “humanizados”

A classe ActiveRecord::ConnectionAdapters::Column tem um método human_name que cria uma versão “humanizada” para os nomes das colunas de tabelas (atributos de um model). Porém, nem sempre o nome criado é o que desejamos. Por exemplo, se temos uma coluna num_usuarios, o método human_name retornará “Num Usuarios”, que, provavelmente, não é o que queremos. Para configurar o human_name manualmente, há duas soluções:

1 - criar um hash e redefinir o método human_attribute_name:


class Model < ActiveRecord::Base
  HUMANIZED_ATTRIBUTES = {
    num_usuarios => 'Número de usuários'
  }

  def self.human_attribute_name(attr)
    HUMANIZED_ATTRIBUTES[attr.to_sym] || super
  end
end

2 - usar o plugin human_attribute_override. Esta solução é mais simples e elegante:


class Model < ActiveRecord::Base
  attr_human_name :num_usuarios => 'Número de usuários'
end

Configurações fora do padrão em Rails, parte 2 - Relacionamentos HABTM

Há um mês escrevi um post sobre configurações fora do padrão em Rails, onde descrevi como executar testes com models cujas tabelas não existem no banco de dados local, e sim em uma base externa. Porém, depois de postar, verifiquei que há um outro problema não resolvido com a configuração que descrevi nesse post: relacionamentos HABTM (has and belongs to many).

Nos relacionamentos HABTM, normalmente, há dois models, um correspondente a cada tabela do banco de dados. Como a relação entre eles é de muitos para muitos, há uma terceira tabela no banco de dados, que é responsável pela associação das demais tabelas. Como essa tabela só costuma ter dois campos, que são FK’s correspondentes às PK’s dessas tabelas, ela não precisa ter um model; basta criar o relacionamento dos dois models como has_and_belongs_to_many, passando como parâmetro join_table essa tabela intermediária.

A configuração descrita no post anterior carrega manualmente os fixtures de cada model, porém não carrega fixtures correspondentes à tabela intermediária. Para isso, precisei implementar um novo método na classe Test::Unit::TestCase (arquivo test/test_helper.rb):


def set_habtm_fixtures(class1, class2)
  return unless (class1.reflections && class1.reflections.values)
  id1 = nil
  id2 = nil
  table = nil

  # Verifica qual dos relacionamentos do model class1 está associado à tabela class2
  class1.reflections.values.each do |r|
    # Se a classe associada for class2 e for uma relação HABTM, le os FK's e o nome da tabela
    if (r.klass == class2 && !r.instance_values['options'][:join_table].nil?)
      id1 = r.primary_key_name
      id2 = r.association_foreign_key
      table = r.instance_values['options'][:join_table]
      break
    end
  end
  return if table.nil?
  connection = class1.connection

  data = File.open(File.join(RAILS_ROOT, 'test', 'fixtures', "#{table}.yml")).readlines.join
  result = ERB.new(data).result
  parsed = YAML.load(result)

  # Exclui todos os registros da tabela
  connection.execute "DELETE FROM #{table}"

  parsed.values.each do |value|
    value1 = value[id1] || 'NULL'
    value2 = value[id2] || 'NULL'
    connection.execute "INSERT INTO #{table} (#{id1}, #{id2}) values (#{value1}, #{value2})"
  end
end

Este método ficou bem “feio”, pois, como não existe um model correspondente a esta tabela, precisei criar a query manualmente. O método recebe dois nomes de classes (ActiveRecord) como parâmetro. Primeiramente é verificado qual dos relacionamentos do model class1 está associado a class2, para descobrir quais são as FK’s e o nome da tabela. Em seguida, os registros desta tabela são excluídos, e cada linha do arquivo de fixtures é carregada (usando a conexão de um dos ActiveRecords).

Além disso, modifiquei o método set_fixtures desta mesma classe, criado no post anterior, pois percebi que não era necessário passar o nome da tabela como parâmetro, basta usar o método table_name:


def set_fixtures (class_name)
  table = class_name.table_name
  return unless class_name.kind_of?(ActiveRecord::Base)

  # Define a conexao usada pela classe
  ActiveRecord::Base.connection = base.connection
  Fixtures.create_fixtures(File.join(RAILS_ROOT, 'test', 'fixtures'), table) { base.connection }
end

Para exemplificar como usar estes métodos, imagine um cadastro de usuários com grupos, onde um usuário pode fazer parte de mais de um grupo. Neste exemplo, teríamos um model Usuario (tabela usuarios), um model Grupo (tabela grupos) e uma tabela usuarios_grupos, sem um model correspondente. Na classe de teste do model Usuario, teríamos o seguinte:


class UsuarioTest < ActiveSupport::TestCase
  def setup
    set_fixtures(Usuario)
    set_fixtures(Grupo)
    set_habtm_fixtures(Usuario, Grupo)
  end

  # Testes
end

No método setup, que é executado automaticamente quando os testes são executados, as duas chamadas a set_fixtures carregam as fixtures das tabelas de usuários e grupos, respectivamente; a chamada a set_habtm_fixtures atualiza a tabela usuarios_grupos.

Como criar accessors para atributos de classe

Os métodos attr_reader, attr_writer e attr_accessor do Ruby servem para simplificar a criação de setters e getters para atributos de instância. Ex:


class Teste
  @valor = 1
  attr_accessor :valor
end

No código acima, o método attr_accessor já cria o getter e o setter para o atributo valor:


t = Teste.new
t.valor = 10
puts t.valor #=> 10

Porém, como fazer o mesmo para atributos de classe? Eu fiz essa pergunta no forum RubyOnBr. O Shairon Toledo me respondeu com o código do método attr_static_accessor, que é, na prática, o equivalente ao attr_accessor, só que para atributos de classe. Eu complementei o código dele com os métodos attr_static_reader e attr_static_writer:


class Module
  def attr_static_reader(*args)
    args.each do |meth|
      init_var(meth)
      set_reader(meth)
    end
  end

  def attr_static_writer(*args)
    args.each do |meth|
      init_var(meth)
      set_writer(meth)
    end
  end

  def attr_static_accessor(*args)
    args.each do |meth|
      init_var(meth)
      set_reader(meth)
      set_writer(meth)
    end
  end

  private
  def init_var(var_name)
    var = "@@#{var_name}".to_sym
    self.send(:class_variable_set, var, nil) unless self.send(:class_variable_defined?, var)
  end

  def set_reader(var_name)
    self.class.send(:define_method, var_name) {
      self.send(:class_variable_get, "@@#{var_name}".to_sym)
    }
  end

  def set_writer(var_name)
    self.class.class_eval %Q{
      def #{var_name}=(value)
        self.send(:class_variable_set, "@@#{var_name}".to_sym,value)
      end
    }
  end
end

Agora é possível fazer o seguinte:


class Teste
  @@valor = 1
  attr_static_accessor :valor
end

puts Teste.valor #=> 1
Teste.valor = 10
puts Teste.valor #=> 10

Executando testes em partes

A execução de testes em Rails é feita, normalmente, com o comando rake test. Este comando executa automaticamente todos os testes unitários e funcionais. Porém, algumas vezes queremos executar apenas uma parte dos testes, seja porque sabemos que outra parte do sistema está dando algum erro que pretendemos tratar depois, seja porque acabamos de alterar um trecho do código (ou dos testes) e queremos verificar se estes estão OK.

Para executar apenas os testes unitários, executamos o comando rake test:units. Para os testes funcionais, rake test:funcionals. Mas se quisermos executar um arquivo de testes específico, devemos trocar o rake pelo próprio ruby:


ruby test/unit/usuario_test.rb

Para sermos ainda mais específicos, e executarmos um único método, basta acrescentar o parâmetro --name:


ruby test/unit/usuario_test.rb --name test_dados

Outra vantagem de testar executando o ruby diretamente (sem o rake) é que não ocorre o problema de tabelas não existentes no ambiente de desenvolvimento, como ocorre com o rake.

Referência:

Atualização do Brazilian Rails

Quem já tentou tratar caracteres acentuados em Ruby deve ter percebido que a linguagem não considera estes caracteres como letras. Métodos como upcase e downcase são “locale insensitive”, como diz a descrição destes métodos no manual do Ruby:

str.downcase => Returns a copy of str with all uppercase letters replaced with their lowercase counterparts. The operation is locale insensitive—only characters “A’’ to “Z’’ are affected.

O projeto Brazilian Rails foi criado com o objetivo de resolver este e outros problemas. O projeto é instalado como um plugin para Rails, e define novos métodos para a classe String, como o upcase_br e o downcase_br, que podem ser usados em substituição aos métodos originais. O plugin acrescenta ainda outras funcionalidades, como data, hora, feriados, dinheiro e mensagens de erro, todas adaptadas ao português brasileiro.

Ontem enviei um patch para o projeto, contribuindo com alguns novos métodos para Strings. Em breve a versão atualizada deverá ser disponibilizada, conforme o post do Celestino Gomes.

Configurações fora do padrão em Rails

O framework Rails é excelente na tarefa de automatizar o máximo possível o desenvolvimento de aplicações web. Todos os detalhes que podem ser implementados automaticamente são abstraídos do desenvolvedor, facilitando e agilizando muito o trabalho.

Porém, toda essa automatização e abstração de detalhes internos tem um custo, que é a padronização. Por exemplo, ao usar o script generate para criar um model, o Rails cria uma classe derivada de ActiveRecord::Base. Os nomes do arquivo, da classe e da tabela são automaticamente gerados a partir do parâmetro passado. Ex:

ruby script/generate model tipo_usuario

Arquivo: tipo_usuario.rb

Classe: TipoUsuario

Tabela: tipo_usuarios

Neste caso, a partir do parâmetro passado para o script, o nome do arquivo é o próprio parâmetro com “.rb” no final, o nome da classe é gerado retirando-se os “_” e colocando a primeira letra de cada palavra em maiúscula, e o nome da tabela é o plural - que pode ser redefinido no arquivo config/initializers/inflections.rb:


Inflector.inflections do |inflect|
  inflect.irregular 'tipo_usuario', 'tipos_usuario'
end

Com a configuração acima, o nome da tabela será tipos_usuario - desde que esta configuração seja realizada antes do generate!

O principal problema que esta padronização traz é que nem sempre queremos/podemos seguir o padrão de configuração gerado pelo Rails. Na maioria das situações, é possível definir explicitamente quando se quer usar uma configuração diferente, como no caso do plural da tabela acima. O nome da tabela associado a um model também pode ser alterado, através do método set_table_name. Desta forma, o Rails não amarra totalmente o desenvolvedor aos seus padrões.

Porém, apesar da flexibilidade, essas configurações fora dos padrões nem sempre funcionam como deveriam. Muitas vezes, ocorre algum “efeito colateral” que faz com que algum outro módulo não funcione corretamente. Conforme escrevi no post Acessando múltiplos bancos de dados em Rails, ao configurar o acesso a múltiplos bancos de dados, os unit tests pararam de funcionar. Mais uma vez, precisei passar algum tempo pesquisando no Google para encontrar uma solução.

O primeiro problema é com os fixtures. Quando o nome da tabela é diferente do model, o nome do arquivo de fixtures deve ser igual ao nome da tabela seguido por “.yml”. Nos unit tests, o símbolo passado para o método fixtures deve ter este nome. No artigo sobre acesso a múltiplos bancos de dados, citei como exemplo uma tabela usuario_tb. Neste caso, o arquivo de fixtures deveria se chamar usuario_tb.yml, e o arquivo com unit tests deveria carregar esses fixtures assim:


class UsuarioTest < ActiveSupport::TestCase
  fixtures :usuario_tb
end

Em casos onde a única configuração fora do padrão seja o nome da tabela, a configuração acima é suficiente. Porém, no caso do exemplo citado no artigo anterior, onde o model não é mapeado numa tabela local, e sim num outro banco de dados, surgem outros problemas. O primeiro é que, ao tentar executar os testes, o rake informa que a tabela não existe. A única maneira que descobri para corrigir este problema foi criar esta tabela no ambiente de desenvolvimento. Ela não é usada para nada, mas se não for criada, os testes não funcionarão.

O segundo problema com esta configuração é que não é possível carregar os fixtures da maneira descrita acima. Em vez disso, é necessário usar o método create_fixtures da classe Fixtures. Para simplificar, como podem haver mais models configurados desta maneira, fiz a seguinte configuração:

1 - criar o método set_fixtures no arquivo test/test_helper.rb:


def set_fixtures (table, base)
  return unless (table && base.kind_of?(ActiveRecord::Base))

  ActiveRecord::Base.connection = base.connection
  Fixtures.create_fixtures(File.join(RAILS_ROOT, 'test', 'fixtures'), table) { base.connection }
end

2 - criar o método setup nos unit tests, utilizando esse método para cada arquivo de fixtures que será carregado:


def setup
  set_fixtures('usuario_tb', AutenticacaoDatabase)
end

No código acima, AutenticacaoDatabase é a classe abstrata usada como base para o model Usuario.

Com essas configurações é possível realizar os testes, com um único problema: não é possível referenciar os fixtures pelos labels. Para simplificar, como ainda não descobri uma maneira de corrigir isto, eu costumo definir os labels dos fixtures como números seqüenciais, começando em 0. Assim eu posso buscar todos os dados da tabela (ex: Usuario.find(:all)) e referenciar o array pelos índices, que, neste caso, correspondem aos labels. Mas cuidado: o find(:all) não garante que o array retornado terá a mesma ordem do arquivo de fixtures. O ideal é criar fixtures com id seqüencial, e adicionar o parâmetro order ao find.

Referências:

Acessando múltiplos bancos de dados em Rails

Esta semana me deparei com uma situação no projeto em Rails em que estou trabalhando, onde necessitei acessar um banco de dados de outra aplicação, para fazer algumas consultas simples. Eu poderia simplesmente fazer a conexão com o banco e as querys em SQL. Porém, eu obviamente queria aproveitar as facilidades que o Rails proporciona ao abstrair estes detalhes com a classe ActiveRecord::Base.

Quando criamos um novo model em Rails, a nova classe criada, derivada de ActiveRecord::Base, é mapeada por padrão em uma tabela no banco de dados da aplicação - o Rails assume o nome da classe no plural, mas permite que você especifique um nome diferente. Na migration que também é criada automaticamente, a tabela correspondente é definida. Porém, neste caso, eu não queria criar uma tabela, pois ela já existe. Além disso, é um servidor de banco de dados diferente, com usuário e senha diferentes.

Após alguma pesquisa, descobri como resolver este problema. Suponha que você queira acessar uma base de usuários para compartilhar login e senha:

1 - especificar a nova conexão no arquivo config/database.yml. No exemplo abaixo, defini o nome “autenticacao_development”, supondo que tenhamos outras conexões para teste e produção:


autenticacao_development:
  adapter: mysql
  host: autenticacao_dev
  username: login
  password: senha
  database: autenticacao

2 - criar uma classe abstrata (model), derivada de ActionRecord::Base, e usar o método establish_connection para referenciar a conexão. O parâmetro “autenticacao_#{RAILS_ENV}” usa a variável RAILS_ENV para especificar a conexão relativa ao ambiente (autenticacao_development, autenticacao_test ou autenticacao_production):


class AutenticacaoDatabase < ActiveRecord::Base
  self.abstract_class = true
  establish_connection "autenticacao_#{RAILS_ENV}"
end

3 - para cada tabela deste banco de dados que será acessada, criar uma nova classe (model) derivada da classe AutenticacaoDatabase recém-criada. Caso o nome da tabela seja diferente do padrão (o nome da classe no plural), use o método set_table_name para especificar o nome correto. E se a primary key não for “id”, use “set_primary_key” para definir o nome correto deste campo.


class Usuario < AutenticacaoDatabase
  set_table_name 'usuario_tb'
  set_primary_key 'usuario_id'
end

Feitas as configurações acima, o banco de dados externo ficará acessível como o da aplicação, com todos os métodos usados normalmente. Por exemplo:


Usuario.find(:all)
Usuario.find_by_usuario_id(1)
Usuario.create

Detalhe importante: com esta configuração, é necessário fazer uma série de alterações nos unit tests para que estes funcionem. Em breve farei outro post detalhando estes passos.

Seguem abaixo os links que eu usei como base:

Verificação de tipos em Ruby

Uma das principais características do Ruby é ser uma linguagem dinâmica, o que significa que a tipagem é dinâmica, ou seja, a linguagem permite que uma variável seja usada sem ser previamente declarada, assuma um valor de uma classe qualquer e depois possa ter seu valor alterado para um objeto de outra classe.

Apesar da simplicidade que esta característica traz, freqüentemente precisamos verificar o tipo de classe de uma variável, para termos certeza de que em uma chamada de função não é passada uma variável contendo um objeto de uma classe diferente da esperada, por exemplo. Isto não é necessário em linguagens como C e Java, onde cada variável precisa ser declarada antes do uso, e na declaração é necessário especificar o tipo de variável.

Este artigo apresenta um módulo Ruby chamado Types, que simplifica bastante o trabalho de verificação do tipo de classe passado em cada parâmetro de uma função. É possível, inclusive, verificar se a classe passada implementa um método específico. Qualquer tipo de verificação é feito acrescentando-se uma linha antes da definição da função, eliminando a necessidade de verificarmos o tipo de cada parâmetro no código da função. Não cheguei a testar o módulo ainda, porém, de acordo com o artigo, parece ser uma solução muito simples e flexível para problemas de tipagem.

Usando DRY no database.yml

Um dos princípios do desenvolvimento em Rails é o DRY (don’t repeat yourself). A idéia é que você nunca repita o código que já escreveu uma vez, procurando reaproveitar sempre que possível.

No caso das views, por exemplo, isso é bem simples de implementar, através do uso de partials. Você deve criar um arquivo de view começando com “_” (ex: _item.rhtml) e usar o comando render em outra view para carregar o partial dentro do layout (ex: render :partial => ‘item’).

Outro dia descobri uma maneira muito interessante de usar essa técnica no arquivo database.yml. Esse arquivo mantém as configurações de banco de dados para cada um dos ambientes - development, test e production. Porém, geralmente alguns destes parâmetros de configuração são iguais. O adapter, por exemplo, muito provavelmente é o mesmo; o username, senha e host também podem se repetir.

Segue abaixo um exemplo de como definir estas configurações sem repetições:


login: &login
  adapter: mysql
  username: username
  password: password
  host: mysql.example.com

development:
  <<: *login
  database: app_dev

test:
  <<: *login
  database: app_test

production:
  <<: *login
  database: app_prod

Só encontrei um problema: se você usar o Aptana RadRails para desenvolver, usando essa técnica, o modo “Data perspective”, que permite analisar a estrutura do banco de dados e executar querys, retorna uma mensagem de erro (”Invalid YML syntax”). Fora isso, está tudo funcionando bem. No NetBeans não há este problema.

Próxima Página »