Introdução a Relacionamentos no Ruby On Rails

Neste artigo veremos como classes podem se relacionar entre si em projetos com Rails, também vamos ver que essas associações entre classes são refletidas no banco de dados da aplicação.

É comum sistemas que utilizam orientação a objetos conterem classes que representam coisas reais do domínio de negócio. Um sistema para uma escola talvez precise de classes como Aluno, Professor, Curso, Turma, etc. Essas classes de entidades geralmente possuem relacionamentos. Por exemplo uma Aluno deve estar matriculado em pelo menos um curso, ou um Professor lecione em várias Turmas e assim por diante. Esses relacionamentos entre as classes precisam ser expressos em código, isso significa que as classes devem ser informadas de como elas devem interagir umas com as outras. O framework Rails oferece recursos para que isso seja feito de modo bastante produtivo. O tópico seguinte apresenta uma introdução aos principais tipos de relacionamentos que podem haver entre as classes do modelo de uma aplicação.

Tipos de relacionamentos

Existem diversas formas de dois objetos se associarem entre si, os relacionamentos mais comuns entre classes são:

Para aplicar esses relacionamentos em uma aplicação Rails temos disponível as seguintes estruturas: belongs_to (pertence a), has_one (tem um), has_many (tem muitos), has_and_belongs_to_many (tem e pertence a muitos), entre outras. A seguir analisaremos cada um destes métodos explorando em que casos eles podem ser usados bem como os recursos que trazem para a aplicação e os efeitos ou modificações que estes causam no banco de dados.

Belongs To

Esse tipo de associação é comumente chamada de One to One (um para um) e é usada para indicar que determinado modelo “pertence a” outro. Na Listagem 1 temos o relacionamento de um modelo chamado Artigo com outo de nome Autor, esse bloco de código indica que cada artigo pertencerá a um autor.

Listagem 1. Exemplo de uso da associação belongs_to.

class Artigo < ActiveRecord::Base belongs_to :autor end class Autor < ActiveRecord::Base end

Para que o exemplo funcione corretamente é preciso que a tabela que armazena os artigos no banco de dados tenham uma referência ao autor de cada artigo. Então o arquivo de migração que cria a tabela relacionada aos artigos deve conter, além dos atributos do artigo, uma coluna para guardar o identificador do autor. A Listagem 2 mostra o arquivo de criação das tabelas para os artigos e autores.

Listagem 2. Exemplo de arquivos de migração para as classes Artigo e Autor.

class CreateArtigos < ActiveRecord::Migration def change create_table :artigos do |t| t.string :titulo t.text :texto t.date :data_publicacao t.integer :autor_id t.timestamps null: false end end end class CreateAutors < ActiveRecord::Migration def change create_table :autors do |t| t.string :nome t.text :curriculo t.timestamps null: false end end end

Note que há uma coluna do tipo integer para armazenar o identificador do autor de um artigo. Essa linha ainda poderia ser escrita de outra forma, usando a opção references, como mostra a Listagem 3.

Os dois códigos têm efeitos semelhantes: ambos criam na tabela artigos uma coluna com o nome autor_id para ser chave estrangeira da tabela autores.

Listagem 3. Exemplo de arquivo de migração utilizando a opção references.

class CreateArtigos < ActiveRecord::Migration def change create_table :artigos do |t| t.string :titulo t.text :texto t.date :data_publicacao t.references :autor t.timestamps null: false end end end

A associação belongs_to adiciona alguns métodos para facilitar a manipulação dos objetos do relacionamento. Veja a descrição do funcionamento e exemplos de uso de dois desses métodos: o autor(force_reload = false) e autor = (autor).

Método de configuração artigo.autor = (autor)

