2017-10-17 3 views
0

Je construis un système qui a des enregistrements dans des tables qui sont des enregistrements de modèles, qui sont visibles par tous les comptes et peuvent être copiés ultérieurement pour créer des enregistrements en direct pour un compte individuel. Le motif de cette décision de conception est que les enregistrements de modèle et les enregistrements en cours partagent 95% et plus du même code, donc je ne voulais pas créer de table distincte pour suivre principalement les mêmes champs.Rails: plusieurs modèles fonctionnant sur la même table

Par exemple, j'ai une table workflows:

  • id: entier
  • account_id: entier
  • Nom
  • : string (obligatoire)
  • is_a_template: Boolean (default: false)
  • is_in_template_library: booléen (par défaut: false)

Dans cette table, j'ai des enregistrements qui sont des modèles. Quand je vais créer un nouvel enregistrement en direct, je peux utiliser un enregistrement de modèle:

# workflows_controller.rb (pseudo-code, not fully tested) 
def create 
    @workflow_template = Workflow.where(is_a_template: true).find_by(id: params[:workflow_template_id]) 
    @workflow = current_account.workflows.new(workflow_params.merge(@workflow_template.dup)) 

    if @workflow.save 
    ... 
    else 
    ... 
    end 
end 

Comme je construis plus de fonctionnalités, je trouve que j'ai vraiment besoin de 2 différents modèles qui fonctionnent différemment sur la table. Il y a plusieurs autres différences, mais celles qui sont énumérées ci-dessous suffisent pour montrer les différences:

class Workflow < ApplicationRecord 
    default_scope -> { where(is_a_template: false) } 

    belongs_to :account 

    validates :account, presence: true 
    validates :name, presence: true 
end 

class WorkflowTemplate < ApplicationRecord 
    default_scope -> { where(is_a_template: true) } 

    validates :name, presence: true 
end 

class WorkflowLibraryTemplate < ApplicationRecord 
    default_scope -> { where(is_a_template: true, is_in_template_library: true) } 

    validates :name, presence: true 
end 

Comme vous pouvez le voir, la table workflows dispose de 3 différents « types » d'enregistrements:

  1. « live » les flux de travail qui appartiennent à un flux de travail de modèle compte
  2. qui appartiennent également à un compte et sont copiés pour créer des flux de travail « live »
  3. flux de travail de la bibliothèque de modèles qui ne font pas partie à un compte et peut être consulté par tout compte, afin qu'ils puissent les copier dans leur propre liste de modèles

Question

Ce que je suis en train de comprendre, à quel moment est-ce que je romps cette table unique en plusieurs tables, par rapport en gardant la même table et ayant plusieurs modèles, ou quelle est la solution à un problème comme celui-ci? La partie frustrante est qu'il y a 5 autres tables qui sont des associations "enfants" de la table workflows. Donc si je décide que j'ai besoin de tables séparées pour chacune, je finirais par passer de 6 tables à quelque chose comme 18, et chaque fois que j'ajoute un champ, je dois le faire pour les 3 "versions" de la table.

Ainsi, je suis très réticent à descendre la route des tables multiples.

Si je garde une seule table et plusieurs modèles, je me retrouve avec une version différente des données dans la table, ce qui n'est pas la fin du monde. J'interagis seulement avec les données à travers mon application (ou une future API que je contrôle).

Une autre solution à laquelle je pense consiste à ajouter un champ role:string à la table, qui fonctionne très bien comme le champ type dans Rails.Je ne voulais pas utiliser STI, cependant, parce qu'il y a trop d'exigences dans Rails avec lesquelles je ne veux pas entrer en conflit.

Ce que je suis envisioning est:

class Workflow < ApplicationRecord 
    scope :templates, -> { where(role: "template") } 
    scope :library_templates, -> { where(role: "library_template") } 

    validates :account, presence: true, if: :account_required? 
    validates :name, presence: true 

    # If record.role matches one of these, account is required 
    def account_required 
    ["live", "template"].include?(role.to_s.downcase) 
    end 
end 

Cela semble répondre à plusieurs des questions, me maintient avec 1 table et 1 modèle, mais commence à avoir une logique conditionnelle dans le modèle, ce qui semble être un mauvaise idée à moi aussi.

Existe-t-il une manière plus simple d'implémenter un système de modèle dans une table?

+0

Avez-vous des difficultés particulières ou attendez-vous à rencontrer votre approche actuelle? – EJ2015

+0

Ce que j'ai en production en ce moment, c'est un tas de logique conditionnelle qui est assez moche. Si je vais avec une approche STI, je vais devoir créer plusieurs fichiers de modèles différents qui implémentent la logique pour chacun séparément. Je pense que c'est plus facile à maintenir que plusieurs tables, cependant. Le gros problème avec STI est que je devrai avoir des fichiers modèles séparés pour toutes les associations, ce qui ajouterait 10+ fichiers de modèles. –

