ruevaughn
ruevaughn

Reputation: 1319

Rails: Ensure only one boolean field is set to true at a time

I have a model that has fields of name:string, default:boolean. I want the true value to be unique so that only one item in the database can be set to true at once. How do I set my update and new actions in my controller to set all the rest of the values of items to false?

Let's say I have the following setup in my database

name:string | default:boolean |  
Item1       | true            |  
Item2       | false           |  
Item3       | false           |  

If I change Item2 default value to true, I want it to loop through all items and set the rest of them to false, so only one is true at once, so it looks like this.

name:string | default:boolean |  
Item1       | false           |  
Item2       | true            |  
Item3       | false           | 

Upvotes: 18

Views: 7955

Answers (9)

Marko Ćilimković
Marko Ćilimković

Reputation: 753

If you're coming here in a more recent time and are using Rails 6, this should be covered on the database level as well as the model level:

db level:

add_index :items, :default, unique: true, where: '(default IS TRUE)'

model level:

class Item < ApplicationRecord
  scope :default, -> { where(default: true) }
  
  validates :default, uniqueness: { conditions: -> { default } }
end

Upvotes: 2

mechnicov
mechnicov

Reputation: 15268

More ActiveRecord, less raw SQL decision

after_commit :reset_default, if: :default?

private

def reset_default
  self.class.where.not(id: id).where(default: true).update_all(default: false)
end

Upvotes: 1

fabriciofreitag
fabriciofreitag

Reputation: 2883

class Model < ApplicationRecord
  before_save :ensure_single_default, if: :is_default?

  private

  def ensure_single_default
    self.class.update_all(is_default: false)
  end
end

You don't need to check the id because this callback happens before the truthy one is saved.

Upvotes: 1

davidpm4
davidpm4

Reputation: 582

if you want this to work for creating and updating (rails v4) make note of this tidbit from rails guides

after_save runs both on create and update, but always after the more specific callbacks after_create and after_update, no matter the order in which the macro calls were executed.

Upvotes: 1

Philip
Philip

Reputation: 7166

I think it's good to check if the one you save is true before you falsify others. Otherwise you falsify everyone when you save a record that isn't active.

def falsify_all_others
    if self.default
        self.class.where('id != ? and default', self.id).update_all("default = 'false'")
    end
end

Upvotes: 5

Jason Aunkst
Jason Aunkst

Reputation: 161

Okay there is a few more things you will need.

Don't use the field name default, its usually reserved for the database. Saving a record with a default as false will set all records to false, this isnt what you want. check to see if we are setting this record to true and the falseify.

  before_save :falsify_all_others
  def falsify_all_others
    if is_default
      self.class.where('id != ?', self.id).where('is_default').update_all(:is_default => false)
    end
  end

Upvotes: 3

CrazyCoderMonkey
CrazyCoderMonkey

Reputation: 433

I also recommend falsifying all your records then making them true.

add_column :users, :name ,:boolean, default: false

Upvotes: 3

denis.peplin
denis.peplin

Reputation: 9851

This code is stolen from previous answer and slightly simplified:

def falsify_all_others
  Item.where('id != ?', self.id).update_all("default = 'false'")
end

You can use this method in before_save callback in your model.

Actually, it is better to "falsify" only records which values are 'true', like this:

Item.where('id != ? and default', self.id).update_all("default = 'false'")

UPDATE: to keep code DRY, use self.class instead of Item:

self.class.where('id != ? and default', self.id).update_all("default = 'false'")

Upvotes: 22

Norto23
Norto23

Reputation: 2269

In your controller code you could do something like this.... please note you're probably taking Item2 as a param[...] so you can interchange that below

@items_to_be_falsified = Item.where('id != ?', Item2.id)

@items_to_be_falsified.each do |item|
  item.default = false
  item.save
end

Please note when you get this working, its good practice to move this into the model, make it into a function and call it like Item2.falsify_all_others like below

def falsify_all_others
  Item.where('id != ?', self.id).each do |item|
    item.default = false
    item.save
  end
end

Enjoy!

Upvotes: 3

Related Questions