3

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!!

Leo Wang
  • 41
  • 4

2 Answers2

1

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
Magnuss
  • 2,270
  • 19
  • 21
  • Thanks for your answer. You resolve my confusion about whether it is necessary to add such a constraint in the database. But as for the migration code, the two foreign keys in Friendship table all refer to the User table. And I don't have a table named Friend. So your code does not work... I guess I should use some alias here, but I can not find the solution – Leo Wang Oct 13 '15 at 08:30
  • You can't have two columns with the same name. How would SQL know which `user_id` you are referring to in your queries? You don't need to have a `friend` model. This will simply create a `friend_id` association. Which you later designate to a specific associated model in rails. – Magnuss Oct 13 '15 at 08:50
  • But in my migration file, if I use `belongs_to :friend`, it will show the error `PG::UndefinedTable: ERROR: relation "friends" does not exist`. @Magnuss – Leo Wang Oct 13 '15 at 08:59
  • Thanks for your patience. But the code still does not work.... The error message is `ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: relation "friends" does not exist` I think `add_foreign_key` is trying to set reference to a table named 'friends', which does not exist. @Magnuss – Leo Wang Oct 13 '15 at 12:11
1

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:

enter image description here

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.

Community
  • 1
  • 1
Richard Peck
  • 76,116
  • 9
  • 93
  • 147
  • 1
    Thanks for your answer! But it seems that in the database level, the referential integrity is not ensured, since the code in the migration file do not set any foreign key. I'm not sure whether I understand it right? @ Rich Peck – Leo Wang Oct 13 '15 at 12:20
  • So, does that mean in this case, setting the constraints in model is enough? Setting constraints in the database is unnecessary? @Rich Peck – Leo Wang Oct 13 '15 at 12:38
  • Yep. We use MYSQL and we don't have constraints in the model – Richard Peck Oct 13 '15 at 12:43