Reputation: 83
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
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
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 id
s. 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
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
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