rails database constraint for self referential association - ruby-on-rails

I'm building a rails website, which involves a directed friendship relation. I know in model level, it is a self referential association. And there are methods like has_and_belongs_to for that association.
My question is: how can I set up the database level constraints for this relation. I guess the migration would be something like this, which uses foreign keys to guarantee the referential integrity:
class CreateFriendships < ActiveRecord::Migration
def change
create_table :friendships do |t|
t.belongs_to :user, null: false, foreign_key: true
t.belongs_to :user, null: false, foreign_key: true
t.integer :accepted, null: false, default: 0
end
end
But when I run rake db:migrate, it has error:
PG::DuplicateObject: ERROR: constraint "fk_friendships_user_id" for relation "friendships" already exists
As a matter of fact, I'm not even sure whether it is necessary for me to set up the database constraint in this case, since I've seen some people's implementation of friendship relation has no database constraint like this:
create_table :friendships do |t|
t.integer :user_id
t.integer :friend_id
t.timestamps
end
According to Rails Guide
The Active Record way claims that intelligence belongs in your models, not in the database. As such, features such as triggers or constraints, which push some of that intelligence back into the database, are not heavily used.
I'm not sure whether in this case, the database constraints are heavily used.
So is it really necessary for me to set up database level constraints (using foreign keys) in this case? Or I just need to realize the constraints in model level? Thanks!!

You have declared user relation twice:
t.belongs_to :user, null: false, foreign_key: true
t.belongs_to :user, null: false, foreign_key: true
Seems that it should be like this:
t.belongs_to :user, null: false, foreign_key: true
t.belongs_to :friend, null: false, foreign_key: true
To answer your question: how can I set up the database level constraints for this relation?
Answer: Just like you already have.
Often developers go the rails way and set these constraints in model, but it's perfectly reasonable to set them up in database.
EDIT:
This will let you create a table with friend_id
class CreateFriendships < ActiveRecord::Migration
def change
create_table :friendships do |t|
t.belongs_to :user, null: false, foreign_key: true
t.integer :friend_id, null: false
t.integer :accepted, null: false, default: 0
end
add_foreign_key :friendships, :users, column: :friend_id
end
end

I think you're getting confused about the role of foreign_keys in your database architecture.
ActiveRecord is just a "coating" for SQL.
It's able to form queries etc which allow you to build associated objects, thus the most important thing you can do is associate those objects properly.
The way to do this - in SQL - is to use a foreign_key, which essentially shows the likes of ActiveRecord (and SQL if you use a join query) which data is associated:
Foreign keys are a standard element of relational database structures, which you probably know.
The reason why your data structure is failing is due to the fact you've replicated the user_id foreign key in your friendships table.
You'll want to refer to the following:
Rails: self join scheme with has_and_belongs_to_many?
This shows you that if you want to create a self referential join table (such as you're doing), you need to use the following:
#app/models/user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :friends,
class_name: "User",
join_table: :friendships,
foreign_key: :user_id,
association_foreign_key: :friend_user_id
end
#db/migrate/______.rb
class CreateFriendships < ActiveRecord::Migration
def self.up
create_table :friendships, id: false do |t|
t.integer :user_id
t.integer :friend_user_id
end
add_index(:friendships, [:user_id, :friend_user_id], :unique => true)
add_index(:friendships, [:friend_user_id, :user_id], :unique => true)
end
def self.down
remove_index(:friendships, [:friend_user_id, :user_id])
remove_index(:friendships, [:user_id, :friend_user_id])
drop_table :friendships
end
end
Notice how the references are for user_id and friend_user_id?
These are the two foreign keys you need to make sure your has_and_belongs_to_many is able to associate two objects of the same model.

Related

Rails model holds several references of its own class type in the same table

There seems to be no sequence of associations that work for this pattern:
Each user holds a reference to two OTHER users in the same table.
The User table contains two fields called user_a_id and user_b_id. I've been trying to get the following model associations to work:
class User < ApplicationRecord
has_one :user_a, class_name: "User", foreign_key: "user_a_id"
has_one :user_b, class_name: "User", foreign_key: "user_b_id"
end
The reference only needs to work in one direction. I simply want to use the model in the following way:
user.user_a.name
user.user_b.name
I won't ever need to access user_a.parent_user. I do not need that type of relationship.
The problem occurs when I reference self.user_a in the before_save callback. I basically get a recursive loop of SQL queries that eventually give me a stack too deep error.
Does anyone know what's going on here?
I just tried what you want to achieve. This is the migration for the users table:
create_table :users do |t|
t.string :name
t.references :user_a
t.references :user_b
t.timestamps
end
Notice how this generates the following schema.rb
create_table "users", force: :cascade do |t|
t.string "name"
t.integer "user_a_id"
t.integer "user_b_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_a_id"], name: "index_users_on_user_a_id"
t.index ["user_b_id"], name: "index_users_on_user_b_id"
end
In the User model I have
class User < ApplicationRecord
has_one :user_a, class_name: "User", foreign_key: "user_a_id"
has_one :user_b, class_name: "User", foreign_key: "user_b_id"
end
After migrating I can do in my rails console the following:
User.create(
name: "inception_user",
user_a: User.create(name: "Adam"),
user_b: User.create(name: "Berta")
)
inception_user = User.find_by_name "inception_user"
inception_user.user_a.name
=> "Adam"
inception_user.user_b.name
=> "Berta"
Everything works as expected with this setup. Please comment if you still have problems!
More information about self-joins: http://guides.rubyonrails.org/association_basics.html#self-joins
Finally found a solution. This may be an edge case BUT I needed to use belongs_to instead of has_one and I needed to remove the id from my table and the foreign_key. Also, because I was storing my references in a before_save callback, and they would have been empty during validation, I needed to add the parameter optional: true. This is the only association that allowed my program to reliably work:
class User < ApplicationRecord
belongs_to :user_a, class_name: "User", foreign_key: "user_a", optional: true
belongs_to :user_b, class_name: "User", foreign_key: "user_b", optional: true
end
Hopefully that helps somebody!

