9

I'm not sure if I should be updating records this way or if I'm missing something.

I have a table with 5 columns (not including timestamps and id) 3 of which are distinct, and 2 which will get updated. The 3 distinct which I will find or create by are room_id, date, and source. The other 2 are price and spots available (these change hourly, daily etc.)

My question is, should I first find or create the record, then update (or create) the price and spots or can I do it all at once? You can see the two ways I'm doing it now, and I'm not sure if its actually doing what I'm expecting.

Also, is there any downside to do a find_and_create_by like this?

Thanks

  private

  def self.parse_data(params,data)
    data.beds.each do |bed|
      room = Room.find_or_create_room(bed.title, params[:id])

      #find clones somehow
      #puts bed.nights.first.price
      bed.nights.each_with_index do |night,index|
        available = Available.find_or_create_by_room_id_and_bookdate_and_source(
          :room_id => room.id, 
          :bookdate => (params[:date].to_date)+index, 
          :source => data.class.to_s#,
          #:price => night.price
        )
        #available.price = night.price
        #available.spots = night.spots
        #available.save
      end

    end
holden
  • 13,471
  • 22
  • 98
  • 160
  • http://stackoverflow.com/questions/5160073/best-way-to-find-or-create-by-id-but-update-the-attributes-if-the-record-is-found/5160233#5160233 – fl00r Apr 07 '11 at 11:19
  • you may be able to simplify this with [upsert](https://github.com/seamusabshere/upsert) – Seamus Abshere May 16 '13 at 15:23

3 Answers3

24

Actually, there is a way without any hacking. Instead of find_or_create_by you can use find_or_initialize_by and set updated atributes with tap

Available.find_or_initialize_by_room_id_and_bookdate_and_source(
  room.id, 
  (params[:date].to_date)+index, 
  data.class.to_s#
).tap do |a|
  a.price = night.price
  a.spots = night.spots
end.save!

Initially this can seems cluttered, but it is doing exactly what you asked for. Find the record, instanciate it if not found and update atributes. this could be called "find_and_update_or_create_by", fortunatelly nobody did that. ;) Hope this help.

formigarafa
  • 377
  • 1
  • 11
Tian Chen
  • 493
  • 4
  • 15
15

Here is two approaches.

First you can extend Available with exact method you need:

def self.find_or_create_by_room_id_and_bookdate_and_source(room_id, bookdate, source, &block)
  obj = self.find_by_room_id_and_bookdate_and_source( room_id, bookdate, source ) || self.new(:room_id => room_id, :bookdate => bookdate, :source => source)
  yield obj
  obj.save
end

usage

Available.find_or_create_by_room_id_and_bookdate_and_source(room.id, (params[:date].to_date)+index, data.class.to_s) do |c|
  c.price = night.price
  c.spots = night.spots
end

This is awkward. So for being more flexible you can create update_or_create_by... method for ActiveRecord using method_missing magic:

class ActiveRecord::Base
  def self.method_missing(method_id, *args, &block)
    method_name = method_id.to_s
    if method_name =~ /^update_or_create_by_(.+)$/
      update_or_create($1, *args, &block)
    else
      super
    end
  end
  def self.update_or_create(search, *args, &block)
    parameters = search.split("_and_")
    params = Hash[ parameters.zip(args) ]
    obj = where(params).first || self.new(params)
    yield obj
    obj.save
    obj
  end
end

So now you can use it:

Available.update_or_create_by_id_and_source(20, "my_source") do |a|
  a.whatever = "coooool"
end
Community
  • 1
  • 1
fl00r
  • 82,987
  • 33
  • 217
  • 237
  • 1
    This is an awesome idea; but I get this error: NoMethodError (undefined method `find_by_id' for #): config/initializers/active_record_monkey_patch.rb:7:in `method_missing' – David Ryder Feb 23 '12 at 16:15
  • You shouldn't! :) looks like you haven't got `id` field for your class. Can you write a test? – fl00r Feb 23 '12 at 19:25
  • Beautiful solution! Though, I get the same error as @DavidRyder when using another dynamic method in the same file (or indirectly using a gem that uses a dynamic method). For instance, find_or_create_by_* gives a undefined method error. So I suspect the `super` part isn't working. – Magne Mar 09 '12 at 11:46
  • In @David Ryder's case it raises an error from super method (line 7) so it means that something wrong in AR method_missing. – fl00r Mar 09 '12 at 12:03
  • @fl00r That's the error I'm getting as well. Don't you get an error when trying to run one of the normal dynamic finders, like Available.find_or_create_by_room_id ? What about on another class, like Room.find_or_create_by_room_id ? (BTW: I'm inserting the ActiveRecord class code in the same file where I make all the calls..). – Magne Mar 09 '12 at 12:58
  • It seems dangerous to me to overload ActiveRecord's method_missing in this way, so I went with your first suggestion (extending the class). – Magne Mar 09 '12 at 12:58
0

I think the simplest way is using Ruby's tap method, like this:

def self.parse_data(params,data)
  data.beds.each do |bed|
    room = Room.find_or_create_room(bed.title, params[:id])

    bed.nights.each_with_index do |night,index|
      Available.find_or_initialize_by(room_id: room.id).tap do |available|
        available.bookdate = (params[:date].to_date) + index
        available.source = data.class.to_s
        available.price = night.price
        available.save
      end
    end
  end
end

find_or_initialize_by finds or intitializes a record, then returns it. We then tap into it, make our updates and save it to the database.

wnm
  • 1,349
  • 13
  • 12