33 votes

Comment implémenter un modèle singleton

J'ai un site dans rails et je veux avoir des paramètres pour tout le site. Une partie de mon application peut notifier l'administrateur par SMS si un événement spécifique se produit. Il s'agit d'un exemple de fonctionnalité que je souhaite configurer via les paramètres du site.

Alors je me suis dit que je devrais avoir un modèle de réglage ou quelque chose comme ça. Il doit s'agir d'un modèle car je veux pouvoir utiliser la fonction has_many :contacts pour la notification par SMS.

Le problème est qu'il ne peut y avoir qu'un seul message dans la base de données pour le modèle de paramètres. J'ai donc pensé à utiliser un modèle Singleton, mais cela ne fait qu'empêcher la création d'un nouvel objet, n'est-ce pas ?

Devrais-je encore créer des méthodes getter et setter pour chaque attribut, comme suit ?

def self.attribute=(param)
  Model.first.attribute = param
end

def self.attribute
  Model.first.attribute
end

N'est-il pas préférable de ne pas utiliser directement Model.attribute mais de toujours créer une instance de celui-ci et de l'utiliser ?

Que dois-je faire ici ?

69voto

Rich Points 1870

(Je suis d'accord avec @user43685 et pas d'accord avec @Derek P -- il y a beaucoup de bonnes raisons de garder les données du site dans la base de données plutôt que dans un fichier yaml. Par exemple : vos paramètres seront disponibles sur tous les serveurs web (si vous avez plusieurs serveurs web) ; les modifications de vos paramètres seront ACID ; vous n'avez pas besoin de passer du temps à implémenter un wrapper YAML, etc. etc.)

Dans rails, c'est assez facile à mettre en œuvre, vous devez juste vous rappeler que votre modèle doit être un "singleton" en termes de base de données, pas en termes d'objet ruby.

La façon la plus simple de mettre cela en œuvre est :

  1. Ajouter un nouveau modèle, avec une colonne pour chaque propriété dont vous avez besoin
  2. Ajoutez une colonne spéciale appelée "singleton_guard", validez qu'elle est toujours égale à "0", et marquez-la comme unique (cela garantira qu'il n'y a qu'une seule ligne dans la base de données pour cette table).
  3. Ajoutez une méthode d'aide statique à la classe du modèle pour charger la rangée singleton.

La migration devrait donc ressembler à ceci :

create_table :app_settings do |t|
  t.integer  :singleton_guard
  t.datetime :config_property1
  t.datetime :config_property2
  ...

  t.timestamps
end
add_index(:app_settings, :singleton_guard, :unique => true)

Et la classe de modèle devrait ressembler à quelque chose comme ça :

class AppSettings < ActiveRecord::Base
  # The "singleton_guard" column is a unique column which must always be set to '0'
  # This ensures that only one AppSettings row is created
  validates_inclusion_of :singleton_guard, :in => [0]

  def self.instance
    # there will be only one row, and its ID must be '1'
    begin
      find(1)
    rescue ActiveRecord::RecordNotFound
      # slight race condition here, but it will only happen once
      row = AppSettings.new
      row.singleton_guard = 0
      row.save!
      row
    end
  end
end

Dans Rails >= 3.2.1, vous devriez être en mesure de remplacer le corps du getter "instance" par un appel à " premier_ou_créer ! " comme ça :

def self.instance
  first_or_create!(singleton_guard: 0)
end

25voto

user43685 Points 499

Je ne suis pas d'accord avec l'opinion commune - il n'y a rien de mal à lire une propriété dans la base de données. Vous pouvez lire la valeur de la base de données et geler si vous le souhaitez, mais il pourrait y avoir des alternatives plus flexibles au simple gel.

En quoi YAML est-il différent de la base de données ? même exercice - externe au code de l'application paramètre persistant.

L'avantage de l'approche par base de données est qu'elle peut être modifiée à la volée de manière plus ou moins sécurisée (sans ouvrir et écraser directement les fichiers). Un autre point positif est qu'elle peut être partagée sur le réseau entre les nœuds du cluster (si elle est correctement implémentée).

La question reste cependant de savoir quelle serait la bonne façon d'implémenter un tel paramètre en utilisant ActiveRecord.

14voto

hoffm Points 666

Vous pourriez également appliquer un maximum d'un enregistrement comme suit :

class AppConfig < ActiveRecord::Base

  before_create :confirm_singularity

  private

  def confirm_singularity
    raise Exception.new("There can be only one.") if AppConfig.count > 0
  end

end

Cela remplace le ActiveRecord de manière à ce qu'elle explose si vous essayez de créer une nouvelle instance de la classe alors qu'il en existe déjà une.

Vous pouvez ensuite définir uniquement les méthodes de classe qui agissent sur l'enregistrement en question :

class AppConfig < ActiveRecord::Base

  attr_accessible :some_boolean
  before_create :confirm_singularity

  def self.some_boolean?
    settings.some_boolean
  end

  private

  def confirm_singularity
    raise Exception.new("There can be only one.") if AppConfig.count > 0
  end

  def self.settings
    first
  end

end

8voto

Derek P. Points 1306

Je ne suis pas sûr de vouloir gaspiller la base de données/ActiveRecord/Model pour un besoin aussi basique. Ces données sont relativement statiques (je suppose) et les calculs à la volée ne sont pas nécessaires (y compris les consultations de la base de données).

Cela dit, je vous recommande de définir un fichier YAML avec les paramètres de votre site et de définir un fichier d'initialisation qui charge les paramètres dans une constante. Vous n'aurez pas autant de pièces mobiles inutiles.

Il n'y a aucune raison pour que les données ne puissent pas rester en mémoire et vous épargner une tonne de complexité. Les constantes sont disponibles partout, et elles n'ont pas besoin d'être initialisées ou instanciées. Si vous devez absolument utiliser une classe comme singleton, je vous recommande de faire ces deux choses :

  1. Undef la méthode d'initialisation/nouvelle
  2. ne définir que des méthodes self.* de cette façon il ne vous est pas possible de maintenir un état

6voto

alfonso Points 8420

Je sais que c'est un vieux fil de discussion, mais je viens d'avoir besoin de la même chose et j'ai découvert qu'il existe un bijou pour cela : agit_comme_singleton .

Les instructions d'installation sont pour Rails 2, mais cela fonctionne très bien avec Rails 3 également.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X