rails overwrite nested attributes update - ruby-on-rails

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: 'new#gmail.com' } } }
this is the rails on how to update the record in the nested attribute
{ person: { name: 'Jack', addresses_attributes: { public_id: XXXXXXXX, content: 'new#gmail.com' } } }
I want to use the public_id to update records of addresses, but sadly this is not working any idea how to implement this?

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.

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.

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 } } )

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

Related

Rails model validation - create only one record with specific value

I've got Rating model to evaluate my features by user. When user click like or dislike button under one of my feature (e.g. data_chart) the new Rating record should be created. Example rating record: #<Rating:0x00007f8d839b2630 id: 7, customer_id: 1, actions: 'like', feature_uid: data_chart>. I want to prevent situation like below:
#<Rating:0x00007f8d839b2630 id: 7, customer_id: 1, actions: 'like', feature_uid: 'data_chart'>
#<Rating:0x00007f8d839b2630 id: 8, customer_id: 1, actions: 'dislike', feature_uid: 'data_chart'>
As you see there are two records for the same customer and for the same feature_uid. I want to prevent such scenarios since customers can evaluate each feature only once, without ability to change.
Code below:
def change
create_table :ratings do |t|
t.references :customer, null: false, foreign_key: true
t.string :action, null: false
t.string :feature_uid, null: false
t.timestamps
end
end
class Rating < ApplicationRecord
belongs_to :customer
validates :feature_uid, inclusion: { in: ['data_chart (...) some_other_features)'] }
validates :action, inclusion: { in: %w[like dislike] }
end
You can prevent the creation of new records for already existing customer_id and feature_uid pairs using the uniqueness validator.
In your particular case it will look like the following:
class Rating < ApplicationRecord
belongs_to :customer
# ...
validates :feature_uid, uniqueness: { scope: :customer_id }
end
Here is a link to the official docs.
It is also worth mentioning that the uniqueness validator works only on the model level, in other words, it does not create a uniqueness constraint in the database, so it may happen that two different simultaneous database connections create two records with the same values for columns that you intend to be unique.
In order to prevent such cases, you need to create a unique index on the pair of columns in your database. For instance, see the MySQL manual for more details about multiple column indexes or the PostgreSQL manual for examples of unique constraints that refer to a group of columns.
Here is a way how to create a unique index on multiple columns using Rails migrations.
Create a migration.
$ rails generate migration add_unique_index_for_customer_id_and_feature_uid_to_ratings
Modify it.
class AddUniqueIndexForCustomerIdAndFeatureUidToRatings < ActiveRecord::Migration[6.1]
def change
add_index(:ratings, [:customer_id, :feature_uid], unique: true)
end
end
Apply that migration.
$ rails db:migrate
Link to add_index docs.

Adding unique constraint through migration not working

I am a novice ruby programmer working on a rails api. Problem is api is in production and now I want to add unique constraint to one of the columns in a model. Currently Duplicate entries are allowed and i want to make that column unique.
So I added two fixtures with same name like this :
two:
name: MyString2
location:
status: 2
three:
name: MyString2
location:
status: 1
And then I tried to run a migration like this:
class AddUniqueToLocationColumnName < ActiveRecord::Migration
class User < ActiveRecord::Base
end
def self.up
remove_index :locations, column: :name
add_index :locations, :name, unique: true
end
def self.down
remove_index :locations, column: :name # remove unique index
add_index :locations, :name # adds just index, without unique
end
end
But Its showing error: "duplicates exists in database. Migration fails."
Same is the problem. Already I have duplicates in production table. And I want to add a unique constraint to column "name" in table "locations" . How can i make this column unique?
So you do not want any duplicate names for any two entries in your locations modal but the duplicates already exist? You will have to get rid of the duplicates by either:
deleting the data in the model and starting over from scratch
deleting the individual locations that have duplicate names
changing the names of the locations that are duplicates.
All of which can be done easily in rails/heroku console.
Also, you can add this code into your Locations Model:
class Location < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name
end

Can I change the ID to string when I work with Active Record?

It looks like the default :id is the integer, can I make it as a string?
Yes you can.
First run a migration:
create_table 'table' id: false, force: true do |t|
t.string 'id', null: false
end
specifying the type string for the id.
Then in your model:
class Table < ActiveRecord::Base
self.primary_key = :id
end
Which will basically explicitly indicate that the string id is the primary key of the object instance.
You should also consider looking up about uuid's in Rails.
Despite it being mainly a PostgreSQL piece of functionality, we've found it works well with MYSQL too:
You can set it up like this:
#app/models/post.rb
class Post < ActiveRecord::Base
before_create :set_uuid
private
def set_uuid
self.uuid = loop do
random_token = SecureRandom.hex(5)
break random_token unless self.class.exists? random_token
end
end
end
This can be accompanied - as pointed out by Cyzanfar - by replacing the id primary key with uuid. Rails 4 automatically supports uuid...
def change
create_column :posts, :uuid, :string
remove_column :posts, :id
rename_column :posts, :uuid, :id
execute "ALTER TABLE table ADD PRIMARY KEY (uuid);"
end
Some references:
Rails 4. Migrate table id to UUID
http://www.lshift.net/blog/2013/09/30/changing-the-primary-key-type-in-ruby-on-rails-models/
--
Because Rails 4 supports uuid out of the box, this should work for you.
As mentioned, we use uuid for some of our models (it allows us to maintain functionality whilst keeping records unique)

