Search code examples
ruby-on-railsrspechas-many-throughdestroy

Ruby on Rails has_many :through destroying more than it should


I am creating an app where there are three main models and relationships between them about which I need to track some properties:

schema.rb

  create_table "entities", force: true do |t|
    t.string   "name"
    t.string   "label"
    t.string   "img"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  add_index "entities", ["name"], name: "index_entities_on_name", unique: true, using: :btree

  create_table "entity_group_relationships", force: true do |t|
    t.integer  "entity_id"
    t.integer  "group_id"
    t.integer  "order"
    t.string   "label"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  add_index "entity_group_relationships", ["entity_id", "group_id"], name: "index_entity_group_relationships_on_entity_id_and_group_id", unique: true, using: :btree

  create_table "entity_property_relationships", force: true do |t|
    t.integer  "entity_id"
    t.integer  "property_id"
    t.integer  "group_id"
    t.integer  "order"
    t.string   "label"
    t.string   "value"
    t.integer  "visibility"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  create_table "group_property_relationships", force: true do |t|
    t.integer  "group_id"
    t.integer  "property_id"
    t.integer  "order"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  add_index "group_property_relationships", ["group_id", "property_id"], name: "index_group_property_relationships_on_group_id_and_property_id", unique: true, using: :btree
  add_index "group_property_relationships", ["group_id"], name: "index_group_property_relationships_on_group_id", using: :btree
  add_index "group_property_relationships", ["property_id"], name: "index_group_property_relationships_on_property_id", using: :btree

  create_table "groups", force: true do |t|
    t.string   "name"
    t.string   "default_label"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  add_index "groups", ["name"], name: "index_groups_on_name", unique: true, using: :btree

  create_table "properties", force: true do |t|
    t.string   "name"
    t.string   "units"
    t.string   "units_short"
    t.string   "default_label"
    t.string   "default_value"
    t.integer  "default_visibility"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  add_index "properties", ["name"], name: "index_properties_on_name", unique: true, using: :btree

Entity.rb

class Entity < ActiveRecord::Base
  has_many :entity_property_relationships
  has_many :entity_group_relationships
  has_many :properties, 
    through: :entity_property_relationships, 
    inverse_of: :entities
  has_many :groups, 
    through: :entity_group_relationships, 
    inverse_of: :entities
  validates :name, 
    presence: true
  #rest omitted
end

Property.rb

class Property < ActiveRecord::Base
  has_many :group_property_relationships
  has_many :entity_property_relationships
  has_many :entities, 
    through: :entity_property_relationships, 
    inverse_of: :properties
  has_many :groups, 
    through: :group_property_relationships, 
    inverse_of: :properties
  validates :name, presence: true
  #rest omitted
end

Group.rb

class Group < ActiveRecord::Base
  has_many :group_property_relationships
  has_many :entity_group_relationships
  has_many :entities, 
    through: :entity_group_relationships, 
    inverse_of: :groups
  has_many :properties, 
    through: :group_property_relationships, 
    inverse_of: :groups
  validates :name, 
    presence: true
  #rest omitted
end

entity_property_relationship.rb

class EntityPropertyRelationship < ActiveRecord::Base
  belongs_to :entity
  belongs_to :property
  validates :entity_id, 
    presence: true
  validates :property_id, 
    presence: true
  validates :order, 
    presence: :true
  #rest omitted
end

entity_group_relationship.rb

class EntityGroupRelationship < ActiveRecord::Base
  belongs_to :entity
  belongs_to :group 
  validates :entity_id, 
    presence: true
  validates :group_id, 
    presence: true
  validates :order, 
    presence: true
  #rest omitted
end

group_property_relationship.rb

class GroupPropertyRelationship < ActiveRecord::Base
  belongs_to :group
  belongs_to :property
  validates :group_id, 
    presence: true
  validates :property_id, 
    presence: true
  validates :order, 
    presence: true
  #rest omitted
end

The Problem

The behavior that is happening that I am trying to prevent is when a property is deleted, all EntityPropertyRelationships that share the same Entity and Group are also deleted. This isn't the case for GroupPropertyRelationships, nor for EntityGroupRelationships, and from everything I can see and understand, these relationships have been set up to behave identically.

I am testing this with rspec, here are some tests.

Test 1:

  it "should still own non-destroyed properties" do
    @entity = Entity.create!(name: "entity")

    @property1 = Property.create!(name: "property1")
    @property2 = Property.create!(name: "property2")

    @group = Group.create!(name: "group")
    @group.own!(@property1)
    @group.own!(@property2)

    @entity.own!(@group)

    @entity.utilizes?(@property1).should eq(true)
    @entity.utilizes?(@property2).should eq(true)

    @property1.destroy

    @entity.utilizes?(@property1).should eq(false)
    @entity.utilizes?(@property2).should eq(true)

    @group.owns?(@property1).should eq(false)
    @group.owns?(@property2).should eq(true)
  end

Test 1 is the test that fails. The entity should still utilize the property - utilizes?() checks that an EntityPropertyRelationship exists between the entity and the given property.

Test 2

  it "should still own non-destroyed groups" do
    @entity = Entity.create!(name: "entity")

    @group1 = Group.create!(name: "group1")
    @group2 = Group.create!(name: "group2")

    @entity.own!(@group1)
    @entity.own!(@group2)

    @entity.owns?(@group1).should eq(true)
    @entity.owns?(@group2).should eq(true)

    @group1.destroy

    @entity.owns?(@group1).should eq(false)
    @entity.owns?(@group2).should eq(true)
  end

Test 3

  it "should still utilize non-destroyed groups' properties" do
    @entity = Entity.create!(name: "entity")

    @group1 = Group.create!(name: "group1")
    @group2 = Group.create!(name: "group2")

    @property1 = Property.create!(name: "property1")
    @property2 = Property.create!(name: "property2")

    @group1.own!(@property1)
    @group2.own!(@property2)

    @entity.own!(@group1)
    @entity.own!(@group2)

    @entity.owns?(@group1).should eq(true)
    @entity.owns?(@group2).should eq(true)
    @entity.utilizes?(@property1).should eq(true)
    @entity.utilizes?(@property2).should eq(true)

    @group1.destroy

    @entity.owns?(@group1).should eq(false)
    @entity.owns?(@group2).should eq(true)
    @entity.utilizes?(@property1).should eq(false)
    @entity.utilizes?(@property2).should eq(true)
  end

Test 2 and Test 3 pass fine. I can not seem to pin down what about these relationships warrants a call to destroy each EntityPropertyRelationship, even where property_id isn't the id of the property being destroyed initially. Is there something I need to change about the relationships, or is there a way I could prevent this with destroy_callbacks?


Solution

  • Ouch! I just found the issue. I never thought it would be where it was, that's why it took so long to find. In my GroupPropertyRelationships, I had an after_destroy that would destroy the EntityPropertyRelationships for each of that Group's associated Entities. It should just be destroying the EntityPropertyRelationships where the group_id and the property_id are the same as the current GroupPropertyRelationship being destroyed. I replaced the after_destroy code for the GroupPropertyRelationship with:

    after_destroy do |r|
      EntityPropertyRelationship.destroy_all group_id: r.group_id, property_id: r.property_id
    end 
    

    That must have been a copy-paste error.