how do rails associations get set?

I always seem to have trouble with this concept. I get what associations allow you to do, I just never seem to be able to tell if the associations are set in an application.
For example, I generated a scaffold for line_items and before I ran my migration, I set the belongs_to and has_many methods in the correct models, and then ran my migration.
After running my migration, I look at my schema and I can't tell if there are any associations set. To me it does not seem like it because I don't see the schema setting any relationships.
Do the has_many and belongs_to methods actually set the association? Or are they there for developers reading the code to understand the relationship?
How would my schema look if the associations were set properly? Do I need to rollback my last migration and include the correct indexes?
create_table "carts", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "line_items", force: :cascade do |t|
t.integer "product_id"
t.integer "cart_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "products", force: :cascade do |t|
t.string "title"
t.text "description"
t.string "image_url"
t.decimal "price", precision: 8, scale: 2
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
it's very simple to tell if the association is set up properly or not through your schema. basically the model has belongs_to should have the corresponding table that contains the foreign_id. For example in the schema you post, it's easy to see that the lineItem belongs to 'Product' and Cart, because it has cart_id and product_id(two foreign id).
activerecord is not a black magic, all has_many and belongs_to do is just dynamically adds a method to the model class, which translate the query to raw sql for you and map the result to ruby object. however it is your responsibility to set up the database table correctly. Because after all, activerecord use SQL to query data following rails convention.
update
I think what you mean is the helper method in your migration file such as
t.belongs_to product, index: true, foreign_key: true
this line of code is also not a black magic, it is a helper method rails provide to make your life much easier. It is equivalent to do three simple things to create your database table.
create a foreign_id depends on your input, in the above example, it will be product_id. equivalent to t.integer product_id
add an index on the foreign_id, because usually you query out the associated data a lot by using the foreign_id, add an index will improve your efficiency. equivalent to add_index "xxx", ["product_id"], name: "index_xxxs_on_product_id"
at the end the foreign_key: true is going to set up an database constrains for your foreign_id, so that if there is still one row in your child table related to one row in your parent table, the row in your parent table will not be accidentally deleted. this is equivalent to add_foreign_key :xxxs, :products
so as a conclusion, using add_foreign_key :articles, :authors will not make anything look special in your schema, because ActiveRecord is just a translator to make your life easier when you deal with sql database, it can only do the thing sql database can do, not anything special. the idea of association in database is just saving a foreign_id in one table so that you can query the related data in another table by using the foreign_id.
The "association" is a rails concept that is implemented by has_many and belongs_to... so those lines are not just for documentation, they create the association.
You have product_id and cart_id in the line_items table and I assume
class LineItem
belongs_to :cart
belongs_to :product
...
end
class Product
has_many :line_items
...
end
class Cart
has_many :line_items
...
end
The has_many and belongs_to do set the associations for rails and means rails now knows that there are the associations...
my_line_item.cart
my_line_item.product
my_cart.line_items
my_product.line_items
If you didn't have the has_many and belongs_to it wouldn't work.
The columns card_id and product_id are needed for the associations to work as they're the way that records are linked and need to be present on the belongs_to side of the relationship. They don't have to be called what they're called but if you don't specifically use a different foreign_key name in the belongs_to and has_many then these field names are what will be expected by rails. And it's best to use the expected names... "Convention over configuration" is the preference.
If you did decide to call the foreign key vegetable_id instead of product_id that's fine, but then you'd define the association as
class LineItem
belongs_to :product, foreign_key: :vegetable_id
and
class Product
has_many :line_items, foreign_key: :vegetable_id
You can go an extra step and have...
class Cart
has_many :line_items
has_many :products, through: :line_items
...
end
This auto-magically gives you the ability to do..
my_cart.products
As rails knows how to build the SQL command to get all products for a cart via the line_items.
Because you need the foreign key in the belongs_to table, you would need to specify that foreign key when you create the table.
create_table :line_items do |t|
t.integer :product_id
There is a helper_method called references (also aliased as belongs_to) which basically does the same thing... creates the :product_id field...
create_table :line_items do |t|
t.belongs_to :product
or
create_table :line_items do |t|
t.references :product
The three versions above basically do the same thing... they create the integer column :product_id
You can also index on the :product_id field to improve retrieval performance, so you'll occastionally see index: true but this isn't essential.

