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?
7 Answers
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 = "" 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 = "" 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 = "" user.save end 7In 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: "").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 4Add 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) 2The magic you have been looking for has been added in Rails 6 Now you can upsert (update or insert). For single record use:
Model.upsert(column_name: value) For multiple records use upsert_all :
Model.upsert_all(column_name: value, unique_by: :column_name) Note:
- Both methods do not trigger Active Record callbacks or validations
- unique_by => PostgreSQL and SQLite only
Old question but throwing my solution into the ring for completeness. I needed this when I needed a specific find but a different create if it doesn't exist.
def self.find_by_or_create_with(args, attributes) # READ CAREFULLY! args for finding, attributes for creating! obj = self.find_or_initialize_by(args) return obj if obj.persisted? return obj if obj.update_attributes(attributes) end You can do it in one statement like this:
CachedObject.where(key: "the given key").first_or_create! do |cached| cached.attribute1 = 'attribute value' cached.attribute2 = 'attribute value' end 1The sequel gem adds an update_or_create method which seems to do what you're looking for.
2