Como posso fazer um relacionamento muitos-para-muitos com o mesmo modelo em trilhos?
Por exemplo, cada postagem está conectada a várias postagens.
fonte
Como posso fazer um relacionamento muitos-para-muitos com o mesmo modelo em trilhos?
Por exemplo, cada postagem está conectada a várias postagens.
Existem vários tipos de relacionamentos muitos para muitos; você tem que se perguntar as seguintes perguntas:
Isso deixa quatro possibilidades diferentes. Vou examinar isso abaixo.
Para referência: a documentação do Rails sobre o assunto . Há uma seção chamada “Muitos para muitos” e, claro, a documentação dos próprios métodos de classe.
Este é o código mais compacto.
Vou começar com este esquema básico para suas postagens:
create_table "posts", :force => true do |t|
t.string "name", :null => false
end
Para qualquer relacionamento muitos para muitos, você precisa de uma tabela de junção. Aqui está o esquema para isso:
create_table "post_connections", :force => true, :id => false do |t|
t.integer "post_a_id", :null => false
t.integer "post_b_id", :null => false
end
Por padrão, o Rails chamará esta tabela de uma combinação dos nomes das duas tabelas que estamos unindo. Mas isso seria como posts_posts
nesta situação, então decidi tomar em post_connections
vez disso.
Muito importante aqui é :id => false
omitir a id
coluna padrão . Rails quer essa coluna em todos os lugares, exceto em tabelas de junção para has_and_belongs_to_many
. Ele vai reclamar em voz alta.
Finalmente, observe que os nomes das colunas também não são padronizados (não post_id
), para evitar conflitos.
Agora, em seu modelo, você simplesmente precisa dizer ao Rails sobre algumas coisas fora do padrão. Será o seguinte:
class Post < ActiveRecord::Base
has_and_belongs_to_many(:posts,
:join_table => "post_connections",
:foreign_key => "post_a_id",
:association_foreign_key => "post_b_id")
end
E isso deve simplesmente funcionar! Aqui está um exemplo de sessão irb executada script/console
:
>> a = Post.create :name => 'First post!'
=> #<Post id: 1, name: "First post!">
>> b = Post.create :name => 'Second post?'
=> #<Post id: 2, name: "Second post?">
>> c = Post.create :name => 'Definitely the third post.'
=> #<Post id: 3, name: "Definitely the third post.">
>> a.posts = [b, c]
=> [#<Post id: 2, name: "Second post?">, #<Post id: 3, name: "Definitely the third post.">]
>> b.posts
=> []
>> b.posts = [a]
=> [#<Post id: 1, name: "First post!">]
Você descobrirá que atribuir à posts
associação criará registros na post_connections
tabela conforme apropriado.
Algumas coisas a serem observadas:
a.posts = [b, c]
, a saída de b.posts
não inclui o primeiro post.PostConnection
. Você normalmente não usa modelos para uma has_and_belongs_to_many
associação. Por esse motivo, você não poderá acessar nenhum campo adicional.Certo, agora ... Você tem um usuário regular que postou hoje no seu site como as enguias são deliciosas. Esse estranho chega ao seu site, se inscreve e escreve uma postagem de repreensão sobre a inépcia do usuário regular. Afinal, as enguias são uma espécie em extinção!
Portanto, você gostaria de deixar claro em seu banco de dados que a postagem B é um discurso de censura na postagem A. Para fazer isso, você deseja adicionar um category
campo à associação.
O que precisamos não é mais um é has_and_belongs_to_many
, mas uma combinação de has_many
, belongs_to
, has_many ..., :through => ...
e um modelo extra para a tabela de junção. Esse modelo extra é o que nos dá o poder de adicionar informações adicionais à própria associação.
Aqui está outro esquema, muito semelhante ao anterior:
create_table "posts", :force => true do |t|
t.string "name", :null => false
end
create_table "post_connections", :force => true do |t|
t.integer "post_a_id", :null => false
t.integer "post_b_id", :null => false
t.string "category"
end
Observe como, nesta situação, post_connections
não tem uma id
coluna. (Não há :id => false
parâmetro.) Isso é necessário, porque haverá um modelo ActiveRecord regular para acessar a tabela.
Vou começar com o PostConnection
modelo, porque é muito simples:
class PostConnection < ActiveRecord::Base
belongs_to :post_a, :class_name => :Post
belongs_to :post_b, :class_name => :Post
end
A única coisa acontecendo aqui é :class_name
, o que é necessário, porque o Rails não pode inferir post_a
ou post_b
que estamos lidando com um Post aqui. Temos que dizer isso explicitamente.
Agora o Post
modelo:
class Post < ActiveRecord::Base
has_many :post_connections, :foreign_key => :post_a_id
has_many :posts, :through => :post_connections, :source => :post_b
end
Com a primeira has_many
associação, dizemos o modelo para se juntar post_connections
em posts.id = post_connections.post_a_id
.
Com a segunda associação, estamos dizendo ao Rails que podemos alcançar os outros postos, os que estão conectados a este, por meio de nossa primeira associação post_connections
, seguida da post_b
associação de PostConnection
.
Só falta mais uma coisa , e é que precisamos dizer ao Rails que a PostConnection
depende dos posts aos quais pertence. Se um ou ambos post_a_id
e post_b_id
fossem NULL
, essa conexão não nos diria muito, não é? Veja como fazemos isso em nosso Post
modelo:
class Post < ActiveRecord::Base
has_many(:post_connections, :foreign_key => :post_a_id, :dependent => :destroy)
has_many(:reverse_post_connections, :class_name => :PostConnection,
:foreign_key => :post_b_id, :dependent => :destroy)
has_many :posts, :through => :post_connections, :source => :post_b
end
Além da ligeira mudança na sintaxe, duas coisas reais são diferentes aqui:
has_many :post_connections
tem um :dependent
parâmetro extra . Com o valor :destroy
, dizemos ao Rails que, uma vez que esta postagem desapareça, ele pode seguir em frente e destruir esses objetos. Um valor alternativo que você pode usar aqui é :delete_all
, que é mais rápido, mas não chamará nenhum hook de destruição se você os estiver usando.has_many
associação para as conexões reversas também, aquelas que nos ligaram post_b_id
. Dessa forma, o Rails pode destruí-los perfeitamente. Observe que temos que especificar :class_name
aqui, porque o nome da classe do modelo não pode mais ser inferido :reverse_post_connections
.Com isso no lugar, trago a você outra sessão de irb por meio de script/console
:
>> a = Post.create :name => 'Eels are delicious!'
=> #<Post id: 16, name: "Eels are delicious!">
>> b = Post.create :name => 'You insensitive cloth!'
=> #<Post id: 17, name: "You insensitive cloth!">
>> b.posts = [a]
=> [#<Post id: 16, name: "Eels are delicious!">]
>> b.post_connections
=> [#<PostConnection id: 3, post_a_id: 17, post_b_id: 16, category: nil>]
>> connection = b.post_connections[0]
=> #<PostConnection id: 3, post_a_id: 17, post_b_id: 16, category: nil>
>> connection.category = "scolding"
=> "scolding"
>> connection.save!
=> true
Em vez de criar a associação e definir a categoria separadamente, você também pode simplesmente criar um PostConnection e pronto:
>> b.posts = []
=> []
>> PostConnection.create(
?> :post_a => b, :post_b => a,
?> :category => "scolding"
>> )
=> #<PostConnection id: 5, post_a_id: 17, post_b_id: 16, category: "scolding">
>> b.posts(true) # 'true' means force a reload
=> [#<Post id: 16, name: "Eels are delicious!">]
E também podemos manipular as associações post_connections
e reverse_post_connections
; isso refletirá perfeitamente na posts
associação:
>> a.reverse_post_connections
=> #<PostConnection id: 5, post_a_id: 17, post_b_id: 16, category: "scolding">
>> a.reverse_post_connections = []
=> []
>> b.posts(true) # 'true' means force a reload
=> []
Em has_and_belongs_to_many
associações normais , a associação é definida em ambos os modelos envolvidos. E a associação é bidirecional.
Mas há apenas um modelo de Post neste caso. E a associação é especificada apenas uma vez. É exatamente por isso que, neste caso específico, as associações são unidirecionais.
O mesmo é verdadeiro para o método alternativo com has_many
e um modelo para a tabela de junção.
Isso é melhor visto simplesmente acessando as associações do irb, e olhando o SQL que o Rails gera no arquivo de log. Você encontrará algo como o seguinte:
SELECT * FROM "posts"
INNER JOIN "post_connections" ON "posts".id = "post_connections".post_b_id
WHERE ("post_connections".post_a_id = 1 )
Para tornar a associação bidirecional, teríamos que encontrar uma maneira de tornar Rails OR
as condições acima com post_a_id
e post_b_id
invertidas, para que olhe nas duas direções.
Infelizmente, a única maneira de fazer isso que eu conheço é bastante hacky. Você terá que especificar manualmente o SQL usando opções para has_and_belongs_to_many
tais como :finder_sql
, :delete_sql
, etc. Não é bonito. (Também estou aberto a sugestões aqui. Alguém?)
:foreign_key
nohas_many :through
não é necessário e adicionei uma explicação sobre como usar o:dependent
parâmetro muito útil parahas_many
.Para responder à pergunta feita por Shteef:
Associações em loop bidirecionais
A relação seguidor-seguidor entre os usuários é um bom exemplo de uma associação em loop bidirecional. Um usuário pode ter muitos:
Esta é a aparência do código para user.rb :
Veja como o código para follow.rb :
As coisas mais importantes a serem observadas são provavelmente os termos
:follower_follows
e:followee_follows
em user.rb. Para usar uma associação run of the mill (sem loop) como exemplo, uma equipe pode ter muitos:players
through:contracts
. Isto não é diferente para um jogador , que pode ter muitos:teams
através:contracts
bem (ao longo de tais Jogador carreira 's). Mas, neste caso, onde existe apenas um modelo nomeado (ou seja, um usuário ), nomear o relacionamento through: de forma idêntica (por exemplothrough: :follow
, ou, como foi feito acima no exemplo de postagens,through: :post_connections
) resultaria em uma colisão de nomenclatura para diferentes casos de uso de ( ou pontos de acesso para) a tabela de junção.:follower_follows
e:followee_follows
foram criados para evitar tal colisão de nomes. Agora, um usuário pode ter muitos:followers
através de:follower_follows
e muitos:followees
através:followee_follows
.Para determinar os seguidores de um Usuário : (mediante uma
@user.followees
chamada para o banco de dados), Rails pode agora olhar para cada instância de class_name: “Seguir” onde tal Usuário é o seguidor (ou sejaforeign_key: :follower_id
) através de: tal Usuário : followee_follows. Para determinar os seguidores de um Usuário : (mediante uma@user.followers
chamada para o banco de dados), Rails pode agora olhar para cada instância de class_name: “Seguir” onde tal Usuário é o seguidor (isto éforeign_key: :followee_id
) por meio de: tal Usuário : follower_follows.fonte
Se alguém viesse aqui para tentar descobrir como criar relacionamentos de amizade no Rails, então eu recomendaria o que eu finalmente decidi usar, que é copiar o que o 'Community Engine' fez.
Você pode se referir a:
https://github.com/bborn/communityengine/blob/master/app/models/friendship.rb
e
https://github.com/bborn/communityengine/blob/master/app/models/user.rb
Para maiores informações.
TL; DR
..
fonte
Inspirado por @ Stéphan Kochen, isso poderia funcionar para associações bidirecionais
então
post.posts
&&post.reversed_posts
ambos devem funcionar, pelo menos funcionou para mim.fonte
Para bidirecional
belongs_to_and_has_many
, consulte a ótima resposta já postada e, em seguida, crie outra associação com um nome diferente, as chaves estrangeiras invertidas e certifique-se de terclass_name
definido para apontar de volta para o modelo correto. Felicidades.fonte
Se alguém teve problemas para fazer a excelente resposta funcionar, como:
ou
Então a solução é substituir
:PostConnection
por"PostConnection"
, substituindo o nome da classe, é claro.fonte