# Locating existing records

In order to reduce redundancy in my app, I have added the method self.find_or_keep(object) to many of my models in order to find existing records with the same values, so I can reference the already existing ones instead of creating new records. Basically, this method checks most attributes for equality between two objects; id, created_at and updated_at are always excluded for obvious reasons.

class IPv4 < ActiveRecord::Base
# returns its param or an already existing record
def self.find_or_keep(ipv4)
# create new object in memory, if param is a Hash
ipv4 = IPv4.new(ipv4) if ipv4.is_a?(Hash)
end
end

class Service < ActiveRecord::Base
# returns its param or an already existing record
def self.find_or_keep(service)
# create new object in memory, if param is a Hash
service = Service.new(service) if service.is_a?(Hash)
return (entry = Service.find_by(protocol: service.protocol, port_from: service.port_from, port_to: service.port_to)) ? entry : service
end
end


As you can see, it's not very DRY. The problem here is that the method each time has to check for different parameters, so I have to rewrite the find_by every time I want to use the method in another class. What I have in mind for DRYing it up is something like this:

class IPv4 < ActiveRecord::Base
end

class Service < ActiveRecord::Base
implement_find_or_keep_with(:protocol, :port_from, :port_to)
end

class ActiveRecord::Base
# whatever is need
end


Is something like this possible? If so, how would I implement it?

Something like the following should do exactly what you want:

class ActiveRecord::Base
def self.find_or_keep_with(*args, &block)
define_method(:find_or_keep) do |*find_or_keep_args|
params = args.collect {|arg| [arg, object.send(arg)] }.to_h
if block_given?
yield(params, *find_or_keep_args)
else
object = find_or_keep_args.first
find_by(params) || object
end
end
end
end


I haven't tested the code, it may be broken, but the basic idea is there. The class method dynamically creates the find_or_create method based on the attribute (as symbols or strings) that you pass in.

Notice the block_given?? If you want to define a custom implementation of find_or_keep then you can do so by:

class Service < ActiveRecord::Base
find_or_keep_with(:protocol, :port_from, :port_to) do |params, object, *args|
# do you custom find_or_keep implementation here
end
end

• Thanks alot! Very helpful answer. I modified the code a little and now it works like a charm. – Brauser Aug 4 '15 at 16:32

Rails already has a built-in mechanism for doing this with the first_or_initialize method within ActiveRecord::Relation.

As an example, you can perform the following

Service.where(protocol: params[:protocol], port_from: params[:port_from], port_to: params[:port_to]).first_or_initialize


This will try and find a service with the given attributes OR initialize a new object with those attributes set if it can't find it.

Hope this helps.

• Thanks, didn't know about that method before. The thing is, I must use objects, that are already initialized. For example, for creating a Service-Object, there is one input for protocol and one input called portrange, which is an attr_accessor and is converted into port_from and port_to via overloading the portrange=(str) (for example "range 1 2" becomes port_from: 1, port_to: 2) method. That means, the object must be initialized before performing the query. – Brauser Aug 4 '15 at 13:23
• I don't understand, your find_or_keep method internally uses find_by which will require a separate :port_from and :port_to to perform the query - this isn't any different. – Bart Jedrocha Aug 4 '15 at 13:52
• But I can't make use of first_or_initialize, or am I being stupid? Since find_or_keep either expects an initialized Object of that class or a hash for initializing an Object. So, when the Object is initialized, the port_from and port_to are automatically set by the overloaded portrange=(str). Before it's initialized I do not know what port_from and port_to are, so I can't Service.where([anything with port_from or port_to]).first_or_initialize. And after it has been initialized I can't use first_or_initialize on the already initialized Object. – Brauser Aug 4 '15 at 14:01
• I see what you mean - yeah in that case you would either need to deconstruct port_range into its constituent parts before making the call, or wrap in a factory method like you're currently doing. – Bart Jedrocha Aug 4 '15 at 14:23