Boolsche Attribute bündeln mit enum in Rails

Boolschen Attribute bei ActiveRecord Modellen sind ein code smell. Sie werden oft verwendet, um Status abzubilden. Wenn es sich dabei lediglich um einen einzelnen Status handelt, ist ein boolean flag sicherlich vertretbar.
Allerdings bleibt es tendenziell oft nicht bei einem Status. Dann macht es Sinn, Alternativen in Betracht zu ziehen.
Natürlich könnten die Statuswerte als eigenständige Tabelle ausgelagert und referenziert werden. Das ist selten sinnvoll bei statischen (meist nur sehr wenigen) Werte, an denen keine weitere Logik hängt.
Eine weitere Alternative ist enum von ActiveRecord (seit Rails 4), wenn sich die Status nicht gegenseitig ausschließen (d.h. nur ein aktueller Status).

Original: Boolsche Attribute

Ausgehend von einem Product Modell mit den 3 Status sold, delivered und complained:

$ rails g model Product serial:string sold:boolean delivered:boolean complained:boolean

und dazu gehörigen Tests:

require 'spec_helper'

describe Product, type: :model do
  subject { Product.new serial: '123' }                                            
                                                                                
  it { is_expected.to have_db_column(:sold).of_type(:boolean) }
  it { is_expected.to have_db_column(:delivered).of_type(:boolean) }
  it { is_expected.to have_db_column(:complained).of_type(:boolean) }

  describe ".sold" do
    it "should return all sold products" do
      subject.sold = true
      subject.save!
      expect(Product.sold).to eq([subject])
    end
  end
end

Das Modell hat nur einen benannten scope. Es läßt sich leicht vorstellen, wie die Tests und das Modell aussehen würden, wenn für jeden Status ein scope erforderlich wäre:

class Product < ActiveRecord::Base
  scope :sold, -> { where(sold: true) }
end

Alternative: enum

Für Enum ist ein Integer Attribut (in diesem Fall state) erforderlich, dass die Werte aufnehmen soll:

$ rails g model Product serial:string state:integer:index

Das enum Attribut wird mit den Werten deklariert:

class Product < ActiveRecord::Base
  enum state: [:sold, :delivered, :complained]
end

Das Prinzip dahinter: die sprechenden Werte (sold, delivered und complained) werden von Rails auf Integer Werte gemappt und persistiert. Zum Auslesen dann vice versa.
Zusätzlich werden implizit auch benannte scopes an der Klasse und accessors an dem Objekt definiert:

Product.sold # =>  #<ActiveRecord::Relation []>
Product.new.sold? # => false
Product.states # => {"sold"=>0, "delivered"=>1, "complained"=>2}

Außerdem ist es sehr einfach, weitere Status hinzuzufügen, wenn es erforderlich ist, ohne an der Datenbankstruktur selber etwas ändern zu müssen. Es reicht, das Status Array um einen weiteren Wert zu ergänzen.
Dank Shoulda Matchers können die Erwartungen mit einem einzigen Test abgedeckt werden:

require 'spec_helper'

describe Produc, type: :model do
  subject { Product.new }

  it { is_expected.to define_enum_for(:state).with([:sold, :delivered, :complained]) }
end

Es ist allerdings zu beachten, dass Enum den Wert für den Datenbank Integer implizit von der Position innerhalb des Werte Arrays ableitet. Also 0 für sold, 1 für delivered und 2 für complained.
Die Mappingwerte können aber auch explizit definiert werden:

class Product < ActiveRecord::Base
  enum state: { sold: 10, delivered: 20, complained: 30 }
end

Die Hash Version macht es möglich, den Status für existierende Objekte zu refactoren.