Search code examples
ruby-on-railsrubyruby-on-rails-5

rails overwrite nested attributes update


I have this model Person

class Person
 generate_public_uid generator: PublicUid::Generators::HexStringSecureRandom.new(32)

 has_many :addresses, as: :resource, dependent: :destroy
 accepts_nested_attributes_for :addresses, allow_destroy: true, update_only: true,
                                        reject_if: proc { |attrs| attrs[:content].blank? }
end

in my person table, I have this public_id that is automatic generated when a person is created. now the nested attribute in adding addresses is working fine. but the update is not the same as what nested attribute default does. my goal is to update the addresses using public_id

class Address
    generate_public_uid generator: PublicUid::Generators::HexStringSecureRandom.new(32)
    
    belongs_to :resource, polymorphic: true
end

this is my address model

 { person: { name: 'Jack', addresses_attributes: { id: 1, content: '[email protected]' } } }

this is the rails on how to update the record in the nested attribute

 { person: { name: 'Jack', addresses_attributes: { public_id: XXXXXXXX, content: '[email protected]' } } }

I want to use the public_id to update records of addresses, but sadly this is not working any idea how to implement this?


Solution

  • Since you say you are using your public_id as primary key I assume you don't mind dropping the current numbered id. The main advantage of not using an auto increment numbered key is that you don't publicly show record creation growth and order of records. Since you are using PostgreSQL, you could use a UUID is id which achieves the same goal as your current PublicUid::Generators::HexStringSecureRandom.new(32) (but does have a different format).

    accepts_nested_attributes_for uses the primary key (which is normally id). By using UUIDs as data type for your id columns, Rails will automatically use those.

    I've never used this functionality myself, so I'll be using this article as reference. This solution does not use the public_uid gem, so you can remove that from your Gemfile.

    Assuming you start with a fresh application, your first migration should be:

    bundle exec rails generate migration EnableExtensionPGCrypto
    

    Which should contain:

    def change
      enable_extension 'pgcrypto'
    end
    

    To enable UUIDs for all future tables create the following initializer:

    # config/initializers/generators.rb
    Rails.application.config.generators do |g|
      g.orm :active_record, primary_key_type: :uuid
    end
    

    With the above settings changes all created tables should use an UUID as id. Note that references to other tables should also use the UUID type, since that is the type of the primary key.

    You might only want to use UUIDs for some tables. In this case you don't need the initializer and explicitly pass the primary key type on table creation.

    def change
      create_table :people, id: :uuid, do |t|
        # explicitly set type uuid ^ if you don't use the initializer
        t.string :name, null: false
        t.timestamps
      end
    end
    

    If you are not starting with a fresh application things are more complex. Make sure you have a database backup when experimenting with this migration. Here is an example (untested):

    def up
      # update the primary key of a table
      rename_column :people, :id, :integer_id
      add_column :people, :id, :uuid, default: "gen_random_uuid()", null: false
      execute 'ALTER TABLE people DROP CONSTRAINT people_pkey'
      execute 'ALTER TABLE people ADD PRIMARY KEY (id)'
    
      # update all columns referencing the old id
      rename_column :addresses, :person_id, :person_integer_id
      add_reference :addresses, :people, type: :uuid, foreign_key: true, null: true # or false depending on requirements
      execute <<~SQL.squish
        UPDATE addresses
        SET person_id = (
          SELECT people.id
          FROM people
          WHERE people.integer_id = addresses.person_integer_id
        )
      SQL
    
      # Now remove the old columns. You might want to do this in a separate
      # migration to validate that all data is migrating correctly.
      remove_column :addresses, :person_integer_id
      remove_column :people, :integer_id
    end
    

    The above provides an example scenario, but should most likely be extended/altered to fit your scenario.


    I suggest to read the full article which explains some additional info.