ImpostorIsAsking
ImpostorIsAsking

Reputation: 83

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?

Upvotes: 0

Views: 1383

Answers (4)

3limin4t0r
3limin4t0r

Reputation: 21150

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.

Upvotes: 1

Clemens Kofler
Clemens Kofler

Reputation: 1968

Rails generally assumes that you have a single column named id that is the primary key. While it is possible to work around this, lots of tools in and around Rails assume this default – so you'll be giving yourself major headaches if you stray from this default assumption.

However, you're not forced to use integer ids. As someone else has already pointed out, you can change the type of the ID. In fact, you can supply any supported type by doing id: type, where type can e.g. be :string in your case. This should then work with most if not all of Rails' default features (including nested attributes) and also with most commonly used gems.

Upvotes: 2

MZaragoza
MZaragoza

Reputation: 10111

This is the way I have my nested-attributes

#app/models/person.rb
class Person < ApplicationRecord
  ...
  has_many :addresses, dependent: :destroy
  accepts_nested_attributes_for :addresses, reject_if: :all_blank, allow_destroy: true
  ...
end

my controller

#app/controllers/people_controller.rb
class PeopleController < ApplicationController
   ...
   def update
     @person = Person.find_by(id: params[:id])
     if @person.update(person_params)
       redirect_to person_path, notice: 'Person was successfully added'
     else
       render :edit, notice: 'There was an error'
     end
   end
   ...
   private
   def person_params
     params.require(:person).permit(
        ... # list of person fields
        addresses_attributes: [
          :id,
          :_destroy,
          ... # list of address fields 
        ]
     )
   end
   ...
end

I hope that this is able to help you.

Let me know if you need more help

Upvotes: 0

user14819812
user14819812

Reputation:

Because you still need an :id field in your params, unless you want to change your to_param directly in model. Try something like this:

person  = Person.first
address = person.address
person.update({ name: 'Jack', adddresses_attributes: { id: address.id, public_id: XXX, _destroy: true } } )

Upvotes: 0

Related Questions