Répondre

3

Donc ce que vous regardez ici est appelé Single Table Inheritance. Les modèles sont appelés polymorphic.

En ce qui concerne le moment de décomposer le STI en tables distinctes, la réponse est: lorsque vous avez suffisamment de divergence, vous commencez à avoir des colonnes spécialisées. Le problème avec STI est que disons que WorkFlows et WorkFlowTemplate commencent à diverger. Peut-être que le modèle commence à obtenir beaucoup d'attributs supplémentaires en tant que colonnes qui ne correspondent pas aux anciens flux de travail. Maintenant vous avez beaucoup de données qui sont vides pour une classe (ou pas nécessaire) et utiles et nécessaires pour l'autre. Dans ce cas, je casserais probablement les tables. La vraie question que vous devriez poser est la suivante:

  1. Jusqu'où vont ces modèles diverger les uns des autres en termes d'exigences?

  2. À quelle fréquence cela se produira-t-il?

S'il arrive très tard dans la vie de mon application:

aura-t-il difficile/impossible de migrer ces tableaux en raison de la façon dont le nombre de lignes/volume de données dont je dispose?

Edit:

Y at-il un moyen plus propre? Dans ce cas précis, je ne pense pas que ce soit donné un modèle et une copie de ce modèle, sont susceptibles d'être étroitement couplés les uns aux autres.

+0

Les enregistrements seront toujours 95% + similaires; Si les modèles deviennent radicalement différents, alors je les ferais entrer dans leur propre ressource, mais je ne peux jamais voir cela se produire. Pour le moment, ils ne diffèrent que par "account_id, is_a_template, is_in_template_library", et si j'utilise un champ "role/type", cela réduirait les champs du template. Je suis d'accord avec 2 ou 3 domaines différents, et je ne vois pas que cela va au-delà de 5, mais je ne peux pas toujours dire l'avenir :) –

+0

Exactement. J'ajouterais aussi: laissez-vous de la place. Metz dit que si vous pouvez éviter de prendre une décision, faites-le, repoussez la décision si vous le pouvez. – apanzerj

0

L'approche que j'ai prise est la décomposition par responsabilité.

décomposition par la responsabilité:

En ce moment, vous avez 3 différentes sources de données et de 2 façons différentes pour créer/valider un flux de travail. Pour ce faire, vous pouvez introduire le concept Repositories et FormObject s. Les référentiels sont des objets wrapper qui résumeront la façon dont vous interrogez votre modèle. Il ne se soucie pas si c'est la même table ou multiple. Il sait juste comment obtenir les données.

Par exemple:

class Workflow < ApplicationRecord 
    belongs_to :account 
end 

class WorkflowRepository 
    def self.all 
    Workflow.where(is_a_template: false) 
    end 
end 

class WorkflowTemplateRepository 
    def self.all 
    Workflow.where(is_a_template: true) 
    end 
end 

class WorkflowLibraryTemplateRepository 
    def self.all 
    Workflow.where(is_a_template: true, is_in_template_library: true) 
    end 
end 

Cela fait en sorte que peu importe ce que vous décidez à l'avenir à faire, vous ne changerez pas d'autres parties du code.

Alors maintenant, nous allons discuter FormObject

FormObject sera abstraite la façon dont vous valider et de construire vos objets. Ce n'est peut-être pas un excellent ajout pour le moment, mais habituellement, cela rapporte à long terme.

Par exemple

class WorkFlowForm 
    include ActiveModel::Model 

    attr_accessor(
    :name, 
    :another_attribute, 
    :extra_attribute, 
    :account 
) 

    validates :account, presence: true 
    validates :name, presence: true 

    def create 
    if valid? 
     account.workflows.create(
     name: name, is_a_template: false, 
     is_in_template_library: false, extra_attribute: extra_attribute) 
    end 
    end 
end 

class WorkflowTemplateForm 
    include ActiveModel::Model 

    attr_accessor(
    :name, 
    :another_attribute, 
    :extra_attribute 
) 

    validates :name, presence: true 

    def create 
    if valid? 
     Workflow.create(
     name: name, is_a_template: true, 
     is_in_template_library: false, extra_attribute: extra_attribute) 
    end 
    end 
end 

class WorkflowLibraryTemplateForm 
    include ActiveModel::Model 

    attr_accessor(
    :name, 
    :another_attribute, 
    :extra_attribute 
) 

    validates :name, presence: true 

    def create 
    if valid? 
     Workflow.create(
     name: name, is_a_template: true, 
     is_in_template_library: true, extra_attribute: extra_attribute) 
    end 
    end 
end 

Cette approche aide à tout est comme extensibilité un objet distinct. Le seul inconvénient de cela est que, à mon humble avis, WorkflowTemplate et WorkflowLibraryTemplate sont sémantique la même chose avec un booléen supplémentaire, mais c'est une option que vous pouvez prendre ou partir.