Broke creation, saving, updating and destruction on Join records when using Postgres UUID in Rails

I create 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 models.

All in all, this works great with ActiveRecord, and I'm writing has_many and belongs_to . However, when crossing the connection table (i.e. has_many ... through: I need to write my own SQL to get the records.

I realized that in general I can do this by writing my own SQL, i.e. SELECT * FROM main_table JOIN join_table ON main_table.uid = cast(join_table.uid AS uuid) WHERE condition=true)

I recently realized that ActiveRecord create , destroy , save and update do not work on the merge model.

I fixed four methods to make them work, but this is too complex a sequence for my taste and probably not optimal. 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 for the first time without explanation. Then it works a second time. In other situations (I'm not sure why, perhaps when updating), the first time it succeeds, but if it is called a second time, a TypeError occurs. See here for another question about this error, in which there are no answers to the question of how to save the connection 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, ie counts and associations. end 

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

I also tried replacing uid with id everywhere and checked that all specifications passed (although I remained in the patch). However, it still failed when I deleted the patch (i.e. renamed the uid columns to id did not fix it).

EDIT

In response to some comments, I tried using activeuuid gem (where I was stuck with an error ) and decided to completely switch to ids. This is mainly for simplicity, since I have the pressure to launch this application as soon as possible.

However, even with this fix, I need to schedule save , create and update . Actually, the delete patch no longer works, and I had to delete it (relying on the original). I would definitely like to avoid the need to make these patches, and I keep the generosity open for this reason.

+7
ruby ruby-on-rails activerecord postgresql
source share
2 answers

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

application / models / user.rb

 class User < ActiveRecord::Base include UserConcerns::Uuidable end 

application / models / problems / 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 the implementation, uuid generation is not taken into account, but I think the answer above has a link to this.

+6
source share

I provided 1 solution for generating UUIDs, I knew that now you switched to id. Find the link for UUID

Yes, I agree that we cannot perform CRUD in the join table, why don’t you use active record relationships to perform CRUD operations.

Understanding is the convention by which Rails implements relationships using ActiveRecord. The book has many characters, and each character belongs to the book, therefore:

 class Book < ActiveRecordBase has_many :characters end class Character < ActiveRecordBase belongs_to :book end 

Rails now assumes that the character table will have a foreign key called book_id, which refers to the book table. To create a character in a book:

 @book = Book.new(:name=>"Book name") @character = @book.characters.build(:name=>"Character name") 

Now that @book is saved (assuming both @book and @character are valid), the string will be created in both books and character tables, with a character string linked via book_id.

To show that the character also belongs to the user, you can add this relation to the character model:

 class Character < ActiveRecordBase belongs_to :book belongs_to :user end 

Thus, Rails now expects the characters to also have a foreign key named user_id that points to the user table (which also needs the User model). To specify a user when creating a character:

 @book = Book.new(:name=>"Book name") @character = @book.characters.build(:name=>"Character name",:user=>current_user) 

You can also assign a foreign key by calling the appropriate method on the object:

 @character.user = current_user 

This all works because it follows the Rails conventions for model names and table names.

+3
source share

All Articles