Atarang
Atarang

Reputation: 422

Rails before_create callback usage

I have fairly simple problem but I can not think of the simple solution. I have 3 models.

class User < ActiveRecord::Base
  has_many :memberships
end

class Membership < ActiveRecord::Base
  belongs_to :user
  belongs_to :school
end

class School < ActiveRecord::Base
  has_many :memberships
end  

A User can create a membership for school(s). "membernumber" is given by school. So far so good.

Except one use case, where a membership record gets created by school without user_id value in memberships table.

In this case a record gets created with "membernumber", "school_id" and empty "user_id" field.

What I want to do is, when a user tries to create his membership (using his "membernumber") with a school, First I want to see if "user_id", "membernumber". school_id combination is unique if found (I used validates_uniqueness_of....).

If record not found than check a record/association with "membernumber" and school exists and it's user_id field is empty. If that is the case then don't create a new membership but just update it's user_id field (with current_user.id) and return with a msg that a creation of new association is successful (as if its a new membership record).

How do I do this? Do I need to use any callbacks? How do I use callbacks in this case? Any help is appreciated.

Thanks,

Atarangp

Upvotes: 1

Views: 2340

Answers (3)

Atarang
Atarang

Reputation: 422

This is how I solved it.
In model membership, added a method membership_exists and called it from membership's create action.

class Membership < ActiveRecord::Base
  belongs_to :school
  belongs_to :user
  #validations, etc.

  def Membership.membership_exists(membernumber, school_id)
     membership = Membership.where(:membernumber =>  membernumber, :school_id => school_id, :user_id => nil).first
     if membership.nil?
        return false
     else
        membership.update_attribute :user_id, user_id
        return true
     end
  end
end

And in Membership controller (in create action)

  def create
    @membership = Membership.new(params[:membership])
    @membership.user_id = current_user.id

    @flag = Membership.membership_exists(@membership.membernumber,@membership.school_id)
    if @flag == true
      flash[:notice] = "Membership creation is successful"
      redirect_to @membership
      return
    end

    respond_to do |format|
      if @membership.save
        format.html { redirect_to @membership, notice: 'Membership was successfully created.' }
        format.json { render json: @membership, status: :created, location: @membership }
      else
        format.html { render action: "new" }
        format.json { render json: @membership.errors, status: :unprocessable_entity }
      end
    end
end

This works. But is it a good way to do it?

Thanks, Atarangp.

Upvotes: 0

rhetonik
rhetonik

Reputation: 1868

Only going by the models, one can make out:

  1. Membership contains references of both, User and School, and can hence act as a join table for a has_and_belongs_to_many or a has_many :through relationship between User and School
  2. If we consider the above, then any Membership record has a valid existence if and only if it is able to refer both, a User and a School. Somehow, I could not think of a use case where you would want to create a Membership record without a reference to the User. It would be great if you could elaborate a little on your use-case.

If the above works for you, then your models shall look something like:

class User < ActiveRecord::Base
  has_many :memberships
  has_many :schools, :through => :memberships
end

class Membership < ActiveRecord::Base
  belongs_to :user
  belongs_to :school
end

class School < ActiveRecord::Base
  has_many :memberships
  has_many :users, :through => :memberships
end

Now, you should be able to add users to a school and vice-versa.

user = User.first
user.schools << School.first

This would automatically create a record in Membership with appropriate references. And the user would start showing up in School.first.users. Further, you could add validations for uniqueness within the scope of a school_id i.e. validates_uniqueness_of :user_id, :scope => :school_id .

before_ after_ callbacks are good but in this case, you'll need some custom code for keeping things in sync. The above would eliminate any dependency on before_ after_ callbacks. Also, you wouldn't need to bother about any other validation checks on Membership model since the record would be created automatically only when a User record is added to a School record and vice-versa.

Upvotes: 1

Michael De Silva
Michael De Silva

Reputation: 3818

You should enforce uniqueness constraints in your migrations as well, however much DHH like's to preach the slathering of 'dumb persistance' across the board and only handle this at the application level.

The simplest approach is to enforce validates_presence_of :user_id in your Membership model. This will not allow join-records to be created if a user ID isn't present; your AR associative queries should inherently pass this ID on anyways.

For example, consider - User.last.memberships.build(@school) and in the context of your application, the User will be the current_user itself.

before_create and other before_ AR callbacks are definitely handy, but I'd always leave such an approach as a last resort. The reason is you'd intuitively expect your app to function in a certain manner whilst these before_ callbacks, can essentially mutate the information hitting the DB. Of course, unit tests will take care of such issues.

Upvotes: 3

Related Questions