How to change a reference column to be polymorphic?

My model (Bar) already has a reference column, let's call it foo_id and now I need to change foo_id to fooable_id and make it polymorphic.
I figure I have two options:
Create new reference column fooable which is polymorphic and migrate the ID's from foo_id (What would be the best way to migrate these? Could I just do Bar.each { |b| b.fooable_id = b.foo_id }?
Rename foo_id to fooable_id and add polymorphic to fooable_id. How to add polymorpic to an existing column?
1. Change the name of foo_id to fooable_id by renaming it in a migration like:
rename_column :bars, :foo_id, :fooable_id
2. and add polymorphism to it by adding the required foo_type column in a migration:
add_column :bars, :fooable_type, :string
3. and in your model:
class Bar < ActiveRecord::Base
belongs_to :fooable,
polymorphic: true
end
4. Finally seed the type of you already associated type like:
Bar.update_all(fooable_type: 'Foo')
Read Define polymorphic ActiveRecord model association!
Update for Rails >= 4.2
TLDR
add new reference
copy reference ids
remove old reference
So the migration is:
def change
add_reference :bars, :fooable, polymorphic: true
reversible do |dir|
dir.up { Bar.update_all("fooable_id = foo_id, fooable_type='Foo'") }
dir.down { Bar.update_all('foo_id = fooable_id') }
end
remove_reference :bars, :foo, index: true, foreign_key: true
end
Background
Current Rails generates references with index and foreign_key, which is a good thing.
This means that the answer of #Christian Rolle is no longer valid as after renaming foo_id it leaves a foreign_key on bars.fooable_id referencing foo.id which is invalid for other fooables.
Luckily, also the migrations evolve, so undoable migrations for references do exist.
Instead of renaming the reference id, you need to create a new reference and remove the old one.
What's new is the need to migrate the ids from the old reference to the new one.
This could be done by a
Bar.find_each { |bar| bar.update fooable_id: bar.foo_id }
but this can be very slow when there are already many relations. Bar.update_all does it on database level, which is much faster.
Of course, you should be able to roll back the migration, so when using foreign_keys the complete migration is:
def change
add_reference :bars, :fooable, polymorphic: true
reversible do |dir|
dir.up { Bar.update_all("fooable_id = foo_id, fooable_type='Foo'") }
dir.down { Bar.update_all('foo_id = fooable_id') }
end
remove_reference :bars, :foo, index: true, foreign_key: true
end
Remember that during rollback, change is processed from bottom to top, so foo_id is created before the update_all and everything is fine.
One small change I would make is to the migration:
#db/migrate/latest.rb
class Latest
def change
rename_column :bars, :foo_id, :fooable_id
add_column :bars, :fooable_type, :string, after: :id, default: 'Foo'
end
end
This would eliminate the need to do a data migration.
Update: this will work on rails 3 and up. According to the question the original base class is implied to be Foo.
In reference to your question specifically, here's what I'd do:
All the data in your Bar model is going to be stored in reference to the Bar model. This means that if you change the foo_id attribute in your model, you'll be able to just populate the bar_type attribute you need to add (as they'll all be able to reference the same model)
The way to do this is as follows:
Create migration for foo_id > fooable_id
Insert a fooable_type column
In rails console, loop through all existing records of Bar, filling the fooable_type column
First things first:
$ rails g migration ChangeFooID
#db/migrate/latest.rb
class Latest
def change
rename_column :bars, :foo_id, :fooable_id
add_column :bars, :fooable_type, :string, after: :id
end
end
This will create the various columns for you. Then you just need to be able to cycle through the records & change the type column:
rails c
Bar.find_each do |bar|
bar.update(barable_type: "Foo")
end
This will allow you to change the type of your columns, giving you the ability to associate all the current records with the respective records.
Polymorphism
You'll be able to use the Rails docs as a reference as to how to associate your models:
#app/models/foo.rb
class Foo < ActiveRecord::Base
has_many :bars, as: :barable
end
#app/models/bar.rb
class Bar < ActiveRecord::Base
belongs_to :foo, polymorphic: true
end

Ruby on Rails, create with through control

I'm in the following situation, taking over an existing website, I have model User which has many devices like that:
has_many :devices, :through => :credits
When I create a device it creates a credit, but some of the attributes of the credits are null. I'd like to know if there's a way to control the creation of this credit and make sure nothing is null in the credit created for the database.
Thanks in advance
Recommended:
Use default values in your credits database table. You can use a database migration to do this.
class AddDefaultValuesToCredits < ActiveRecord::Migration
def change
change_column :credits, :value1, :boolean, default: false
change_column :credits, :value2, :string, default: 'words'
# change other columns
end
end
If no explicit value is specified for value1 or value2, they'll default to false and 'words', respectively.
Alternative: You can also set default values in your Credit model.
class Credit < ActiveRecord::Base
after_initialize :set_values, unless: persisted?
# other model code
def set_values
if self.new_record?
self.value1 = false if self.value.nil? # initial boolean value
self.value2 ||= 'words' # initial string value
# other values
end
end

Resources