Search code examples
ruby-on-railsassociationsmodel-associations

Rails - belongs_to has_many association error


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.

  1. Is a belongs_to / has_many association correct for this situation? Should this be a has_one?
  2. Should both migrations have a foreign key attribute?
  3. 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.
  4. 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?
  5. 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

Solution

    1. 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.

    1. 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.

    1. 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
    
    1. 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.

    1. 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.