Как реализовать одноэлементную модель

У меня есть сайт на рельсах, и я хочу иметь настройки для всего сайта. Одна часть моего приложения может уведомлять администратора по SMS, если происходит определенное событие. Это пример функции, которую я хочу настроить с помощью настроек всего сайта.

Так что я подумал, что у меня должна быть модель настройки или что-то в этом роде. Это должна быть модель, потому что я хочу иметь возможность has_many :contacts для SMS-уведомления.

Проблема в том, что для модели настроек может быть только один пост в базе данных. Итак, я думал об использовании модели Singleton, но это только предотвращает создание нового объекта, верно?

Нужно ли мне по-прежнему создавать методы получения и установки для каждого атрибута, например:

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

def self.attribute
  Model.first.attribute
end

Возможно, не рекомендуется использовать Model.attribute напрямую, но всегда создавать его экземпляр и использовать его?

Что мне здесь делать?


person Fredrik    schedule 30.12.2008    source источник


Ответы (13)


Я не уверен, что стал бы тратить накладные расходы базы данных/ActiveRecord/Model на такую ​​базовую потребность. Эти данные относительно статичны (я предполагаю), и расчеты на лету не нужны (включая поиск в базе данных).

Сказав это, я бы рекомендовал вам определить файл YAML с настройками для всего сайта и определить файл инициализатора, который загружает настройки в константу. У вас не будет почти столько ненужных движущихся частей.

Нет никаких причин, по которым данные не могли бы просто храниться в памяти и избавить вас от множества сложностей. Константы доступны везде, и их не нужно инициализировать или создавать экземпляры. Если вам абсолютно необходимо использовать класс как синглтон, я бы рекомендовал сделать следующие две вещи:

  1. undef метод инициализации/нового
  2. определите только методы self.* таким образом, что вы не сможете поддерживать состояние
person Derek P.    schedule 30.12.2008
comment
Использование файла .yml негибко и не позволяет пользователю (а не автору сайта) обновлять настройки. - person Undistraction; 27.06.2014

(Я согласен с @user43685 и не согласен с @Derek P - есть много веских причин хранить данные всего сайта в базе данных, а не в файле yaml. Например: ваши настройки будут доступны на всех веб-серверах (если вы иметь несколько веб-серверов); изменения в ваших настройках будут ACID; вам не нужно тратить время на реализацию оболочки YAML и т. д. и т. д.)

В рельсах это достаточно легко реализовать, вам просто нужно помнить, что ваша модель должна быть «одиночкой» в терминах базы данных, а не в терминах объекта ruby.

Самый простой способ реализовать это:

  1. Добавьте новую модель с одним столбцом для каждого свойства, которое вам нужно.
  2. Добавьте специальный столбец с именем «singleton_guard» и убедитесь, что он всегда равен «0», и пометьте его как уникальный (это обеспечит наличие только одной строки в базе данных для этой таблицы).
  3. Добавьте статический вспомогательный метод в класс модели для загрузки одноэлементной строки.

Таким образом, миграция должна выглядеть примерно так:

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)

И класс модели должен выглядеть примерно так:

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

В Rails >= 3.2.1 вы должны иметь возможность заменить тело геттера «instance» вызовом «first_or_create!" вот так:

def self.instance
  first_or_create!(singleton_guard: 0)
end
person Rich    schedule 17.09.2012
comment
На случай, если это сэкономит кому-то пару минут, я бы рекомендовал модель под названием Config: oldwiki.rubyonrails .org/rails/pages/ReservedWords - person ; 03.11.2012
comment
Спасибо, @Ricky, я переименовал модель - person Rich; 04.11.2012
comment
Эти зарезервированные слова будут получать вас каждый раз! - person ; 07.11.2012
comment
Почему бы просто не использовать класс AppSettings ‹ ActiveRecord::Base def self.instance AppSettings.first_or_create!(...) end end - person BvuRVKyUVlViVIc7; 11.05.2017
comment
Спасибо, @Lichtamberg, я добавил это к ответу. Я думаю, что этого не существовало в то время, когда я писал это; он был добавлен в Rails 3.2.1, январь 2012 г. - person Rich; 11.05.2017

Я не согласен с общепринятым мнением - нет ничего плохого в чтении свойства из базы данных. Вы можете прочитать значение базы данных и заморозить, если хотите, однако могут быть более гибкие альтернативы простой заморозке.

Чем YAML отличается от базы данных? .. та же тренировка - внешняя по отношению к коду приложения постоянная настройка.

Хорошая вещь в подходе к базе данных заключается в том, что ее можно изменить на лету более или менее безопасным способом (не открывая и не перезаписывая файлы напрямую). Еще одна приятная вещь — его можно использовать по сети между узлами кластера (при правильной реализации).

Однако остается вопрос, как правильно реализовать такую ​​настройку с помощью ActiveRecord.

person user43685    schedule 13.01.2009
comment
Я читаю, потому что у меня похожая проблема, но согласен, что файл YAML — плохой выбор. На моем сайте будет несколько пользователей с правами администратора, которые могут редактировать настройки сайта, и я не могу ожидать, что они будут редактировать файлы конфигурации. - person Jeff; 23.12.2011

Вы также можете установить максимум одну запись следующим образом:

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

