I am building an application and I need to be able to sing a lead teacher
I need to prevent that 2 teachers share the title of lead for a particular class
class CreateClassroomTeachers < ActiveRecord::Migration[5.2]
def change
create_table :classroom_teachers do |t|
t.belongs_to :classroom
t.belongs_to :teacher
t.boolean :lead, default: false
end
add_index :household_people, [:classroom_id, :teacher_id], unique: true
# Only one teacher in a classroom can be lead
end
end
I have this in my model
class ClassroomTeacher < ApplicationRecord
belongs_to :classroom
belongs_to :teacher
validate :only_one_is_lead_teacher
def only_one_is_lead_teacher
if lead
if ClassroomTeacher.where(classroom_id: classroom_id, lead: true).count > 0
errors.add(:lead, "There can only be one (1) lead teacher per classroom")
end
end
end
end
The problem on this is that on Create I can have 2 or more teachers be lead
Thanks for the help
There's several ways for achieving this with constraints, triggers etc. – depending on what your respective database server supports.
What should work at least in Postgres (even though it might be slightly hacky) is to set a unique index on %i[classroom_id lead] and make sure that lead is either true or NULL. This should work because Postgres treats NULL values as distinct, meaning that it doesn't complain if multiple NULL values are stored in a column that has a uniqueness constraint on it.
If you want to solve it in code (which personally I would not recommend, because your database might be access by things other than your code and even your code can work around it, e.g. by directly writing to the database instead of using ActiveRecord's higher level methods), here's how I've done this in the past:
class ClassroomTeacher < ActiveRecord::Base
before_save :ensure_only_one_lead_teacher
private
def ensure_only_one_lead_teacher
# We don't have to do this unless the record is the one who should be the (new) lead.
return unless lead?
# Set all other records for the class room to lead = false.
self.class.where(classroom_id: classroom_id).update_all(lead: false)
# Now if the record gets persisted, it will be the only one with lead = true.
end
end
A probably slightly more "correct" approach would be to ensure the uniqueness after the record has been persisted:
class ClassroomTeacher < ActiveRecord::Base
after_commit :ensure_only_one_lead_teacher
private
def ensure_only_one_lead_teacher
# We don't have to do this unless the record is the one who should be the (new) lead.
return unless lead?
# Set all other records for the class room to lead = false. Note that we now have to exclude
# the record itself by id because it has already been saved.
self.class.where.not(id: id).where(classroom_id: classroom_id).update_all(lead: false)
end
end
As per the migration, attributes for the model are
ClassroomTeacher: classroom_id, teacher_id, lead
Considering teachers are being added to class:
/controller file
def create
ClassroomTeacher.create(teacher_id: data1, classroom_id: data2, lead: data3)
end
Possible sample data with ideal values would be:
id classroom_id teacher_id lead
1 1 3 false
2 2 4 true
3 1 2 false
4 1 5 true
Now you need to avoid any new teachers being added to the classroom as lead. Model validation code could be
validate :only_one_is_lead_teacher
def only_one_is_lead_teacher
if self.lead
class_obj = ClassroomTeacher.where(classroom_id: self.classroom_id, lead: true).first
if class_obj.present?
self.errors.add(:lead, "There can only be one (1) lead teacher per classroom")
return false
end
end
end
Could not find nothing close to what I'm trying to do. I want to store an object into a user's column. That column is in the form of an array:
#postgres
def change
add_column :users, :interest, :string, array: true, default: '{}'
end
I have another model called FooBar setup for other use. Each user has unique information inside as I've added a user_id key.
Im trying to make more sense:
def interest
#user = User.find(current_user.id ) # I need the logged in user's id
#support = Support.find(params[:id]) # I need the post's id they are on
u = FooBar.new
u.user_id = #user
u.support_id = #support
u.save # This saves a new Foo object..this is what I want
#user.interest.push(FooBar.find(#user)) # This just stores the object name itself ;)
end
So when I call u1 = FooBar.find(1) I get value return in hash. I want when I say u1.interest I get the same. The reason is, I need to target those keys on the user ie: u1.interest[0].support_id
Is this possible? I've looked over my basic ruby docs and nothing works. Oh..if I passed FooBar.find(#user).inspect I get the hash but not the way I want it.
Im trying to do something similar to stripe. Look at their data key. That's a hash.
Edit for Rich' answer:
I have, literally, a model called UserInterestSent model and table:
class UserInterestSent < ActiveRecord::Base
belongs_to :user
belongs_to :support # you can call this post
end
class CreateUserInterestSents < ActiveRecord::Migration
def change
create_table :user_interest_sents do |t|
t.integer :user_id # user's unique id to associate with post (support)
t.integer :interest_sent, :default => 0 # this will manually set to 1
t.integer :support_id, :default => 0 # id of the post they're on
t.timestamps # I need the time it was sent/requested for each user
end
end
end
I call interest interest_already_sent:
supports_controller.rb:
def interest_already_sent
support = Support.find(params[:id])
u = UserInterestSent.new(
{
'interest_sent' => 1, # they can only send one per support (post)
'user_id' => current_user.id, # here I add the current user
'support_id' => support.id, # and the post id they're on
})
current_user.interest << u # somewhere this inserts twice with different timestamps
end
And the interest not interests, column:
class AddInterestToUsers < ActiveRecord::Migration
def change
add_column :users, :interest, :text
end
end
HStore
I remembered there's a PGSQL datatype called hStore:
This module implements the hstore data type for storing sets of
key/value pairs within a single PostgreSQL value. This can be useful
in various scenarios, such as rows with many attributes that are
rarely examined, or semi-structured data. Keys and values are simply
text strings.
Heroku supports it and I've seen it used on another live application I was observing.
It won't store your object in the same way as Stripe's data attribute (for that, you'll just need to use text and save the object itself), but you can store a series of key:value pairs (JSON).
I've never used it before, but I'd imagine you can send a JSON object to the column, and it will allow you to to use the attributes you need. There's a good tutorial here, and Rails documentation here:
# app/models/profile.rb
class Profile < ActiveRecord::Base
end
Profile.create(settings: { "color" => "blue", "resolution" => "800x600" })
profile = Profile.first
profile.settings # => {"color"=>"blue", "resolution"=>"800x600"}
profile.settings = {"color" => "yellow", "resolution" => "1280x1024"}
profile.save!
--
This means you should be able to just pass JSON objects to your hstore column:
#app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
def update
#profile = current_user.profile
#profile.update profile_params
end
private
def profile_params
params.require(:profile).permit(:x, :y, :z) #-> z = {"color": "blue", "weight": "heavy"}
end
end
As per your comments, it seems to me that you're trying to store "interest" in a User from another model.
My first interpretation was that you wanted to store a hash of information in your #user.interests column. Maybe you'd have {name: "interest", type: "sport"} or something.
From your comments, it seems like you're wanting to store associated objects/data in this column. If this is the case, the way you're doing it should be to use an ActiveRecord association.
If you don't know what this is, it's essentially a way to connect two or more models together through foreign keys in your DB. The way you set it up will determine what you can store & how...
#app/models/user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :interests,
class_name: "Support",
join_table: :users_supports,
foreign_key: :user_id,
association_foreign_key: :support_id
end
#app/models/support.rb
class Support < ActiveRecord::Base
has_and_belongs_to_many :users,
class_name: "Support",
join_table: :users_supports,
foreign_key: :support_id,
association_foreign_key: :user_id
end
#join table = users_supports (user_id, support_id)
by using this, you can populate the .interests or .users methods respectively:
#config/routes.rb
resources :supports do
post :interest #-> url.com/supports/:support_id/interest
end
#app/controllers/supports_controller.rb
class SupportsController < ApplicationController
def interest
#support = Support.find params[:support_id] # I need the post's id they are on
current_user.interests << #support
end
end
This will allow you to call #user.interests and bring back a collection of Support objects.
Okay, look.
What I suggested was an alternative to using interest column.
You seem to want to store a series of hashes for an associated model. This is exactly what many-to-many relationships are for.
The reason your data is being populated twice is because you're invoking it twice (u= is creating a record directly on the join model, and then you're inserting more data with <<).
I must add that in both instances, the correct behaviour is occurring; the join model is being populated, allowing you to call the associated objects.
What you're going for is something like this:
def interest_already_sent
support = Support.find params[:id]
current_user.interests << support
end
When using the method I recommended, get rid of the interest column.
You can call .interests through your join table.
When using the code above, it's telling Rails to insert the support object (IE support_id into the current_user (IE user_id) interests association (populated with the UserInterestSelf table).
This will basically then add a new record to this table with the user_id of current_user and the support_id of support.
EDIT
To store Hash into column, I suggest you to use "text" instead
def change
add_column :users, :interest, :text
end
and then set "serialize" to attribute
class User < ActiveRecord::Base
serialize :interest
end
once it's done, you can save hash object properly
def interest
#user = User.find(current_user.id ) # I need the logged in user's id
#support = Support.find(params[:id]) # I need the post's id they are on
u = FooBar.new
u.user_id = #user
u.support_id = #support
u.save # This saves a new Foo object..this is what I want
#user.interest = u.attributes # store hash
#user.save
end
To convert AR object to hash use object.attributes.
To store a custom hash in a model field you can use serialize or ActiveRecord::Store
You can also use to_json method as object.to_json
User.find(current_user.id ).to_json # gives a json string
Using Rails 3.1.3 and I'm trying to figure out why our counter caches aren't being updated correctly when changing the parent record id via update_attributes.
class ExhibitorRegistration < ActiveRecord::Base
belongs_to :event, :counter_cache => true
end
class Event < ActiveRecord::Base
has_many :exhibitor_registrations, :dependent => :destroy
end
describe ExhibitorRegistration do
it 'correctly maintains the counter cache on events' do
event = Factory(:event)
other_event = Factory(:event)
registration = Factory(:exhibitor_registration, :event => event)
event.reload
event.exhibitor_registrations_count.should == 1
registration.update_attributes(:event_id => other_event.id)
event.reload
event.exhibitor_registrations_count.should == 0
other_event.reload
other_event.exhibitor_registrations_count.should == 1
end
end
This spec fails indicating that the counter cache on event is not being decremented.
1) ExhibitorRegistration correctly maintains the counter cache on events
Failure/Error: event.exhibitor_registrations_count.should == 0
expected: 0
got: 1 (using ==)
Should I even expect this to work or do I need to manually track the changes and update the counter myself?
From the fine manual:
:counter_cache
Caches the number of belonging objects on the associate class through the use of increment_counter and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it’s destroyed.
There's no mention of updating the cache when an object is moved from one owner to another. Of course, the Rails documentation is often incomplete so we'll have to look at the source for confirmation. When you say :counter_cache => true, you trigger a call to the private add_counter_cache_callbacks method and add_counter_cache_callbacks does this:
Adds an after_create callback which calls increment_counter.
Adds an before_destroy callback which calls decrement_counter.
Calls attr_readonly to make the counter column readonly.
I don't think you're expecting too much, you're just expecting ActiveRecord to be more complete than it is.
All is not lost though, you can fill in the missing pieces yourself without too much effort. If you want to allow reparenting and have your counters updated, you can add a before_save callback to your ExhibitorRegistration that adjusts the counters itself, something like this (untested demo code):
class ExhibitorRegistration < ActiveRecord::Base
belongs_to :event, :counter_cache => true
before_save :fix_counter_cache, :if => ->(er) { !er.new_record? && er.event_id_changed? }
private
def fix_counter_cache
Event.decrement_counter(:exhibitor_registration_count, self.event_id_was)
Event.increment_counter(:exhibitor_registration_count, self.event_id)
end
end
If you were adventurous, you could patch something like that into ActiveRecord::Associations::Builder#add_counter_cache_callbacks and submit a patch. The behavior you're expecting is reasonable and I think it would make sense for ActiveRecord to support it.
If your counter has been corrupted or you've modified it directly by SQL, you can fix it.
Using:
ModelName.reset_counters(id_of_the_object_having_corrupted_count, one_or_many_counters)
Example 1: Re-compute the cached count on the post with id = 17.
Post.reset_counters(17, :comments)
Source
Example 2: Re-compute the cached count on all your articles.
Article.ids.each { |id| Article.reset_counters(id, :comments) }
I recently came across this same problem (Rails 3.2.3). Looks like it has yet to be fixed, so I had to go ahead and make a fix. Below is how I amended ActiveRecord::Base and utilize after_update callback to keep my counter_caches in sync.
Extend ActiveRecord::Base
Create a new file lib/fix_counters_update.rb with the following:
module FixUpdateCounters
def fix_updated_counters
self.changes.each {|key, value|
# key should match /master_files_id/ or /bibls_id/
# value should be an array ['old value', 'new value']
if key =~ /_id/
changed_class = key.sub(/_id/, '')
changed_class.camelcase.constantize.decrement_counter(:"#{self.class.name.underscore.pluralize}_count", value[0]) unless value[0] == nil
changed_class.camelcase.constantize.increment_counter(:"#{self.class.name.underscore.pluralize}_count", value[1]) unless value[1] == nil
end
}
end
end
ActiveRecord::Base.send(:include, FixUpdateCounters)
The above code uses the ActiveModel::Dirty method changes which returns a hash containing the attribute changed and an array of both the old value and new value. By testing the attribute to see if it is a relationship (i.e. ends with /_id/), you can conditionally determine whether decrement_counter and/or increment_counter need be run. It is essnetial to test for the presence of nil in the array, otherwise errors will result.
Add to Initializers
Create a new file config/initializers/active_record_extensions.rb with the following:
require 'fix_update_counters'
Add to models
For each model you want the counter caches updated add the callback:
class Comment < ActiveRecord::Base
after_update :fix_updated_counters
....
end
A fix for this has been merged in to active record master
https://github.com/rails/rails/issues/9722
The counter_cache function is designed to work through the association name, not the underlying id column. In your test, instead of:
registration.update_attributes(:event_id => other_event.id)
try
registration.update_attributes(:event => other_event)
More information can be found here: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Clients have many Invoices. Invoices have a number attribute that I want to initialize by incrementing the client's previous invoice number.
For example:
#client = Client.find(1)
#client.last_invoice_number
> 14
#invoice = #client.invoices.build
#invoice.number
> 15
I want to get this functionality into my Invoice model, but I'm not sure how to. Here's what I'm imagining the code to be like:
class Invoice < ActiveRecord::Base
...
def initialize(attributes = {})
client = Client.find(attributes[:client_id])
attributes[:number] = client.last_invoice_number + 1
client.update_attributes(:last_invoice_number => client.last_invoice_number + 1)
end
end
However, attributes[:client_id] isn't set when I call #client.invoices.build.
How and when is the invoice's client_id initialized, and when can I use it to initialize the invoice's number? Can I get this logic into the model, or will I have to put it in the controller?
Generate a migration that adds invoices_number column to users table. Then in Invoice model write this:
class Invoice < ActiveRecord::Base
belongs_to :user, :counter_cache => true
...
end
This will automatically increase invoices_count attribute for user once the invoice is created.
how about this:
class Invoice < ActiveRecord::Base
...
def initialize(attributes = {})
super
self.number = self.client.invoices.size + 1 unless self.client.nil?
end
end
Here is some useful discussion on after_initialize per Jonathan R. Wallace's comment above:
http://blog.dalethatcher.com/2008/03/rails-dont-override-initialize-on.html
first of all, you don't need to use the attributes collection, you can just do self.client_id. Better yet, as long as you have a belongs_to :client in your Invoice, you could just do self.client.last_invoice_number. Lastly, you almost always want to raise an exception if an update or create fails, so get used to using update_attributes!, which is a better default choice. (if you have any questions about those points, ask, and I'll go into more detail)
Now that that is out of the way, you ran into a bit of a gotcha with ActiveRecord, initializer methods are almost never the right choice. AR gives you a bunch of methods to hook into whatever point of the lifecycle you need to. These are
after_create
after_destroy
after_save
after_update
after_validation
after_validation_on_create
after_validation_on_update
before_create
before_destroy
before_save
before_update
before_validation
before_validation_on_create
before_validation_on_update
What you probably want is to hook into before_create. Something like this
def before_create
self.number ||= self.client.last_invoice_number + 1 unless self.client
end
What that will do is it will hit up the database for your client, get the last invoice number, increment it by one, and set it as its new number, but only if you haven't already set a number (||= will assign, but only if the left side is nil), and only if you have set a client (or client_id) before the save.
How can I set default value in ActiveRecord?
I see a post from Pratik that describes an ugly, complicated chunk of code: http://m.onkey.org/2007/7/24/how-to-set-default-values-in-your-model
class Item < ActiveRecord::Base
def initialize_with_defaults(attrs = nil, &block)
initialize_without_defaults(attrs) do
setter = lambda { |key, value| self.send("#{key.to_s}=", value) unless
!attrs.nil? && attrs.keys.map(&:to_s).include?(key.to_s) }
setter.call('scheduler_type', 'hotseat')
yield self if block_given?
end
end
alias_method_chain :initialize, :defaults
end
I have seen the following examples googling around:
def initialize
super
self.status = ACTIVE unless self.status
end
and
def after_initialize
return unless new_record?
self.status = ACTIVE
end
I've also seen people put it in their migration, but I'd rather see it defined in the model code.
Is there a canonical way to set default value for fields in ActiveRecord model?
There are several issues with each of the available methods, but I believe that defining an after_initialize callback is the way to go for the following reasons:
default_scope will initialize values for new models, but then that will become the scope on which you find the model. If you just want to initialize some numbers to 0 then this is not what you want.
Defining defaults in your migration also works part of the time... As has already been mentioned this will not work when you just call Model.new.
Overriding initialize can work, but don't forget to call super!
Using a plugin like phusion's is getting a bit ridiculous. This is ruby, do we really need a plugin just to initialize some default values?
Overriding after_initialize is deprecated as of Rails 3. When I override after_initialize in rails 3.0.3 I get the following warning in the console:
DEPRECATION WARNING: Base#after_initialize has been deprecated, please use Base.after_initialize :method instead. (called from /Users/me/myapp/app/models/my_model:15)
Therefore I'd say write an after_initialize callback, which lets you default attributes in addition to letting you set defaults on associations like so:
class Person < ActiveRecord::Base
has_one :address
after_initialize :init
def init
self.number ||= 0.0 #will set the default value only if it's nil
self.address ||= build_address #let's you set a default association
end
end
Now you have just one place to look for initialization of your models. I'm using this method until someone comes up with a better one.
Caveats:
For boolean fields do:
self.bool_field = true if self.bool_field.nil?
See Paul Russell's comment on this answer for more details
If you're only selecting a subset of columns for a model (ie; using select in a query like Person.select(:firstname, :lastname).all) you will get a MissingAttributeError if your init method accesses a column that hasn't been included in the select clause. You can guard against this case like so:
self.number ||= 0.0 if self.has_attribute? :number
and for a boolean column...
self.bool_field = true if (self.has_attribute? :bool_value) && self.bool_field.nil?
Also note that the syntax is different prior to Rails 3.2 (see Cliff Darling's comment below)
Rails 5+
You can use the attribute method within your models, eg.:
class Account < ApplicationRecord
attribute :locale, :string, default: 'en'
end
You can also pass a lambda to the default parameter. Example:
attribute :uuid, :string, default: -> { SecureRandom.uuid }
The second argument is the type and it can also be a custom type class instance, for example:
attribute :uuid, UuidType.new, default: -> { SecureRandom.uuid }
We put the default values in the database through migrations (by specifying the :default option on each column definition) and let Active Record use these values to set the default for each attribute.
IMHO, this approach is aligned with the principles of AR : convention over configuration, DRY, the table definition drives the model, not the other way around.
Note that the defaults are still in the application (Ruby) code, though not in the model but in the migration(s).
Some simple cases can be handled by defining a default in the database schema but that doesn't handle a number of trickier cases including calculated values and keys of other models. For these cases I do this:
after_initialize :defaults
def defaults
unless persisted?
self.extras||={}
self.other_stuff||="This stuff"
self.assoc = [OtherModel.find_by_name('special')]
end
end
I've decided to use the after_initialize but I don't want it to be applied to objects that are found only those new or created. I think it is almost shocking that an after_new callback isn't provided for this obvious use case but I've made do by confirming whether the object is already persisted indicating that it isn't new.
Having seen Brad Murray's answer this is even cleaner if the condition is moved to callback request:
after_initialize :defaults, unless: :persisted?
# ":if => :new_record?" is equivalent in this context
def defaults
self.extras||={}
self.other_stuff||="This stuff"
self.assoc = [OtherModel.find_by_name('special')]
end
The after_initialize callback pattern can be improved by simply doing the following
after_initialize :some_method_goes_here, :if => :new_record?
This has a non-trivial benefit if your init code needs to deal with associations, as the following code triggers a subtle n+1 if you read the initial record without including the associated.
class Account
has_one :config
after_initialize :init_config
def init_config
self.config ||= build_config
end
end
The Phusion guys have some nice plugin for this.
An even better/cleaner potential way than the answers proposed is to overwrite the accessor, like this:
def status
self['status'] || ACTIVE
end
See "Overwriting default accessors" in the ActiveRecord::Base documentation and more from StackOverflow on using self.
I use the attribute-defaults gem
From the documentation:
run sudo gem install attribute-defaults and add require 'attribute_defaults' to your app.
class Foo < ActiveRecord::Base
attr_default :age, 18
attr_default :last_seen do
Time.now
end
end
Foo.new() # => age: 18, last_seen => "2014-10-17 09:44:27"
Foo.new(:age => 25) # => age: 25, last_seen => "2014-10-17 09:44:28"
Similar questions, but all have slightly different context:
- How do I create a default value for attributes in Rails activerecord's model?
Best Answer: Depends on What You Want!
If you want every object to start with a value: use after_initialize :init
You want the new.html form to have a default value upon opening the page? use https://stackoverflow.com/a/5127684/1536309
class Person < ActiveRecord::Base
has_one :address
after_initialize :init
def init
self.number ||= 0.0 #will set the default value only if it's nil
self.address ||= build_address #let's you set a default association
end
...
end
If you want every object to have a value calculated from user input: use before_save :default_values
You want user to enter X and then Y = X+'foo'? use:
class Task < ActiveRecord::Base
before_save :default_values
def default_values
self.status ||= 'P'
end
end
I've also seen people put it in their migration, but I'd rather see it
defined in the model code.
Is there a canonical way to set default value for fields in
ActiveRecord model?
The canonical Rails way, before Rails 5, was actually to set it in the migration, and just look in the db/schema.rb for whenever wanting to see what default values are being set by the DB for any model.
Contrary to what #Jeff Perrin answer states (which is a bit old), the migration approach will even apply the default when using Model.new, due to some Rails magic. Verified working in Rails 4.1.16.
The simplest thing is often the best. Less knowledge debt and potential points of confusion in the codebase. And it 'just works'.
class AddStatusToItem < ActiveRecord::Migration
def change
add_column :items, :scheduler_type, :string, { null: false, default: "hotseat" }
end
end
Or, for column change without creating a new one, then do either:
class AddStatusToItem < ActiveRecord::Migration
def change
change_column_default :items, :scheduler_type, "hotseat"
end
end
Or perhaps even better:
class AddStatusToItem < ActiveRecord::Migration
def change
change_column :items, :scheduler_type, :string, default: "hotseat"
end
end
Check the official RoR guide for options in column change methods.
The null: false disallows NULL values in the DB, and, as an added benefit, it also updates so that all pre-existing DB records that were previously null is set with the default value for this field as well. You may exclude this parameter in the migration if you wish, but I found it very handy!
The canonical way in Rails 5+ is, as #Lucas Caton said:
class Item < ActiveRecord::Base
attribute :scheduler_type, :string, default: 'hotseat'
end
This is what constructors are for! Override the model's initialize method.
Use the after_initialize method.
Sup guys, I ended up doing the following:
def after_initialize
self.extras||={}
self.other_stuff||="This stuff"
end
Works like a charm!
Rails 6.1+
You can now use the attribute method on your model without setting a type.
attribute :status, default: ACTIVE
or
class Account < ApplicationRecord
attribute :locale, default: 'en'
end
Note that feeding a default to attribute cannot reference the instance of the class (a lambda will execute in the context of the class, not the instance). So, if you need to set the default to a value dynamically based on the instance or associations, you're still going to have to use an alternative, such as an after_initialize callback. As stated previously, it's recommended to limit this to new records only to avoid n+1 queries if you reference associations.
after_initialize :do_something_that_references_instance_or_associations, if: :new_record?
This has been answered for a long time, but I need default values frequently and prefer not to put them in the database. I create a DefaultValues concern:
module DefaultValues
extend ActiveSupport::Concern
class_methods do
def defaults(attr, to: nil, on: :initialize)
method_name = "set_default_#{attr}"
send "after_#{on}", method_name.to_sym
define_method(method_name) do
if send(attr)
send(attr)
else
value = to.is_a?(Proc) ? to.call : to
send("#{attr}=", value)
end
end
private method_name
end
end
end
And then use it in my models like so:
class Widget < ApplicationRecord
include DefaultValues
defaults :category, to: 'uncategorized'
defaults :token, to: -> { SecureRandom.uuid }
end
I ran into problems with after_initialize giving ActiveModel::MissingAttributeError errors when doing complex finds:
eg:
#bottles = Bottle.includes(:supplier, :substance).where(search).order("suppliers.name ASC").paginate(:page => page_no)
"search" in the .where is hash of conditions
So I ended up doing it by overriding initialize in this way:
def initialize
super
default_values
end
private
def default_values
self.date_received ||= Date.current
end
The super call is necessary to make sure the object initializing correctly from ActiveRecord::Base before doing my customize code, ie: default_values
after_initialize method is deprecated, use the callback instead.
after_initialize :defaults
def defaults
self.extras||={}
self.other_stuff||="This stuff"
end
however, using :default in your migrations is still the cleanest way.
The problem with the after_initialize solutions is that you have to add an after_initialize to every single object you look up out of the DB, regardless of whether you access this attribute or not. I suggest a lazy-loaded approach.
The attribute methods (getters) are of course methods themselves, so you can override them and provide a default. Something like:
Class Foo < ActiveRecord::Base
# has a DB column/field atttribute called 'status'
def status
(val = read_attribute(:status)).nil? ? 'ACTIVE' : val
end
end
Unless, like someone pointed out, you need to do Foo.find_by_status('ACTIVE'). In that case I think you'd really need to set the default in your database constraints, if the DB supports it.
class Item < ActiveRecord::Base
def status
self[:status] or ACTIVE
end
before_save{ self.status ||= ACTIVE }
end
I strongly suggest using the "default_value_for" gem: https://github.com/FooBarWidget/default_value_for
There are some tricky scenarios that pretty much require overriding the initialize method, which that gem does.
Examples:
Your db default is NULL, your model/ruby-defined default is "some string", but you actually want to set the value to nil for whatever reason: MyModel.new(my_attr: nil)
Most solutions here will fail to set the value to nil, and will instead set it to the default.
OK, so instead of taking the ||= approach, you switch to my_attr_changed?...
BUT now imagine your db default is "some string", your model/ruby-defined default is "some other string", but under a certain scenario, you want to set the value to "some string" (the db default): MyModel.new(my_attr: 'some_string')
This will result in my_attr_changed? being false because the value matches the db default, which in turn will fire your ruby-defined default code and set the value to "some other string" -- again, not what you desired.
For those reasons I don't think this can properly be accomplished with just an after_initialize hook.
Again, I think the "default_value_for" gem is taking the right approach: https://github.com/FooBarWidget/default_value_for
Although doing that for setting default values is confusing and awkward in most cases, you can use :default_scope as well. Check out squil's comment here.
I've found that using a validation method provides a lot of control over setting defaults. You can even set defaults (or fail validation) for updates. You even set a different default value for inserts vs updates if you really wanted to.
Note that the default won't be set until #valid? is called.
class MyModel
validate :init_defaults
private
def init_defaults
if new_record?
self.some_int ||= 1
elsif some_int.nil?
errors.add(:some_int, "can't be blank on update")
end
end
end
Regarding defining an after_initialize method, there could be performance issues because after_initialize is also called by each object returned by :find :
http://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initialize-and-after_find
If the column happens to be a 'status' type column, and your model lends itself to the use of state machines, consider using the aasm gem, after which you can simply do
aasm column: "status" do
state :available, initial: true
state :used
# transitions
end
It still doesn't initialize the value for unsaved records, but it's a bit cleaner than rolling your own with init or whatever, and you reap the other benefits of aasm such as scopes for all your statuses.
https://github.com/keithrowell/rails_default_value
class Task < ActiveRecord::Base
default :status => 'active'
end
Here's a solution I've used that I was a little surprised hasn't been added yet.
There are two parts to it. First part is setting the default in the actual migration, and the second part is adding a validation in the model ensuring that the presence is true.
add_column :teams, :new_team_signature, :string, default: 'Welcome to the Team'
So you'll see here that the default is already set. Now in the validation you want to ensure that there is always a value for the string, so just do
validates :new_team_signature, presence: true
What this will do is set the default value for you. (for me I have "Welcome to the Team"), and then it will go one step further an ensure that there always is a value present for that object.
Hope that helps!
# db/schema.rb
create_table :store_listings, force: true do |t|
t.string :my_string, default: "original default"
end
StoreListing.new.my_string # => "original default"
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
attribute :my_string, :string, default: "new default"
end
StoreListing.new.my_string # => "new default"
class Product < ActiveRecord::Base
attribute :my_default_proc, :datetime, default: -> { Time.now }
end
Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600
sleep 1
Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600
I had a similar challenge when working on a Rails 6 application.
Here's how I solved it:
I have a Users table and a Roles table. The Users table belongs to the Roles table. I also have an Admin and Student Models that inherit from the Users table.
It then required that I set a default value for the role whenever a user is created, say admin role that has an id = 1 or student role that has an id = 2.
class User::Admin < User
before_save :default_values
def default_values
# set role_id to '1' except if role_id is not empty
return self.role_id = '1' unless role_id.nil?
end
end
This means that before an admin user is created/saved in the database the role_id is set to a default of 1 if it is not empty.
return self.role_id = '1' unless role_id.nil?
is the same as:
return self.role_id = '1' unless self.role_id.nil?
and the same as:
self.role_id = '1' if role_id.nil?
but the first one is cleaner and more precise.
That's all.
I hope this helps
Been using this for a while.
# post.rb
class Post < ApplicationRecord
attribute :country, :string, default: 'ID'
end
use default_scope in rails 3
api doc
ActiveRecord obscures the difference between defaulting defined in the database (schema) and defaulting done in the application (model). During initialization, it parses the database schema and notes any default values specified there. Later, when creating objects, it assigns those schema-specified default values without touching the database.
discussion
From the api docs http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
Use the before_validation method in your model, it gives you the options of creating specific initialisation for create and update calls
e.g. in this example (again code taken from the api docs example) the number field is initialised for a credit card. You can easily adapt this to set whatever values you want
class CreditCard < ActiveRecord::Base
# Strip everything but digits, so the user can specify "555 234 34" or
# "5552-3434" or both will mean "55523434"
before_validation(:on => :create) do
self.number = number.gsub(%r[^0-9]/, "") if attribute_present?("number")
end
end
class Subscription < ActiveRecord::Base
before_create :record_signup
private
def record_signup
self.signed_up_on = Date.today
end
end
class Firm < ActiveRecord::Base
# Destroys the associated clients and people when the firm is destroyed
before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" }
before_destroy { |record| Client.destroy_all "client_of = #{record.id}" }
end
Surprised that his has not been suggested here