Rails create or update magic?
I have a class called CachedObject
that stores generic serialized objects indexed by a key. I want this class to implement a create_or_update
method. If an object is found it will update it, otherwise it will create a new one.
Is there a way to do this in Rails or do I have to write my own method?
Rails 6
Rails 6 added an upsert
and upsert_all
methods that deliver this functionality.
Model.upsert(column_name: value)
[upsert] It does not instantiate any models nor does it trigger Active Record callbacks or validations.
Rails 5, 4, and 3
Not if you are looking for an "upsert" (where the database executes an update or an insert statement in the same operation) type of statement. Out of the box, Rails and ActiveRecord have no such feature. You can use the upsert gem, however.
Otherwise, you can use: find_or_initialize_by
or find_or_create_by
, which offer similar functionality, albeit at the cost of an additional database hit, which, in most cases, is hardly an issue at all. So unless you have serious performance concerns, I would not use the gem.
For example, if no user is found with the name "Roger", a new user instance is instantiated with its name
set to "Roger".
user = User.where(name: "Roger").first_or_initialize
user.email = "[email protected]"
user.save
Alternatively, you can use find_or_initialize_by
.
user = User.find_or_initialize_by(name: "Roger")
In Rails 3.
user = User.find_or_initialize_by_name("Roger")
user.email = "[email protected]"
user.save
You can use a block, but the block only runs if the record is new.
User.where(name: "Roger").first_or_initialize do |user|
# this won't run if a user with name "Roger" is found
user.save
end
User.find_or_initialize_by(name: "Roger") do |user|
# this also won't run if a user with name "Roger" is found
user.save
end
If you want to use a block regardless of the record's persistence, use tap
on the result:
User.where(name: "Roger").first_or_initialize.tap do |user|
user.email = "[email protected]"
user.save
end
In Rails 4 you can add to a specific model:
def self.update_or_create(attributes)
assign_or_new(attributes).save
end
def self.assign_or_new(attributes)
obj = first || new
obj.assign_attributes(attributes)
obj
end
and use it like
User.where(email: "[email protected]").update_or_create(name: "Mr A Bbb")
Or if you'd prefer to add these methods to all models put in an initializer:
module ActiveRecordExtras
module Relation
extend ActiveSupport::Concern
module ClassMethods
def update_or_create(attributes)
assign_or_new(attributes).save
end
def update_or_create!(attributes)
assign_or_new(attributes).save!
end
def assign_or_new(attributes)
obj = first || new
obj.assign_attributes(attributes)
obj
end
end
end
end
ActiveRecord::Base.send :include, ActiveRecordExtras::Relation
Add this to your model:
def self.update_or_create_by(args, attributes)
obj = self.find_or_create_by(args)
obj.update(attributes)
return obj
end
With that, you can:
User.update_or_create_by({name: 'Joe'}, attributes)