Adding unique constraint through migration not working - ruby-on-rails

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

Related

Referencing a column on a table to a column on another table Ruby on Rails

I was reading another question on here regarding referencing columns from two separate tables but was a little confused if it addressed my issue. What's going on is I have two tables, Destination and Booking. The Destination table has a column for location_id, and the Booking has a column for location, and I am trying to reference location in Booking table from location_id column in Destination table.
Here is my table for Booking(migration)
class CreateBookings < ActiveRecord::Migration[6.1]
def change
create_table :bookings do |t|
t.string :name
t.string :start_date
t.string :end_date
t.string :email
t.integer :location
t.timestamps
end
end
end
and here is my table(Migration) for Destination
class CreateDestinations < ActiveRecord::Migration[6.1]
def change
create_table :destinations do |t|
t.string :address
t.string :city
t.string :state
t.string :zip
t.integer :location_id
t.timestamps
end
end
end
My Models are setup currently as
class Booking < ApplicationRecord
# belongs_to :reservation, optional: true
has_many :destinations, :class_name => 'Destination', :foreign_key=> 'location_id'
validates :name, :start_date, :end_date, :email, presence: true
end
and
class Destination < ApplicationRecord
has_many :bookings, :class_name => 'Booking', :foreign_key=> 'location'
end
Am I currently referencing the columns correctly, or is there something else I should be doing?
How you should write your migrations depends on the association between your models. Foreign keys go onto tables that have a belongs_to association.
Can a single Booking have multiple Destinations? If the answer is no, you need to change the association in your Booking model to belongs_to :destination and then put a :destination_id on your bookings table (you can give it a custom name like :location_id if you want but the convention is to use the model name).
If a single Booking can have multiple Destinations, and surely a single Destination can have multiple Bookings, then you have a many-to-many relationship. In that case you will not put foreign keys on the destinations table, nor the bookings table. Instead you will need a join table between them and that's where the foreign keys go.
Rails gives 2 different ways to declare many-to-many relationships. See https://guides.rubyonrails.org/association_basics.html#choosing-between-has-many-through-and-has-and-belongs-to-many.
If you want to use has_and_belongs_to_many, your models would look like this:
class Booking < ApplicationRecord
has_and_belongs_to_many :destinations
end
class Destination < ApplicationRecord
has_and_belongs_to_many :bookings
end
And the migration would look like this:
class CreateBookingsAndDestinations < ActiveRecord::Migration[6.0]
def change
create_table :bookings do |t|
# ...
end
create_table :destinations do |t|
# ...
end
create_table :bookings_destinations, id: false do |t|
t.belongs_to :booking
t.belongs_to :destination
end
end
end
Caveat: Based on your question I'm assuming you want a booking to have a destination. If you want a destination to many bookings and vise-versa, Sean's answer is great.
I think you're misunderstanding how foreign keys / associations work in databases.
It sounds like you want a column in the bookings table to "reference" a value column in the destinations table (or maybe the opposite), as in:
bookings.location -> destinations.location_id or maybe destinations.location_id -> bookings.location.
That's not typically what we mean by "reference" in a relational database. Instead, when you say that a table (for example, a 'comments' table) references another table (for example, a comments table references a user table), what we typically mean is that we're storing the primary key column of the referenced table (e.g. the user's id) in a column in the first table (e.g. comments.user_id --> users.id).
From an english language standpoint I expect that you want a booking to refer to a destination, so I'm going to assuming we want a the booking table to reference/refer to the destinations table, like this:
booking.location -> destinations.id
In Ruby on Rails, the convention is to name a column that stores an association with the same as the table it references, plus _id, like so the convention would be this:
booking.destination_id -> destinations.id
A common way to create this in a migration would be with:
add_reference :bookings, :destination
When adding a reference in a database you almost always want to index by that value (so that you can do Bookings.where(destination_id: #destination.id) and not kill your database). I am also a strong advocate for letting your database enforce referential integrity for you, so (if your database supports it) i'd recommend the following:
add_reference :destinations, :booking, index: true, foreign_key: true
This would prevent someone from deleting a destination that has a booking associated with it.

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: '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

Remove _id suffix from foreign key migration

I am trying to add a foreign key to via migration. It works as expected, but it automatically adds _id to the end (the column name I want to reference doesn't include _id). How can I make it reference the column name as I give it?
Here is the migration
class ChangeRefOnMemberPresentations < ActiveRecord::Migration[5.2]
def change
add_reference :member_presentations, 'employee_number', foreign_key: { to_table: :users }
end
end
Which results in both the reference column name and foreign key reference column being called employee_number_id in schema.rb
The following worked by defining everything manually, but seems messy. If there is a better migration answer, I'll be happy to accept.
def change
# column was added in another migration, but including for completness
add_column :member_presentations, :employee_number, :bigint
add_index :member_presentations, :employee_number, name: "index_member_presentations_on_employee_number"
add_foreign_key :member_presentations, :users, column: "employee_number"
end

ActiveRecord adding rating range in migration file

class AddRatingToBooks < ActiveRecord::Migration
def up
add_column :books, :rating, :integer
end
def down
remove_column :books, :rating
end
I have the following snippet of code in my db/migrate/, I'm trying to add ratings to my books table, where it would be in a range from 0-100, but I'm not sure how to add that here, all i could find was querying with ranges. I'm sure it's simple I'm just not there yet.
You don't need to specify the range of integer values in your migration file. The migration file is simply used to add the database column to store the rating. This is not the place to add validations.
You should use your Book model to specify a validation that ensures your ratings fall within a certain range. Something like this:
class Book < ActiveRecord::Base
validates :rating, :inclusion => { :in => 0..100 }
end
I would highly recommend reading the Rails guides on both migrations and validations.
Probably I'm too late with the answer. But it's possible to define validation on db level with Migration Validators project: https://github.com/vprokopchuk256/mv-core
As example, in your migration:
def change
change_table :books do |t|
t.integer :rating, inclusion: 0..100
end
end
and then in your model:
class Book < ActiveRecord::Base
enforce_migration_validations
end
As result your validation will be defined both in db ( as statement inside trigger or check constraint, depending on your db) and on your model
SQL ( PostgreSQL ):
=# insert into books(rating) values(10);
INSERT 0 1
=# insert into books(rating) values(200);
ERROR: new row for relation "books" violates check constraint "chk_mv_books_rating"
Rails console:
Book.new(title: 10).valid?
=> true
Book.new(title: 200).valid?
=> false

A migration to add unique constraint to a combination of columns

What I need is a migration to apply unique constraint to a combination of columns. i.e. for a people table, a combination of first_name, last_Name and Dob should be unique.
add_index :people, [:firstname, :lastname, :dob], unique: true
According to howmanyofme.com, "There are 46,427 people named John Smith" in the United States alone. That's about 127 years of days. As this is well over the average lifespan of a human being, this means that a DOB clash is mathematically certain.
All I'm saying is that that particular combination of unique fields could lead to extreme user/customer frustration in future.
Consider something that's actually unique, like a national identification number, if appropriate.
(I realise I'm very late to the party with this one, but it could help future readers.)
You may want to add a constraint without an index. This will depend on what database you're using. Below is sample migration code for Postgres. (tracking_number, carrier) is a list of the columns you want to use for the constraint.
class AddUniqeConstraintToShipments < ActiveRecord::Migration
def up
execute <<-SQL
alter table shipments
add constraint shipment_tracking_number unique (tracking_number, carrier);
SQL
end
def down
execute <<-SQL
alter table shipments
drop constraint if exists shipment_tracking_number;
SQL
end
end
There are different constraints you can add. Read the docs
For completeness sake, and to avoid confusion here are 3 ways of doing the same thing:
Adding a named unique constraint to a combination of columns in Rails 5.2+
Let's say we have Locations table that belongs to an advertiser and has column reference_code and you only want 1 reference code per advertiser. so you want to add a unique constraint to a combination of columns and name it.
Do:
rails g migration AddUniquenessConstraintToLocations
And make your migration look either something like this one liner:
class AddUniquenessConstraintToLocations < ActiveRecord::Migration[5.2]
def change
add_index :locations, [:reference_code, :advertiser_id], unique: true, name: 'uniq_reference_code_per_advertiser'
end
end
OR this block version.
class AddUniquenessConstraintToLocations < ActiveRecord::Migration[5.2]
def change
change_table :locations do |t|
t.index ['reference_code', 'advertiser_id'], name: 'uniq_reference_code_per_advertiser', unique: true
end
end
end
OR this raw SQL version
class AddUniquenessConstraintToLocations < ActiveRecord::Migration[5.2]
def change
execute <<-SQL
ALTER TABLE locations
ADD CONSTRAINT uniq_reference_code_per_advertiser UNIQUE (reference_code, advertiser_id);
SQL
end
end
Any of these will have the same result, check your schema.rb
Hi You may add unique index in your migration to the columns for example
add_index(:accounts, [:branch_id, :party_id], :unique => true)
or separate unique indexes for each column
In the typical example of a join table between users and posts:
create_table :users
create_table :posts
create_table :ownerships do |t|
t.belongs_to :user, foreign_key: true, null: false
t.belongs_to :post, foreign_key: true, null: false
end
add_index :ownerships, [:user_id, :post_id], unique: true
Trying to create two similar records will throw a database error (Postgres in my case):
ActiveRecord::RecordNotUnique: PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_ownerships_on_user_id_and_post_id"
DETAIL: Key (user_id, post_id)=(1, 1) already exists.
: INSERT INTO "ownerships" ("user_id", "post_id") VALUES ($1, $2) RETURNING "id"
e.g. doing that:
Ownership.create!(user_id: user_id, post_id: post_id)
Ownership.create!(user_id: user_id, post_id: post_id)
Fully runnable example: https://gist.github.com/Dorian/9d641ca78dad8eb64736173614d97ced
db/schema.rb generated: https://gist.github.com/Dorian/a8449287fa62b88463f48da986c1744a
If you are creating a new table just add unique: true
class CreatePosts < ActiveRecord::Migration[6.0]
def change
create_table :posts do |t|
t.string :title, unique: true
t.text :body
t.references :user, foreign_key: true
t.timestamps
end
add_index :posts, :user_id, unique: true
end
end

Resources