samhuri.net


By Sami J. Samhuri

ActiveRecord::Base.find_or_create and find_or_initialize

I've extended ActiveRecord with find_or_create(params) and find_or_initialize(params). Those are actually just wrappers around find_or_do(action, params) which does the heavy lifting.

They work exactly as you'd expect them to work with possibly one gotcha. If you pass in an id attribute then it will just find that record directly. If it fails it will try and find the record using the other params as it would have done normally.

Enough chat, here's the self-explanatory code:

1
2
3
4
# extend ActiveRecord::Base with find_or_create and find_or_initialize.
ActiveRecord::Base.class_eval do
  include ActiveRecordExtensions
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
module ActiveRecordExtensions
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def find_or_initialize(params)
      find_or_do('initialize', params)
    end

    def find_or_create(params)
      find_or_do('create', params)
    end

    private

    # Find a record that matches the attributes given in the +params+ hash, or do +action+
    # to retrieve a new object with the given parameters and return that.
    def find_or_do(action, params)
      # if an id is given just find the record directly
      self.find(params[:id])

    rescue ActiveRecord::RecordNotFound => e
      attrs = {}     # hash of attributes passed in params

      # search for valid attributes in params
      self.column_names.map(&:to_sym).each do |attrib|
        # skip unknown columns, and the id field
        next if params[attrib].nil? || attrib == :id

        attrs[attrib] = params[attrib]
      end

      # no valid params given, return nil
      return nil if attrs.empty?

      # call the appropriate ActiveRecord finder method
      self.send("find_or_#{action}_by_#{attrs.keys.join('_and_')}", *attrs.values)
    end
  end
end