Relacionamento muitos-para-muitos com ActiveRecord no Rails
16/04/08
Esta semana recebi dois e-mails com a mesma dúvida: como funciona o relacionamento muitos-para-muitos no ActiveRecord. Aparentemente, este é um assunto que muitas pessoas têm dúvidas, mas cuja resposta é bastante simples, até. Você pode fazer tal relacionamento de duas maneiras diferentes, como veremos abaixo.
Utilizando o método has_and_belongs_to_many
O método has_and_belongs_to_many possui uma convenção para se nomear a tabela que persistirá os relacionamentos. Você deve colocar ambas as tabelas relacionadas no nome da tabela de relacionamentos, ordenados alfabeticamente e separados por underscore. Se você tem as tabelas "posts" e "categories", o nome de sua tabela de relacionamentos será "categories_posts". Os campos também possuem uma convenção, que é colocar o nome da tabela no singular, adicionando o sufixo "_id". Neste caso, teríamos um arquivo de migração como este:
class AddPostsAndCategories < ActiveRecord::Migration
def self.up
create_table :categories_posts do |t|
t.references :category, :post
end
end
def self.down
drop_table :categories_posts
end
end
Para relacionar ambos os modelos, precisaríamos colocar o seguinte método no modelo Post:
class Post < ActiveRecord::Base
has_and_belongs_to_many :categories
end
Já no modelo Category, o relacionamento seria o seguinte:
class Category < ActiveRecord::Base
has_and_belongs_to_many :posts
end
Muito simples! Apenas com estas poucas linhas você já pode usar diversos métodos, adicionados pelo próprio ActiveRecord.
post = Post.find(:first)
category = Category.find(:category)
# creating relationship through post
post.categories << category
# creating relationship through category
category.posts << post
# getting all posts
categories.posts.each do |post|
puts post.title
end
# getting all categories from a post
post.categories.each do |cat|
puts cat.title
end
# getting all the post ids
category.post_ids
O método has_and_belongs_to_many possui uma grande desvantagem. Você não pode adicionar campos extras aos relacionamentos. No entanto, o método has_many permite que você faça isso.
Utilizando o método has_many
Ao contrário do método has_and_belongs_to_many, você pode ter quantos campos adicionais precisar se fizer o relacionamento com o método
has_many.
Isso é extremamente útil e bastante utilizado por plugins que lidam com ActiveRecord. A convenção para este tipo de relacionamento é um pouco diferente.
Sua tabela pode ter um nome específico que identifica o tipo de relacionamento que está sendo feito, já que ele terá um modelo próprio, que fará o papel
intermediário entre as tabelas relacionadas.
Crie um modelo chamado Categorization. Ele deve ter os seguintes campos:
class CreateCategorizations < ActiveRecord::Migration
def self.up
create_table :categorizations do |t|
t.references :post, :category
t.timestamps
end
end
def self.down
drop_table :categorizations
end
end
Agora, você precisa criar relacionamentos entre este modelo e os demais (Post e Category). No seu modelo Categorization, adicione os seguintes relacionamentos:
class Categorization < ActiveRecord::Base
belongs_to :category
belongs_to :post
end
E em cada um dos outros modelos faça o relacionamento contrário, utilizando o método has_many.
class Post < ActiveRecord::Base
has_many :categorizations
end
class Category < ActiveRecord::Base
has_many :categorizations
end
E aqui entra realmente a parte importante: você irá usar um relacionamento comumente chamado de "has_many :through", que fará uma consulta SQL, unindo os resultados em uma única consulta. No seu modelo Post, adicione mais um relacionamento.
class Post < ActiveRecord::Base
has_many :categorizations
has_many :categories,
:through => :categorizations
end
Faça a mesma coisa no modelo Category.
class Category < ActiveRecord::Base
has_many :categorizations
has_many :posts,
:through => :categorizations
end
Pronto! Seu relacionamento já foi criado. Para testar, faça algo como isto usando o console.
post = Post.find(:first)
# get post categories
post.categories.collect(&:title).to_sentence
category = Category.find(:first)
# get posts by categories
category.posts.collect(&:title).to_sentence
Para criar uma nova associação, você precisa fazer isso através do relacionamento categorizations; caso contrário, você receberá um erro dizendo que não possui um campo para o id.
# through post
post.categorizations.create(:category => category)
# through category
category.categorizations.create(:post => post)
É isso! Se você tem alguma dúvida, envie um e-mail. Se for um assunto que me interessa, ou que interessa a mais pessoas, eu posso escrever algo aqui!
Executando SQL no Rails
23/08/07
Às vezes precisamos realizar consultas que não pertencem a nenhum modelo no Rails. Uma maneira de fazer isso é utilizando o método execute. Veja um exemplo:
# setting a variable
# with the current connection
conn = ActiveRecord::Base.connection
result = conn.execute "SELECT id, email FROM users LIMIT 3"
# => #<Mysql::Result>
Como resultado, você recebe um objeto Mysql::Result, que pode ser iterado da seguinte maneira:
result = conn.execute "SELECT id, email FROM users LIMIT 2"
result.each do |user|
puts "id: %s" % user[0]
puts "email: %s" % user[1]
puts
end
Normalmente, precisamos fazer consultas baseadas em valores passados pelo usuário. É extremamente importante que você tome os cuidados necessários para não sofrer um ataque através de SQL Injection. Para que isto não aconteça, você deve utilizar o método quote:
query = "SELECT id, email FROM users WHERE email = %s" % conn.quote("' OR 1=1 #")
puts query
# => SELECT id, email FROM users WHERE email = '\' OR 1=1 #'
Além do método execute, você pode utilizar outros mais específicos como é o caso do método select_all. Este método retorna um array de hashes, com o nome das colunas como índice, tornando mais simples a iteração.
result = conn.select_all "SELECT id, email FROM users LIMIT 2"
result.each do |user|
puts "id: %s" % user['id']
puts "email: %s" % user['email']
puts
end
Para os casos em que tudo o que você precisa é de apenas uma linha como resultado, você pode utilizar o método select_one:
result = conn.select_one("SELECT COUNT(*) AS total FROM users")
puts result['total']
No Rails, todos os modelos estendem a classe ActiveRecord::Base. Por isso, os métodos acima também estão disponíveis no modelo.
class User < ActiveRecord::Base
def self.all
connection.select_all "SELECT id, email FROM users"
end
end
puts User.all
# => [{"id"=>"1", "email"=>"user@example.com"}, {"id"=>"2", "email"=>"another@example.com"}]
Lembre-se que não é nenhuma vergonha utilizar SQL diretamente no Rails. O que o ActiveRecord faz é nos poupar das tarefas rotineiras. Já para as mais complexas, você provavelmente vai precisar de uma das soluções acima. Para mais informações, acesse a documentação do ActiveRecord.
- Permalink
- Trackback
- Comentários (3)
- Ao som de: Anberlin – Naive Orleans
Como migrar suas Stored Procedures no ambiente de teste
11/06/07
Normalmente, evito utilizar Stored Procedures e Functions até onde posso, mas no Spesa, não foi possível, devido uma quantidade de consultas "avançadas" que são feitas. Se não as utilizasse, a repetição de código seria absurdamente grande e, por conseguinte, seria impossível de se fazer eventuais modificações. Como estou refatorando o código do Spesa e escrevendo testes para cada item do sistema, caí em um problema um pouco incômodo na hora da migração.
Não sei se você sabe, mas toda vez que você quer sincronizar o seu banco de testes com o de desenvolvimento, basta executar o comando rake db:test:prepare. O grande problema deste comando é que ele não leva em consideração os arquivos de migração, mas sim o arquivo schema.rb, que não adiciona queries executadas através do método execute. Para corrigir isto, basta executar o comando rake db:migrate RAILS_ENV="test".
Update: depois de fazer alguns testes, vi que as stored procedures continuavam a serem excluídas. O único jeito foi estender o ActiveRecord para ele fazer o dump das funções e salvá-las no arquivo schema.rb. Basta adicionar o código abaixo no arquivo environment.rb.
module ActiveRecord
class SchemaDumper
def dump(stream)
header(stream)
tables(stream)
functions(stream)
trailer(stream)
stream
end
private
def functions(stream)
result = @connection.execute <<QUERY
SELECT
ROUTINE_NAME AS name
FROM
INFORMATION_SCHEMA.ROUTINES
WHERE
ROUTINE_TYPE='FUNCTION' AND
ROUTINE_SCHEMA='#{@connection.current_database}'
QUERY
result.each do |row|
stream = function(row[0], stream)
end
end
def function(name, stream)
result = @connection.execute "SHOW CREATE FUNCTION #{name}"
fields = result.fetch_hash
func = fields['Create Function'].gsub(/CREATE DEFINER.*? FUNCTION/, "CREATE FUNCTION")
stream.puts <<FUNCTION
execute <<QUERY
#{func}
QUERY
FUNCTION
stream
end
end
end - Permalink
- Trackback
- Comentários (3)
- Ao som de: The Get Up Kids – Conversation
