Rails - belongs_to has_many association error - ruby-on-rails

Goal: A user can create a User account (devise), and subsequently a Group. Each User only belongs_to one group and a group has_many Users.
After creating and running the Migrations - If I attempt to create a User i’m being presented with the following error: “1 error prohibited this user from being saved: Group must exist”.
Clearly the current setup wants a group_id to exist when creating a user.
Is a belongs_to / has_many association correct for this situation? Should this be a has_one?
Should both migrations have a foreign key attribute?
Is setting #group.user_id = current_user.id in GroupsController#create a suitable way to assign the creating user to the group? I tried to do this in the Groups model, by using a callback but I wasn’t able to access the current_user variable.
I would also like to enforce (at the database level) that a user can only belong to one group - Is this achieved using unique => true in the schema?
How can I enforce (at the database level) that a group must have a user?
.
class Group < ApplicationRecord
has_many :users
validates :users, presence: true
end
class User < ApplicationRecord
...
belongs_to :group
...
end
class GroupsController < ApplicationController
...
def create
#group = Group.new(group_params)
#group.user_id = current_user.id
...
end
...
private
...
def group_params
params.require(:group).permit(:name, :user_id)
end
...
end
class AddGroupReferenceToUser < ActiveRecord::Migration[5.0]
def change
add_reference :users, :group, foreign_key: true
end
end
class AddUserReferenceToGroup < ActiveRecord::Migration[5.0]
def change
add_reference :groups, :user, foreign_key: true
end
end
ActiveRecord::Schema.define(version: 20160903125553) do
create_table "groups", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id"
t.index ["user_id"], name: "index_groups_on_user_id"
end
create_table "users", force: :cascade do |t|
...
t.integer "group_id"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["group_id"], name: "index_users_on_group_id"
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
end