Do I need a foreign key declaration in the individual table when creating join table?

I have a join table of "contents" and "roles" called content_roles and this is the join table.
class CreateContentRoles < ActiveRecord::Migration
def change
create_table :content_roles, :id => false do |t|
t.belongs_to :content, foreign_key: "content_id"
t.belongs_to :roles, foreign_key: "role_id"
end
add_index :content_roles, ["content_id", "roles_id"]
end
end
So in the individual roles and contents migration, do I need to have a foreign_key that refers back to the join table and/or the roles/contents? Sorry, I didn't explain this any better.
why cant you use references as shown below
class CreateContentRoles < ActiveRecord::Migration
def change
create_table :content_roles, :id => false do |t|
t.references :content, index: true, foreign_key: true
t.references :roles, index: true, foreign_key: true
t.timestamps null: false
end
end
end
The id columns in the content and roles tables act as foreign keys. Rails joins the content and content_roles tables using id in content table and content_id in content_roles table. So are the roles and content_roles tables.

Rails / postgresQL ActiveRecord - HOW and WHY change the primary key?

I have a Rails project with postgresql database.
Let's say I have three models - Student, Teacher and Schedule - that joins a student and teacher
Student Model - Instead of going with the student_id as my primary key, I want to change that to the even_cooler_unique_student_number that a school has for a student.
Teacher Model - typical & traditional.
Schedule Model - I want to associate a Schedule (think - just a math class for now) with one teacher and its students.
How do I do that at the database level and with the AR associations?
What does changing the primary_key do to the database? To my associations through ActiveRecord?
class CreateStudent < ActiveRecord::Migration
def change
create_table :students do |t|
t.integer :unique_cooler_student_id, null: false
t.string :first_name
t.string :last_name
t.timestamps null: false
end
end
end
class CreateTeacher < ActiveRecord::Migration
def change
create_table :teachers do |t|
t.string :first_name
t.string :last_name
t.string :department
t.timestamps null: false
end
end
end
class CreateSchedules < ActiveRecord::Migration
def change
create_table :schedules, id: false, force: true do |t|
t.belongs_to :students, :primary_key => 'unique_cooler_student_id'
t.belongs_to :teachers
t.string :something_else
t.timestamps null: false
end
end
end
class Student
self.primary_key = 'unique_cooler_student_id'
has_many :teachers, through: :classes
end
class Teacher
has_many :students, through: :classes
end
class Schedule
belongs_to :students
belongs_to :teachers
end
Changing the name of primary key usually does very little besides adding a false sense of security - which is only by obscurity.
You can however change the primary key from a auto-incrementing integer to a hash or some other sort of UUID. And there are many valid reasons to do so. This solely changes the method of generating primary keys.
You can even have separate external UUIDs which are used in url params for example. However this does not involve changing the primary key that ActiveRecord uses to join records:
Foo.joins(:bars).find_by(uuid: 'ABCD')
Of course ActiveRecord will let you crack out the tin-foil hat and use whatever primary keys you want - however you will need to specify the primary_key and probably also manually setup the foreign keys in your database to maintain referential integrity. So basically your losing every advantage of convention over configuration for no benefit.
You would have to do it like this:
class CreateSchedules < ActiveRecord::Migration
def change
create_table :schedules, id: false, force: true do |t|
t.references :students, foreign_key: false
t.belongs_to :teachers
t.string :something_else
t.timestamps null: false
end
end
end
class AddStudensIdContraintToSchedules < ActiveRecord::Migration
def change
add_foreign_key :schedules, :students, primary_key: "unique_cooler_student_id"
end
end
class Schedule
has_many :students, primary_key: 'unique_cooler_student_id'
end
This way AR uses WHERE students.unique_cooler_student_id = 2 in the join query.
The only reason you would ever really want to do this this is if you have to use a legacy database and cannot change the database schema.