Это переопределяет метод ActiveRecord, так что он взорвется, если вы попытаетесь создать новый экземпляр класса, когда он уже существует.

Затем вы можете определить только методы класса, которые действуют на одну запись:

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
person hoffm    schedule 28.06.2013
comment
Является ли ссылка на класс ShopGlobalSetting преднамеренной? Будет ли AppConfig count показывать вам, что у этого класса есть один экземпляр? - person Chris Bosco; 21.04.2015
comment
Нет, должно быть AppConfig. Хороший улов; Я изменю это. И да, он считает вызов должен работать. - person hoffm; 23.04.2015

Я знаю, что это старый поток, но мне просто нужно было то же самое, и я обнаружил, что для этого есть жемчужина: acts_as_singleton< /а>.

Инструкции по установке предназначены для Rails 2, но они прекрасно работают и с Rails 3.

person alf    schedule 26.02.2013

Скорее всего, вам не нужен синглтон. К сожалению, одна из худших дизайнерских привычек, вытекающая из повального увлечения шаблонами, также является одной из наиболее распространенных. Я виню неудачную видимость простоты, но я отвлекся. Если бы они назвали это шаблоном «Статический глобальный», я уверен, что люди были бы более смущенными при его использовании.

Я предлагаю использовать класс-оболочку с частным статическим экземпляром класса, который вы хотите использовать для синглтона. Вы не будете вводить тесную пару в свой код, как в случае с синглтоном.

Некоторые люди называют это шаблоном моносостояния. Я склонен думать об этом как об еще одном повороте концепции стратегии/агента, поскольку вы можете обеспечить большую гибкость, реализуя различные интерфейсы для раскрытия/скрытия функциональности.

person JGeiser    schedule 30.12.2008

Простой:

class AppSettings < ActiveRecord::Base 
  before_create do
    self.errors.add(:base, "already one setting object existing") and return false if AppSettings.exists?      
  end

  def self.instance
    AppSettings.first_or_create!(...) 
  end 
end
person BvuRVKyUVlViVIc7    schedule 11.05.2017

Сделаю несколько замечаний к предыдущим ответам:

  • нет необходимости в отдельном поле для уникального индекса, мы можем установить ограничение на поле id. Поскольку в любом случае у нас должно быть поле id в приложении Rails, это хороший компромисс.
  • не нужны специальные проверки модели и явное назначение идентификатора, достаточно указать значение по умолчанию для столбца

Если мы применим эти модификации, решение станет очень простым:

# migration
create_table :settings, id: false do |t|
  t.integer :id, null: false, primary_key: true, default: 1, index: {unique: true}
  t.integer :setting1
  t.integer :setting2
  ...
end

# model
class Settings < ApplicationRecord
  def self.instance
    first_or_create!(...)
  end  
end
person ka8725    schedule 10.12.2019
comment
Почему не def self.find(*args); first_or_create!; end вместо self.instance? - person Joshua Muheim; 10.12.2020
comment
@JoshuaMuheim решать тебе. instance - это просто общее название для одноэлементного метода, вот и все. - person ka8725; 11.12.2020

Использование has_many :contacts не означает, что вам нужна модель. has_many творит чудеса, но в итоге просто добавляет какой-то метод с указанным контрактом. Нет никаких причин, по которым вы не можете реализовать эти методы (или какое-то подмножество, которое вам нужно), чтобы ваша модель has_many :contacts вела себя так, как она, но на самом деле не использовала модель ActiveRecord (или модель вообще) для Contact.

person Otto    schedule 30.12.2008

Вы также можете проверить Configatron:

http://configatron.mackframework.com/

Configatron делает настройку ваших приложений и скриптов невероятно простой. Больше нет необходимости использовать константы или глобальные переменные. Теперь вы можете использовать простую и безболезненную систему для настройки своей жизни. И, поскольку это все Ruby, вы можете делать любые сумасшедшие вещи, которые захотите!

person user44104    schedule 02.01.2009

class Constant < ActiveRecord::Base
  after_initialize :readonly!

  def self.const_missing(name)
    first[name.to_s.downcase]
  end
end

Константа::FIELD_NAME

person araslanov_e    schedule 22.11.2016
comment
рассмотреть возможность перезаписать const_defined вместе с const_missing? apidock.com/ruby/Module/const_defined%3F . Я не уверен, но это может быть полезно - person gayavat; 25.02.2019

Вы можете сделать это следующим образом:

 class Config < ApplicationRecord
  def self.instance
    Config.first || Config.create!
  end
 end
person user3049407    schedule 13.02.2019

Я предполагаю использовать столбец наследования type с ограничением uniq.

# miragtion
class CreateSingletonRecords < ActiveRecord::Migration[5.2]
  create_table :balance_holders do |t|
    t.string :type
    t.index :type, unique: true

    t.timestamps
  end
end

несколько методов в вашем родительском классе:

class SingletonRecord < ApplicationRecord
  class << self
    def instance
      @singleton__instance__
    end

    def load_record(params = {})
      @singleton__instance__ = find_or_create_by!(params)
    end
  end

  load_record

  validates :type, uniqueness: true
end

После этого вы можете навсегда использовать одну запись для класса одноэлементной модели. Ваш экземпляр будет загружен или создан один раз во время загрузки класса модели.

person Кирилл Григорьев    schedule 14.02.2019