Is a belongs_to / has_many association correct for this situation? Should this be a has_one?
To setup a relationship where a user can only belong to a single group but groups have many users then you are on the right track.
class Group < ActiveRecord::Base
has_many :users
end
class User < ActiveRecord::Base
belongs_to :group
end
belongs_to places the foreign key column as users.group_id. Using has_one would place it on groups.user_id column which would only allow a one to one mapping - a group could only have a single user.
Should both migrations have a foreign key attribute?
No - only the the users table should contain a foreign key. The AddUserReferenceToGroup should be removed or you should write another migration which removes the groups.user_id column if you have pushed it to production.
Is setting #group.user_id = current_user.id in GroupsController#create a suitable way to assign the creating user
to the group?
No - since the group_id column is on the users column you need to update the users table - not groups.
if #group.save
current_user.update(group: #group)
end
I would also like to enforce (at the database level) that a user can only belong to one group - Is this achieved using unique => true in the schema?
No - since a row on the users table can only have one id in the groups_id column a user can only belong to one group anyways. Using unique => true would create a uniqueness index on the users.groups_id column which would only allow a single users to be associated with a group.
How can I enforce (at the database level) that a group must have a user?
This is not actually possible. To be able to associate a user with a group you must first insert the group into the database so that it is assigned a id.
Adding a validation on the software level would also create a "chicken vs egg" situation where a group cannot be valid since it has no users and a user cannot be associated with a group since it is not persisted.
You can however setup a constraint on the belongs_to end by declaring the foreign key column as NOT NULL.

Related

What side do you use Uniq in rails migration

In my rails app I have 2 models Profile and Skill.
Profile has_and_belongs_to_many Skill and can only have one time the same Skill.
Skill has_and_belongs_to_many Profile. If we respect the first relation, it should therefore not have more than once the same Profile.
When I create my join table I have two possibilities:
rails g migration CreateProfilesSkillsJoinTable profiles:uniq skills
or
rails g migration CreateProfilesSkillsJoinTable profiles skills:uniq
The first option will generate
class CreateProfilesSkillsJoinTable < ActiveRecord::Migration[5.1]
def change
create_join_table :profiles, :skills do |t|
t.index [:profile_id, :skill_id], unique: true
# t.index [:skill_id, :profile_id]
end
end
end
The second will generate
class CreateProfilesSkillsJoinTable < ActiveRecord::Migration[5.1]
def change
create_join_table :profiles, :skills do |t|
# t.index [:profile_id, :skill_id]
t.index [:skill_id, :profile_id], unique: true
end
end
end
You want to make the index unique :
add_index :something, [:profile_id, :skill_id], unique: true
First first rule is verified (you can get 1:2 only once). Note that even with an habtm, you'll tend to create your relation the same way (profile.skills << skill), you just need to ensure skill.profiles << profile does not creates unwanted relations

Is it advisable to use :foreign_key in my migrations rather than just adding user_id?

I am using rails 4.2, I just want to know if there would be any difference if I use the :foreign_key keyword in my migrations rather than just adding a user_id column to add relationship to my models ?
YES
The key difference is not on the application layer but on the database layer - foreign keys are used to make the database enforce referential integrity.
Lets look at an example:
class User < ActiveRecord::Base
has_many :things
end
class Thing < ActiveRecord::Base
belongs_to :user
end
If we declare things.user_id without a foreign key:
class CreateThings < ActiveRecord::Migration
def change
create_table :things do |t|
t.integer :user_id
t.timestamps null: false
end
end
end
ActiveRecord will happily allow us to orphan rows on the things table:
user = User.create(name: 'Max')
thing = user.things.create
user.destroy
thing.user.name # Boom! - 'undefined method :name for NilClass'
While if we had a foreign key the database would not allow us to destroy user since it leaves an orphaned record.
class CreateThings < ActiveRecord::Migration
def change
create_table :things do |t|
t.belongs_to :user, index: true, foreign_key: true
t.timestamps null: false
end
end
end
user = User.create(name: 'Max')
thing = user.things.create
user.destroy # DB says hell no
While you can simply regulate this with callbacks having the DB enforce referential integrity is usually a good idea.
# using a callback to remove associated records first
class User < ActiveRecord::Base
has_many :things, dependent: :destroy
end

RAILS-4 - has_and_belongs_to_many

I have two models, sellers and customers.
I want it to be a customer has a seller, many customer can have the same seller.
Ideally i want can do customer.seller = seller
With belongs_to association a seller can belongs to jsute one customer.
I use has_and_belongs_to_many association though a in my cas un customer can only have one seller.
# migration
create_table :sellers do |t|
t.string :name
t.timestamps null: false
end
create_table :customers do |t|
t.string :name
t.timestamps null: false
end
create_table :customers_sellers, id: false do |t|
t.belongs_to :customer, index: true
t.belongs_to :seller, index: true
end
# models/seller.rb
class Seller < ActiveRecord::Base
has_and_belongs_to_many :customers
end
# models/customer.rb
class Customer < ActiveRecord::Base
has_and_belongs_to_many :sellers
end
With that i can't do something like that:
customer = Customer.create(name: "John")
seller = Seller.create(name: "Dave")
customer.sellers = seller
I have an error
NoMethodError: undefined method `each' for #<Seller:0x0000000582fb18>
But i can:
customer.sellers<< seller
But if I change the name of the seller like that
Seller.first.name = "Bud"
I want it's to be also modified in my customer.sellers.name.
It's possible to make something like that?
Ok, so for a start, Seller.first.name = "Bud" does nothing to update the database, that name attribute is set on the Seller.first instance which is then lost because you've not assigned it to any variable.
So you'll need to change that to either:
Seller.first.update name: "Bud"
In order to update the DB with the new value, or (more likely) something like:
seller = Seller.first
seller.name = "Bud"
seller.save
That's step 1, actually getting that value saved into the database.
The second problem is that if you have already read customer.sellers from the database then your application already has the value for each of the names stored in memory, you need to reload at least that first record from the DB in order to get the new value:
customer.sellers.reload
And now (I'm assuming that Seller.first is also customer.sellers.first) customer.sellers.first.name will be "Bud"

Relation one to many with 2 foreigns keys of the same table Ruby on rails

I have a table called customers. These table has two addresses . One address of work and One direccion of house
Those 2 addresses belong to a table called addresses
I don't know how to relation those 2 tables
Migrations
class CreateCustomers < ActiveRecord::Migration
def change
create_table :customers do |t|
t.string :name
t.integer :address_id #Address of work
t.integer :address_id_1 #Address of home
t.timestamps
end
end
end
class CreateAdresses < ActiveRecord::Migration
def change
create_table :adresses do |t|
t.string :street
t.timestamps
end
end
end
I do not believe this is a good approach or database design. If you want to proceed this way and not get out of the rails convention just create two columns address_id and address_two_id
and in customer.rb
belongs_to :address, class_name: "Address"
belongs_to :address_two, class_name: "Address"
By default rails takes the name of the foreign key and stores it in a column called "name"+"_id"
The better way is two have a column customer_id in your Address model and create a relation in your customer class
customer.rb
has_many :addresses
And you can also validate that a customer has no more than two addresses by adding this validation to
address.rb
validate :validate_two_addresses
def validate_two_addresses
address_count = Address.where(customer_id: self.customer_id).count
errors.add(:base, "You cannot have more than 2 addresses.") unless address_count < 3
end

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