Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

broken create, save, update, and destroy on Join record when using Postgres UUIDs in Rails

I'm creating uids using

create_table :users, { id: false } do |t|
    t.uuid :uid, default: 'uuid_generate_v4()'
    ... other columns

and setting self.primary_key = :uid in the models.

In general this works fine with ActiveRecord and I write has_many and belongs_to associations fine. However, when crossing a join table (i.e. has_many ... through:, I need to write custom SQL to get records.

I've figured out that I can in general do this by writing custom SQL, i.e. SELECT * FROM main_table JOIN join_table ON main_table.uid = cast(join_table.uid AS uuid) WHERE condition=true)

I've just recently realized that ActiveRecord's create, destroy, save and update dont work on the join model.

I have patched the four methods so they work, but it's too complex a sequence for my taste and probably unoptimal. Here are my patches:

def save(*args)
    # save sometimes works if it is called twice,
    # and sometimes works the first time but says theres an error
    super(*args) unless (super(*args) rescue true)
end

Sometimes, save issues a ROLLBACK the first time with no explanation. Then it works the second time. In other situations (I'm not sure why, possibly when updating), the first time it goes through successfully but if called a second time raises a TypeError. See here for another question about this error which doesn't have any answers for how to save a join when using uid instead of id. Here are my other (working) patches.

def create(*args)
    attrs = args[0]
    raise( ArgumentError, "invalid args to bucket list create" ) unless attrs.is_a?(Hash)
    bucket_list_photo = self.class.new(
        attrs
    )
    bucket_list_photo.save
    bucket_list_photo = BucketListPhoto.find_by(
        bucket_list_uid: bucket_list_photo.bucket_list_uid,
        photo_uid: bucket_list_photo.photo_uid
    )
    return bucket_list_photo
end

def update(*args)
    # similar bug to save
    attrs = args[0]
    raise( ArgumentError, "invalid args to bucket list update" ) unless attrs.is_a?(Hash)
    bucket_list_uid = self.bucket_list_uid
    photo_uid = self.photo_uid
    due_at = self.due_at
    self.destroy
    bucket_list_photo = self.class.new(
        {
            bucket_list_uid: bucket_list_uid,
            photo_uid: photo_uid,
            due_at: due_at
        }.merge(attrs)
    )
    bucket_list_photo.save
    bucket_list_photo = self.class.find_by(
        photo_uid: photo_uid,
        bucket_list_uid: bucket_list_uid
    )
    return bucket_list_photo # phew
end

def destroy
    # patching to fix an error on #destroy, #destroy_all etc.
    # the problem was apparently caused by custom primary keys (uids)
    # see https://stackoverflow.com/a/26029997/2981429
    # however a custom fix is implemented here
    deleted_uids = ActiveRecord::Base.connection.execute(
        "DELETE FROM bucket_list_photos WHERE uid='#{uid}' RETURNING uid"
    ).to_a.map { |record| record['uid'] }
    raise "BucketListPhoto not deleted" unless (
        (deleted_uids.length == 1) && (deleted_uids.first == uid)
    )
    ActiveRecord::Base.connection.query_cache.clear
    # since, the cache isnt updated when using ActiveRecord::Base.connection.execute,
    # reset the cache to ensure accurate values, i.e. counts and associations. 
end

I even ensured that self.primary_key = :uid in all my models.

I also tried replacing uid with id everywhere and verified that all the specs were passing (though I left in the patch). However, it still failed when I removed the patch (i.e. renaming the uid columns to id did not fix it).

EDIT

In response to some comments I've tried the activeuuid gem (where i got stuck on an error) and decided to totally switch over to ids. This is basically for simplicity's sake since I have pressure to launch this app ASAP.

Still, even with this fix I am required to patch save, create, and update. Actually the delete patch no longer works and I had to remove it (relying on the original). I would definitely like to avoid having to make these patches and I am keeping the bounty open for this reason.

like image 390
max pleaner Avatar asked Dec 07 '15 00:12

max pleaner


1 Answers

There are pros and cons to retaining both id and uuid. For JSON APIs that expose uuid, using Concerns would be a Rails-ish implementation.

app/models/user.rb

class User < ActiveRecord::Base

    include UserConcerns::Uuidable

end

app/models/concerns/user_concerns/uuidable.rb

module UserConcerns::Uuidable
    extend ActiveSupport::Concern

    included do
        before_save :ensure_uuid!
    end


    def ensure_uuid!
        self.uuid = generate_uuid if uuid.blank?
    end

    def generate_uuid
        # your uuid using uuid_generate_v4() here
    end

    def to_param
        uuid
    end
end

Above implementation leaves out the uuid generation but I think above answer has a link to that.

like image 118
tw airball Avatar answered Nov 16 '22 02:11

tw airball