2
\$\begingroup\$

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)
    return (entry = IPv4.find_by(address: ipv4.address, subnetmask: ipv4.subnetmask)) ? entry : ipv4
  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
  implement_find_or_keep_with(:address, :subnetmask)
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?

\$\endgroup\$
0

2 Answers 2

1
\$\begingroup\$

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
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thanks alot! Very helpful answer. I modified the code a little and now it works like a charm. \$\endgroup\$
    – Brauser
    Aug 4, 2015 at 16:32
2
\$\begingroup\$

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.

\$\endgroup\$
4
  • \$\begingroup\$ 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. \$\endgroup\$
    – Brauser
    Aug 4, 2015 at 13:23
  • \$\begingroup\$ 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. \$\endgroup\$ Aug 4, 2015 at 13:52
  • \$\begingroup\$ 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. \$\endgroup\$
    – Brauser
    Aug 4, 2015 at 14:01
  • \$\begingroup\$ 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. \$\endgroup\$ Aug 4, 2015 at 14:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.