Search code examples
ruby-on-railsruby-on-rails-3associationsorphan

Prevent orphaned objects, with has_many through associations, in Rails 3


I have a model with "member", "group", "membership" and "user". The data is organized in a tree structure with groups that have members associated to them. Members without any group associations are considered orphaned and is of no use for the application.

When a user destroys a group there should not be left any orphaned members. In other words: a member should be destroyed if and only if the last group association is removed. Preferably this should happen in a single transaction, but the most important aspect is that orphaned object is not accumulated in the database over time.

Only members and groups that are associated with the user should be removed. Groups, members and memberships owned by other users should not be affected at all. (One might argue that a global cleanup method could be run anytime, but I want to isolate destructive operations to only affect the current users objects.)

My question: What is the most efficient and elegant way to implement this functionality in Rails 3? My current implementation, illustrated by the simplified model described here, does not remove a member from the database unless the user manually deletes it (or that the entire user and all his/her data is removed by cascade deletion.)

class User < ActiveRecord::Base
  has_many :groups, :foreign_key => 'owner_id', :dependent => :delete_all
  has_many :members, :foreign_key => 'owner_id', :dependent => :destroy
end

class Member < ActiveRecord::Base
  belongs_to :owner, :class_name => 'User'    
  has_many :memberships, :dependent => :destroy
  has_many :groups, :through => :memberships
end

class Membership < ActiveRecord::Base
  belongs_to :member
  belongs_to :group
end

class Group < ActiveRecord::Base
  belongs_to :owner, :class_name => 'User'
  belongs_to :parent, :class_name => 'Group'    
  has_many :groups, :foreign_key => 'parent_id', :dependent => :destroy
  has_many :memberships, :dependent => :destroy
  has_many :members, :through => :memberships
end

Solution

  • I solved this by adding a callback in the Membership class, which notifies a Member when the membership is destroyed. The member object then destroys itself if it no longer has any group associations.

    class Membership < ActiveRecord::Base
      belongs_to :member
      belongs_to :group
    
      after_destroy :notify_member
    
      def notify_member
        member.destroy_if_empty_groups
      end
    end
    
    class Member < ActiveRecord::Base
      belongs_to :owner, :class_name => 'User'    
      has_many :memberships, :dependent => :destroy
      has_many :groups, :through => :memberships
    
      def destroy_if_empty_groups
        if groups.count == 0
          self.destroy
        end
      end
    end