Esse método configura ou “seta” um determinado objeto para a associação. Ele faz mais do que apenas armazenar um objeto em uma variável, além disso esse método associa a chave primária do objeto que está sendo passado por parâmetro a chave estrangeira do objeto que tem o relacionamento belongs_to. No caso da Listagem 4 perceba que autor1 foi configurado para fazer parte da associação com artigo1, isso significa que o identificador de autor1 será armazenado na coluna que é chave estrangeira na tabela de artigos.

Listagem 4. Execução do método artigo.autor = (autor)

~/RailsProjects/tests_associations$ rails console irb(main):001:0> autor1 = Autor.new(nome: 'José Camilo Filho') irb(main):002:0> artigo1 = Artigo.new(nome: 'Criação de uma aplicação com Rails') irb(main):003:0> artigo1.autor=(autor1) irb(main):005:0> puts artigo1.autor.nome => José Camilo Filho

Método de recuperação artigo.autor(force_reload = false)

Esse método recupera o objeto associado. No caso do exemplo de artigos e autores ele retornaria o autor que está associado ao artigo em questão. A Listagem 5 mostra o exemplo de seu uso a partir do console do framework, que simula a aplicação sem a interface gráfica sendo executada no terminal. Para iniciar essa ferramenta basta digitar o comando rails console no terminal estando dentro do diretório da aplicação.

Listagem 5. Exemplo de execução do método autor(force_reload = false) a partir do rails console.

~/RailsProjects/tests_associations$ rails console irb(main):001:0>autor1=Autor.new(nome: 'João da Silva Souza') irb(main):002:0> autor1.save irb(main):003:0>artigo1=Artigo.new(titulo: 'Introdução ao Rails', data_publicacao: '01/12/2015') irb(main):004:0> artigo1.save irb(main):005:0> puts artigo1.autor.nome =>João da Silva Souza

Note que por padrão o parâmetro force_reload desse método é configurado com o valor false isso indica que não haverá uma consulta ao banco de dados para recuperar o objeto da associação, o objeto será recuperado do cache que o ActiveRecord faz. Caso esse parâmetro seja alterado para true uma nova consulta será feita ao banco como mostra a Listagem 6.

Listagem 6. Exemplo de execução do método autor(force_reload = true) a partir do rails console, fazendo uma nova consulta no banco de dados.

irb(main):006:0> puts artigo1.autor(true).nome Autor Load (0.3ms) SELECT "autores".* FROM "autores" WHERE "autores"."id" = ? LIMIT 1 [["id", 1]] =>João da Silva Souza

Has One

Essa associação é bem semelhante a belongs_to chegando a causar algumas confusões de entendimento entre as duas. O método has_one também é, assim como belongs_to, uma associação do tipo one to one, porém ela deve ser usada em situações diferentes e causa um efeito diferenciado.

Has One é também conhecido como a associação bidirecional a belongs_to. Ela não adiciona chave estrangeira ao modelo que a declara, essa é a diferença mais significativa dela para belongs_to. A Listagem 7 mostra um exemplo de aplicação desse relacionamento.

Listagem 7. Exemplo de uso de has_one

class Usuario < ActiveRecord::Base has_one :conta end class Conta < ActiveRecord::Base belongs_to :usuario end

Note que no exemplo apresentado na Listagem 7 o has_one foi usado juntamente com belongs_to como um complemento. Isso torna possível que a classe Usuário tenha métodos de acesso ao objeto Conta ao qual está relacionado. Mas como já dito a chave estrangeira ficará na classe que contém a associação belongs_to, como mostra os arquivos de migrações apresentados na Listagem 8. Perceba que apenas a migração referente a classe Conta possui uma coluna do tipo references que se tornará uma chave estrangeira.

Listagem 8. Arquivos de migrações para as classes Usuário e Conta.

class CreateContas < ActiveRecord::Migration def change create_table :contas do |t| t.string :login t.string :senha t.references :usuario t.timestamps null: false end end end class CreateUsuarios < ActiveRecord::Migration def change create_table :usuarios do |t| t.string :nome t.timestamps null: false end end end