Rails 4 Migration | Add Table with Reference

I am attempting to create a Collaboration table in my Rails 4 project, but I've run into an issue. I wish it to belong_to a single user, the collaborator.
I ran the following command to generate the model and the migration, which I've also copied below.
rails generate model Collaboration project:references collaborator:references accepted:boolean
Migration:
class CreateCollaborations < ActiveRecord::Migration
def change
create_table :collaborations do |t|
t.references :project, index: true, foreign_key: true
t.references :collaborator, index: true, foreign_key: true
t.boolean :accepted
t.timestamps null: false
end
end
end
Model:
class Collaboration < ActiveRecord::Base
belongs_to :project
belongs_to :collaborator, class_name: 'User'
end
I updated the Collaboration model to include , class_name: 'User' as shown above. Similarly, I updated the existing Strategy model to include a has_many :collaborations
class Project < ActiveRecord::Base
has_many :collaborations
end
When I run rake db:migrate, I get the following error reported.
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:
PG::UndefinedTable: ERROR: relation "collaborators" does not exist
I'm a bit puzzled as to wy this is happening. Any assistance would be greatly appreciated! Thank you. :)
EDIT:
Adding code for my User model as well.
class User < ActiveRecord::Base
authenticates_with_sorcery!
has_many :projects
has_many :collaborations
end
I edited out validations for fields such as password, email, etc to try to remove clutter.
This part of your migration:
t.references :collaborator, index: true, foreign_key: true
will try to create a foreign key inside the database so that the collaborator_id column of the collaborations table will be guaranteed to be NULL or contain the id of a column in the collaborators table. You can't create that FK until the collaborators table exists.
The error you're getting is:
relation "collaborators" does not exist
and that's just telling you that you don't have a collaborators table but you're trying to reference it.
You need a migration to create the collaborators table before you create your collaborations table.
In Rails 5, at least, you can use foreign_key: {to_table: ... }} as follows.
create_table :messages, id: :uuid do |t|
t.references :from_user, type: :uuid, index: true, null: false, foreign_key: {to_table: :users, on_delete: :cascade}
t.references :to_user, type: :uuid, references: :user, index: true, null: false, foreign_key: {to_table: :users, on_delete: :cascade}
t.text :body, null: false
t.timestamps
end
sorry for being late, but essentially it's all about convenience, remember that's the essence of rails. so; every reference should be targeting the table that should be in the plural (since a table holds many "objects") therefore, you must make the reference to plural so rails will generate a reference to a singular object. button line, your migration should look more like;
class CreateCollaborations < ActiveRecord::Migration
def change
create_table :collaborations do |t|
t.references :projects, index: true, foreign_key: true
t.references :collaborators, index: true, foreign_key: true
t.boolean :accepted
t.timestamps null: false
end
end
end
Now, if you follow the conventions, then you should have no problem with the rest, just keep in mind that belong_to is to a singular object and has_many is to a plural object.
PS: I would not use past reference for the column, like accepted
Happy Coding

Resources