web-dev-qa-db-fra.com

Possible d'avoir une relation "has_one polymorphe" dans les rails?

Je voudrais faire quelque chose comme ça:

Category
--------
- id
- name

Tag
--------
- id
- tag


Campaign
--------
- id
- name
- target (either a tag *or* a category)

Une association polymorphe est-elle la réponse ici? Je n'arrive pas à comprendre comment l'utiliser avec has_one: target,: as =>: targetable.

Fondamentalement, je veux que Campaign.target soit défini sur un tag ou une catégorie (ou potentiellement un autre modèle à l'avenir).

39
markquezada

Je ne pense pas que vous ayez besoin d'un has_one l'association ici, la belongs_to devrait être ce que vous cherchez.

Dans ce cas, vous souhaitez un target_id et target_type colonne sur votre table de campagne, vous pouvez les créer dans un râteau avec un t.references :target appel (où t est la variable table).

class Campaign < ActiveRecord::Base
  belongs_to :target, :polymorphic => true
end

La campagne peut désormais être associée à un Tag ou Category et @campaign.target renverrait celui qui convient.

Le has_one l'association serait utilisée si vous avez une clé étrangère sur la table cible pointant vers votre Campaign.

Par exemple, vos tables auraient

Tag: id, tag, campaign_idCategory: id, category, campaign_id

et aurait un belongs_to :campaign association sur les deux. Dans ce cas, vous devez utiliser has_one :tag et has_one :category, mais vous ne pouvez pas utiliser de générique target à ce stade.

Est-ce que ça fait plus de sens?

MODIFIER

Puisque target_id et target_type sont en fait des clés étrangères à une autre table, votre Campaign appartient à l'une d'entre elles. Je peux voir votre confusion avec le libellé car logiquement le Campaign est le conteneur. Je suppose que vous pouvez le considérer comme Campaign a une seule cible, et c'est un Tag ou un Container, donc il appartient à un Tag ou Container.

Le has_one est la façon de dire que la relation est définie sur la classe cible. Par exemple, un Tag aurait été associé à la campagne via un has_one relation car il n'y a rien sur la classe de balise qui identifie l'association. Dans ce cas, vous auriez

class Tag < ActiveRecord::Base
  has_one :campaign, :as => :target
end

et de même pour un Category. Ici le :as le mot clé indique Rails quelle association se rapporte à cette Tag. Rails ne sait pas comment comprendre cela à l'avance car il y a aucune association avec le nom tag sur le Campaign.

Les deux autres options qui peuvent créer une confusion supplémentaire sont les source et source_type options. Ils ne sont utilisés que dans :through relations, où vous rejoignez réellement l'association through une autre table. Les documents le décrivent probablement mieux, mais le source définit le nom de l'association et source_type est utilisé lorsque cette association est polymorphe. Ils ne doivent être utilisés que lorsque l'association cible (sur le :through class) a un nom qui n'est pas évident - comme dans le cas ci-dessus avec target andTag - et nous devons dire Rails lequel utiliser).

79
Kristian PD

Les réponses à ces questions sont excellentes, mais je voulais juste mentionner une autre façon d'y parvenir. Ce que vous pourriez faire à la place, c'est créer deux relations, par exemple:

class Campaign < ActiveRecord::Base
  belongs_to :tag
  belongs_to :category
  validate :tag_and_category_mutually_exclusive

  def target=(tag_or_category)
    case
    when tag_or_category.kind_of?(Tag)
      self.tag = tag_or_category
      self.category = nil
    when tag_or_category.kind_of?(Category)
      self.category = tag_or_category
      self.tag = nil
    else
      raise ArgumentError, "Expected Tag or Category"
    end
  end

  def target(tag_or_category)
    tag || category
  end

  private 
  def tag_and_category_mutually_exclusive
    if tag && category
      errors.add "Can't have both a tag and a category"
    end
  end
end

La validation garantit que vous ne vous retrouvez pas accidentellement avec les deux champs définis, et les assistants target permettent un accès polymorphe à la balise/catégorie.

L'avantage de le faire comme ceci est que vous obtenez un schéma de base de données un peu plus correct, où vous pouvez définir des contraintes de clé étrangère appropriées sur les colonnes id. Cela conduira également à des requêtes SQL plus agréables et plus efficaces au niveau de la base de données.

7
troelskn

Petit ajout: dans la migration où vous avez créé la table Campaign, la t.references :target l'appel devrait avoir :polymorphic => true (au moins avec Rails 4.2)

1
user5390702