[ruby-on-rails] Rails create or update magic?

I have a class called CachedObject that stores generic serialised objects indexed by 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?

This question is related to ruby-on-rails activerecord

The answer is


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)

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

The 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

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

The sequel gem adds an update_or_create method which seems to do what you're looking for.