Esta publicação apresenta uma nova associação avançada do Rails: has_subtypes
O Rails tem 6 tipos básicos de associações: belongs_to, has_one, has_many, has_many :through, has_one :through e has_and_belongs_to_many.
Também existem associações avançadas. As principais são as polimórficas e os tipos delegados, que permitem que um modelo pertença a mais de um outro modelo por meio de um tipo e de um ID. Essencialmente, são variações especiais da associação belongs_to. Por exemplo, um tipo delegado ainda é um belongs_to, mas com uma camada adicional de semântica e convenções.
Meu objetivo aqui é apresentar uma alternativa aos tipos delegados usando uma nova associação avançada: has_subtypes. Em vez de se concentrar em belongs_to, esse novo método especializa a associação has_one.
Essa nova abordagem para resolver o problema da herança em múltiplas tabelas também permite maior controle do banco de dados. Diferentemente dos tipos delegados, agora é possível garantir a integridade referencial entre cada tabela filha (subtipo) e a tabela pai.
Implementação
rails new app
cd app
rails generate model Entry
rails generate model Message entry:references content:text
rails generate model Comment entry:references body:text
Agora, abra o projeto e insira o método a seguir dentro da classe ApplicationRecord:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
def self.has_subtypes(*types, dependent: :destroy)
types.each { has_one(_1, dependent:) }
scope :with_subtype, -> { includes(*types) }
class_eval <<~RUBY, __FILE__, __LINE__ + 1
def subtype = #{types.join(" || ")} # mensagem || comentário
before_save do
subtypes = [#{types.join(", ")}].compact # mensagem, comentário
raise "must have exactly one subtype" unless subtypes.one?
end
RUBY
end
end
Seria ótimo se, no futuro, esse método passasse a fazer parte do próprio Rails. Por enquanto, você já pode começar a usar essa especialização da associação has_one.
Exemplos de modelos
class Entry < ApplicationRecord
has_subtypes :message, :comment
end
class Message < ApplicationRecord
belongs_to :entry
end
class Comment < ApplicationRecord
belongs_to :entry
end
Cada Entry pode ser agora um Message ou um Comment.
Passo a passo no console do Rails
# Inicie o console do Rails
rails console
# 1. Crie um Entry com um Message
entry1 = Entry.create!(message: Message.new(content: "Hello world"))
# 2. Crie um Entry com um Comment
entry2 = Entry.create!(comment: Comment.new(body: "First comment"))
# 3. Verifique o subtipo de cada registro
entry1.subtype
# => #<Message id:1, content:"Hello world">
entry2.subtype
# => #<Comment id:1, body:"First comment">
# 4. Consulte todos os registros com seus subtipos
Entry.with_subtype.each do |entry|
puts "Entry ##{entry.id} subtype: #{entry.subtype.class.name}"
end
# Saída:
# Entry #1 subtype: Message
# Entry #2 subtype: Comment
# 5. Tente criar um Entry com os dois subtipos (isso gerará um erro)
Entry.create!(message: Message.new(content: "Test"), comment: Comment.new(body: "Invalid"))
# => RuntimeError: must have exactly one subtype
Conclusão
Com essa nova associação has_subtypes, você pode:
- Trabalhar com um conceito claro de subtipos, estendendo associações
has_one. - Garantir a integridade referencial entre as tabelas pai e filha.
- Consultar e paginar registros com seus subtipos em um único comando, de forma simples, semelhante aos tipos delegados, mas com garantias mais fortes no banco de dados.
Agradecimentos
Obrigado, Matz, por criar uma linguagem tão divertida: Ruby.
Obrigado, David Heinemeier Hansson, por esclarecer a doutrina do Rails: ampliar a tenda — “Precisamos de discordâncias. Precisamos de dialetos. Precisamos de diversidade de pensamento e de pessoas.” Esta publicação só foi possível graças a essa filosofia de “ampliar a tenda”.
Por fim, este resultado não teria sido possível sem a refatoração e as sugestões de Kasper Timm Hansen. Muito obrigado, Kasper!