A associação has_one também adiciona métodos para manipular o objeto relacionado. Os métodos tem funcionamento semelhante aos recebidos pela associação belongs_to. Veja na Listagem 9 o exemplo de uso deles.

Listagem 9. Exemplo de uso dos métodos adicionados por has_one.

~/RailsProjects/tests_associations$ rails console irb(main):001:0> usuario1 = Usuario.new(nome: 'João da Silva') irb(main):002:0> conta1 = Conta.new(login: 'joao', senha: '12345') irb(main):003:0> usuario1.conta=conta1 irb(main):004:0> puts "Login: #{usuario1.conta.login} - Senha: #{usuario1.conta.senha}" =>Login: joao - Senha: 12345

A linha 3 do exemplo acima mostra o uso do método de atribuição “usuario.conta=”, já a linha 4 usa o método que recupera o objeto da associação (usuario.conta) para imprimir no terminal o login e senha do usuário.

Has Many

Essa associação é usada para indicar que um modelo tem nenhum ou muitos elementos de outro modelo da aplicação, chamada frequentemente de one to many (um para muitos). Assim como a associação has_one, has_many não adiciona em que a usa nenhuma coluna, na verdade ela é usada em conjunto com belong_to que indica por meio de uma chave estrangeira com quem o objeto filho se relaciona. Um exemplo da aplicação dessa estrutura é apresentado na Listagem 10. Perceba que ao passo que uma equipe tem muitos (has_many) integrantes, um integrante pertence a (belongs_to) uma equipe.

Listagem 10. Uso do relacionamento has_many.

class Equipe < ActiveRecord::Base has_many :integrantes end class Integrante < ActiveRecord::Base belongs_to :equipe end

As migrações para os modelos apresentados na Listagem 10 poderiam ser semelhantes ao código mostrado na Listagem 11.

Listagem 11. Exemplo de migrações para as classes Equipe e Integrante.

class CreateEquipes < ActiveRecord::Migration def change create_table :equipes do |t| t.string :nome t.timestamps null: false end end end class CreateIntegrantes < ActiveRecord::Migration def change create_table :integrantes do |t| t.string :nome t.references :equipe t.timestamps null: false end end end

Essa associação também adiciona um conjunto bastante diverso de métodos para facilitar o uso dos objetos que fazem parte do relacionamento. No caso do relacionamento das classes Equipe e Integrante alguns métodos que a classe Equipe receberia são: equipe.integrantes=, equipe.integrantes<<, equipe.integrantes(force_reload = false), equipe.integrantes.destroy, equipe.integrantes_ids entre outros. Veja abaixo alguns detalhes sobre esses métodos.

Métodos de configuração equipe.integrantes= e equipe.integrantes <<

Esses métodos tem como objetivo adicionar valores na variável da associação. O método “=” aceita uma lista de objetos já “<<” aceita tanto um único valor, como uma lista contendo vários objetos. Seguindo ainda com o exemplo de equipe e integrantes veja a Listagem 12 que usa esses métodos para inicializar a lista de integrantes com valores.

Listagem 12. Adicionando valores a lista de integrantes.

~/RailsProjects/tests_associations$ rails console irb(main):001:0> integrante1 = Integrante.new(nome: 'josé') irb(main):002:0> integrante2 = Integrante.new(nome: 'carlos') irb(main):003:0> integrante3 = Integrante.new(nome: 'silva') irb(main):004:0> integrante4 = Integrante.new(nome: 'souza') irb(main):005:0> equipe1 = Equipe.new(nome: 'equipe de testes') irb(main):006:0> equipe1.integrantes.empty? => true irb(main):007:0> equipe1.integrantes << integrante1 irb(main):008:0> equipe1.integrantes = [integrante2, integrante3, integrante4] irb(main):009:0> equipe1.integrantes.size => 3

Método de recuperação equipe.integrantes(force_reload = false)

