MarkUs: Model Architecture
PR #7681 — Enforcing referential integrity through ActiveRecord association semantics.
The Problem
Deleting a Section record could orphan student records or leave dangling foreign key references to starter file groups. The existing implementation handled cleanup in the controller, scattering data integrity logic across the request-handling layer rather than encoding it in the domain model.
This violates a fundamental Rails principle: models own their invariants. A controller orchestrates the request lifecycle; the model enforces the rules that keep data consistent regardless of how the model is accessed (controller, background job, console, seed script).
Association Lifecycle Semantics
ActiveRecord’s dependent option on has_many controls what happens to associated records when the parent is destroyed. The options encode different integrity strategies:
| Option | Behavior | Use Case |
|---|---|---|
:destroy | Destroy each child (runs callbacks) | Owned resources with their own cleanup |
:delete_all | SQL DELETE without callbacks | Performance: skip callbacks for bulk deletion |
:nullify | Set foreign key to NULL | Soft dissociation (child survives) |
:restrict_with_error | Prevent parent deletion, add error | Protect children from accidental deletion |
:restrict_with_exception | Raise DeleteRestrictionError | Hard prevention (programming error) |
The Fix
class Section < ApplicationRecord
has_many :section_starter_file_groups, dependent: :destroy
has_many :students, dependent: :restrict_with_error
end:destroy for starter file groups. These are owned resources: a starter file group belongs to a section and has no meaning without it. When the section is deleted, its starter file groups should cascade-delete. Using :destroy (not :delete_all) ensures that any callbacks on SectionStarterFileGroup execute during deletion.
:restrict_with_error for students. Students should not be orphaned by section deletion. If an instructor attempts to delete a section that still has enrolled students, the deletion fails with a validation error. This forces the instructor to reassign students first, preventing data loss.
The controller-level cleanup code was removed entirely. The model now self-enforces its invariants through declarative association semantics.
Testing Deletion Behavior
Each association strategy requires explicit test coverage:
# Cascade deletion
test "destroying section destroys starter file groups" do
section = create(:section, :with_starter_files)
assert_difference("SectionStarterFileGroup.count", -1) do
section.destroy
end
end
# Restriction
test "cannot destroy section with enrolled students" do
section = create(:section, :with_students)
assert_not section.destroy
assert_includes section.errors[:base], "Cannot delete record"
endThese tests document the intended behavior and prevent regression if association options are accidentally modified.
Design Principle
Encode invariants in the model, not the controller. If a data integrity rule must hold regardless of how the model is accessed, it belongs in the model layer. Controller-level enforcement is fragile: it protects one access path but leaves others (background jobs, console access, other controllers) unguarded. ActiveRecord’s dependent options, validations, and callbacks provide a declarative vocabulary for expressing invariants that the framework enforces automatically.