Skip to main content

How do I do upserts and/or conditional saves?

Ruby • Asked by Morgan
I have an external API that I am parsing and saving to the database via a scheduled job and I am having trouble working out how to:

A: How to only save the parsed object if it has changed.
B. How to only save the related images if the post object has changed.

Luckily every object from the API comes with a mod_time field which is updated whenever something changes.

Below is a simplified version of what I have now which is currently just saving straight to the database.

module SomeApi
  class ImportPosts < Api

    def self.import
      response = client.get

      response.body['posts'].each do |post|
        post_body = {
          external_id: post['external_id'],
          mod_time: post['mod_time'],
          status: post['status'],
          external_post_id: post['post_id'],
          title: post['title'],
          body:  post['body']
        }

        # This works as it should with no dublicates
        @new_post = Post.where(external_id: post_body[:external_id]).first_or_create(post_body)

        # This will always run
        post['objects']['img'].each do |media|
          @new_post.images.build(
            external_id: media['external_id'],
            mod_time: media['mod_time'],
            url: media['url'],
            format: media['format']
          ).save
        end
      end
    end

    def client
      Api.client
    end
  end
end

My models are pretty standard:

Post
class Post < ApplicationRecord
  has_many :images

  accepts_nested_attributes_for :images
end

Image
class Image < ApplicationRecord
  belongs_to :post
end

As you can see, the Post will only save if the external_id does not exist but I think I need to first check if the mod_time is newer but I can only do that if the post object has been saved to compare.


The other issue which I have found surprisingly difficult, is how to only run the associated images if the parent post is ran? I tried wrapping the image code in a first_or_create block but it only saves the posts external_id.



Hey Morgan, 

Your first_or_create method allows you to check attributes on the found or created object, you just need to remove the post_body params and assign them later to do what you want:

@new_post = Post.where(external_id: post_body[:external_id]).first_or_create

if @new_post.mod_time.present? #update record
  # do whatever updating you want
else #it's a new record
  post['objects']['img'].each do |media|
    @new_post.images.build(
        external_id: media['external_id'],
    mod_time: media['mod_time'],
    url: media['url'],
    format: media['format']
    )
  end
  @new_post.assign_attributes(post_params)
  @new_post.save 
  # you may be able to get away with just doing @new_post.update(post_params) but I can't recall if it
  # will save your newly created association as well, so you'll want to tinker with it some.
end

This might take care of your last issue as well, but I'm not 100% sure I understand what the issue is there so play with this some and if it doesn't then post back. 

Thanks again Jacob!

I originally had the first_or_create as a block with the image code inside it which would save the post objects with only the external_id so I was missing the crucial @new_post.assign_attributes(post_body)



Excellent! Glad you were able to get it working! :)

Login or Create An Account to join the conversation.

Subscribe to the newsletter

Join 22,346+ developers who get early access to new screencasts, articles, guides, updates, and more.

    By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

    More of a social being? We're also on Twitter and YouTube.