Esse método retorna uma lista contendo todos os objetos associados. No caso do exemplo citado acima do relacionamento entre equipe e integrantes, esse método retornaria uma lista de objetos do tipo Integrante que estão associados ao objeto Equipe no qual tal método foi chamado. Veja na Listagem 13 um exemplo em que vários integrantes são adicionados a uma determinada equipe e depois disso o método equipe.integrantes é usado para recuperar a lista de integrantes associados.

Listagem 13. Exemplo de uso do método equipe.integrantes.

~/RailsProjects/tests_associations$ rails console irb(main):001:0> integrante1 = Integrante.new(nome: 'josé') irb(main):002:0> integrante2 = Integrante.new(nome: 'carlos') irb(main):003:0> integrante3 = Integrante.new(nome: 'silva') irb(main):004:0> equipe1 = Equipe.new(nome: 'equipe de testes') irb(main):005:0> equipe1.integrantes.empty? => true irb(main):006:0>equipe1.integrantes<<[integrante1, integrante2, integrante3] irb(main):007:0>equipe1.integrantes.size =>3 irb(main):008:0>equipe1.integrantes.each do |i| irb(main):009:1* puts i.nome irb(main):010:1>end =>josé =>carlos =>silva

Perceba que esse método recebe por padrão o valor false para o atributo force_reload. Caso seja necessário que a busca venha direto do banco de dados ao invés dos dados já carregados o método deve ser chamado passando o valor true como argumento, conforme Listagem 14.

Listagem 14. Listando os integrantes diretamente do banco de dados.

irb(main):001:0>equipe1.integrantes(true).each do |i| irb(main):002:1* puts i.nome irb(main):003:1>end Integrante Load (0.5ms) SELECT "integrantes".* FROM "integrantes" WHERE "integrantes"."equipe_id" = ? [["equipe_id", 1]] =>josé =>carlos =>silva

Método de remoção equipe1.integrantes.destroy

Esse método remove do banco de dados o objeto passado como parâmetro. Caso o método receba uma lista, todos os elementos contidos nela serão excluídos. A Listagem 15 mostra o seu funcionamento.

Listagem 15. Removendo objetos da associação com destroy.

~/RailsProjects/tests_associations$ rails console irb(main):001:0> integrante1 = Integrante.new(nome: 'josé') irb(main):002:0> integrante2 = Integrante.new(nome: 'carlos') irb(main):003:0> integrante3 = Integrante.new(nome: 'silva') irb(main):004:0> equipe1 = Equipe.new(nome: 'equipe de testes') irb(main):005:0> equipe1.integrantes<<[integrante1, integrante2, integrante3] irb(main):006:0> equipe1.integrantes.empty? => 3 irb(main):007:0> equipe1.save irb(main):008:0> equipe1.integrantes.destroy(integrante1) SQL (0.5ms) DELETE FROM "integrantes" WHERE "integrantes"."id" = ? [["id", 1]] irb(main):009:0> equipe1.integrantes.destroy([integrante2, integrante3]) SQL (0.4ms) DELETE FROM "integrantes" WHERE "integrantes"."id" = ? [["id", 2]] SQL (0.1ms) DELETE FROM "integrantes" WHERE "integrantes"."id" = ? [["id", 3]]

Método de listagem de identificadores equipe.integrante_ids

O método equipe.integrante_ids retorna um array contendo todos os identificadores dos objetos na lista. A Listagem 16 mostra isso.


irb(main):001:0> integrante1 = Integrante.new(nome: 'josé') irb(main):002:0> integrante2 = Integrante.new(nome: 'carlos') irb(main):003:0> integrante3 = Integrante.new(nome: 'silva') irb(main):004:0> equipe1.integrantes =[integrante1, integrante2, integrante3] irb(main):005:0> equipe1.integrante_ids => [1, 2, 3]

Has And Belongs To Many

