How to Use Model Filters and Keep Your Controllers Clean
Rails is all about best practices. Doing things correctly has a vast effect on various aspects of your applications. One of the things that you really need to utilize, is the incredible power of model filters. They are a great way to keep your business logic into your models and minimize the code of your controllers, which is always a great thing to do.
Simply put, model filters are just callbacks that are triggered upon specific events. Think of a situation where we need to create a User group and upon creating the entry, we also need to create a membership entry for that specific user and specify that this user is the actual ‘leader’ of this group, since she was the one creating it. Let’s break down the events :
1) Create Group
2) Create GroupMembership
3) Set GroupMembership for that User, as the ‘leader’ of the group.
Notice that this is a ‘Group has_many users through group_memberships‘ association. The intermediate group_memberships association is simply defined as group_membership(id, group_id, user_id, rank). When our user creates the group, we also want to set her as the leader. Now, you are correctly thinking that it would be wise to use a Transaction, in order to make sure that if the group creation fails, there is no group membership created as well, in case of an exception or a race condition.
There is a Better Way Than Using a Transaction
Using a transaction to create the group, the group membership and set the leader as well, is not the correct way of doing things. The most important reason for that is that using a transaction inside your controllers is pretty much domain code that belongs to the models. The best way to go about it is to create an after_create filter in your Group model. The idea is that right after the group is created, the group membership is created as well.
This technique is also great because it also mimics a transaction. If there is an exception on creating the the group membership entry, the whole group creation process is going to be reverted. Therefore, we can make sure that everything is secure as much as it would be with using a transaction. Let’s now examine the controller method for creating a group :
def create @group = Group.new(params[:group]) @group.leader = current_user if @group.save flash[:success] = 'Group created' and redirect_to groups_url else render new_group_path end end
And let’s now take a look at the Group model :
class Group < ActiveRecord::Base after_create :assign_leader attr_accessor :leader has_many :group_memberships, :dependent => :destroy private def assign_leader self.group_memberships.create(:user => leader, :role => 'leader') end end
Notice that we also specify a leader virtual attribute. The reason is that our group model does not have a leader attribute. However, since we do want to pass this information in our group, in order to save it as our new group_membership leader, we just specify a getter/setter with attr_accessor and just pass that value. Notice that we pass our leader in the groups controller, as the current_user.