Essa opção cria uma conexão do tipo many to many (muitos para muitos) entre dois modelos da aplicação. Por exemplo, aplicações que armazenam professores e as turmas que eles lecionam, geralmente permitem que um professor ensine a mais de uma turma e que uma turma tenha mais de um professor, esse relacionamento pode ser traduzido pela associação muitos para muitos. A Listagem 17 mostra como ficariam duas classes de modelo com esse relacionamento. Note que a associação deve ser usada nas duas entidades que participam do relacionamento.

Listagem 17. Exemplo de associação has_and_belongs_to_many.

class Professor < ActiveRecord::Base has_and_belongs_to_many :turmas end class Turma < ActiveRecord::Base has_and_belongs_ to_many :professores end

No banco de dados a associação muitos para muitos é refletida de uma maneira diferente das demais. Não há modificações nas tabelas que fazem parte do relacionamento, na verdade uma nova tabela é criada para armazenar o identificador de cada modelo participante do relacionamento. Essa tabela é chamada de join table ou tabela de junção, o seu nome é a concatenação do nome dos dois modelos respeitando-se a ordem alfabética. A Listagem 18 mostra os arquivos de migração para criação das tabelas para professores, turmas e para a join table professores_turmas. Veja que não há atributos de referencias nas tabelas professores e turmas, mas a tabela professores_turmas aponta para as duas tabelas do relacionamento.

Listagem 18. Arquivos de migração para as entidades Professor, Turma e para a tabela de junção.

class CreateProfessores < ActiveRecord::Migration def change create_table :professores do |t| t.string :nome t.timestamps null: false end end end class CreateTurmas < ActiveRecord::Migration def change create_table :turmas do |t| t.string :nome t.timestamps null: false end end end class CreateJoinTableProfessorTurma < ActiveRecord::Migration def change create_join_table :professores, :turmas end end

Assim como os demais relacionamentos has_and_belongs_to_many também adiciona métodos as classes de modelo que o utilizam. Alguns dos métodos mais utilizados são: <<, clear entre outros. Veja abaixo exemplos de utilização destes.

Método de configuração professores<< ou turmas<<

Assim como em outros tipos de relacionamentos o método << adiciona objetos nas variáveis que representam as associações. A Listagem 19 mostra um exemplo de adição de turmas para um determinado professor.

~/RailsProjects/tests_associations$ rails console irb(main):001:0> professor1 = Professor.new(nome: 'João da Silva') irb(main):002:0> turma1 = Turma.new(nome: '1º A') irb(main):003:0> turma2 = Turma.new(nome: '1º B') irb(main):004:0> turma3 = Turma.new(nome: '1º C') irb(main):005:0> professor1.turmas.size => 0 irb(main):006:0> professor1.turmas<<[turma1, turma2, turma3] irb(main):007:0> professor1.turmas.size => 3

Método Clear

Esse método exclui todos os objetos da coleção na qual foi chamado. Ele remove todas as linhas de relacionamento da tabela de junção mas sem excluir os objetos das duas respectivas tabelas. Veja na Listagem 20 que depois da chamada ao método clear nas turmas (linha 7) elas são removidas do relacionamento, mas não são excluídas da tabela turmas (linha 8).

~/RailsProjects/tests_associations$ rails console irb(main):001:0> professor1 = Professor.new(nome: 'João da Silva') irb(main):002:0> turma1 = Turma.new(nome: '1º A') irb(main):003:0> turma2 = Turma.new(nome: '1º B') irb(main):004:0> turma3 = Turma.new(nome: '1º C') irb(main):005:0> professor1.turmas<<[turma1, turma2, turma3] irb(main):006:0> professor1.turmas.size => 3 irb(main):007:0> professor1.turmas.clear irb(main):008:0> professor1.turmas.size => 0 irb(main):009:0> Turma.all.size => 3

Referências

Rails Guides
http://guides.rubyonrails.org/association_basics.html

API Ruby on Rails
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

Artigos relacionados