One-on-One with ActiveRecord One-to-One
{Wednesday, February 1st, 2006}This is the first in a series of entries exploring ActiveRecord associations through tests. I start with the simplest theoretical form – a one-to-one.
The MySQL schema driving the tests follows.
create table parents (
id int not null auto_increment,
created_on datetime not null,
updated_on datetime not null,
lock_version int default 0,
name varchar(15) not null,
primary key (id)
) type=InnoDB;
create table children (
id int not null auto_increment,
parent_id int not null,
created_on datetime not null,
updated_on datetime not null,
lock_version int default 0,
name varchar(15) not null,
constraint fk_children_parent foreign key (parent_id) references parents(id),
primary key (id)
) type=InnoDB;
It’s a simple child -> parent relationship, the tables are named accordingly and conform to ActiveRecord’s pluralization demands. I’ve included the columns created_on, updated_on and lock_version mostly to exercise the features that automatically populate these values, but I occasionally put these columns to use in the tests.
The object representation of these tables, the Parent and Child classes, follow.
class Parent < ActiveRecord::Base
has_one :child, :dependent => true
has_one :youngest_child, :class_name => “Child”, :order => “created_on DESC”
end
class Child < ActiveRecord::Base
belongs_to :parent
belongs_to :mother, :class_name => “Parent”, :foreign_key => “parent_id”, :conditions => “name = ‘Mother’”
end
I’ve included simple uses of the class methods for forming the association (has_one and belongs_to) as well as the odder order clause filtering a parents one-to-many view of its children to a one-to-one and the conditions clause that filters a child’s view of a parent.
In total that’s a staggering 8 lines for a fair amount of features.
Let’s fire up some tests and see what’s happening under the hood.
As a sanity check let’s try and save a Parent.
def test_create_successfully_persists_parent
parent = Parent.create(:name => "parent1")
assert(!parent.new_record?)
assert_not_nil(parent.id)
assert_not_nil(Parent.find(parent.id))
end
Now how about a Child?
def test_create_without_parent_causes_foreign_key_constraint
assert_raise(ActiveRecord::StatementInvalid) { Child.create(:name => "child2") }
end
OK, so the child needs a parent. For those of you with Agile Web Development with Rails this goes against the grain of what’s recommended on page 230, but that approach would work if the FK to Order was nullable (although that makes no conceptual sense).
So lets try building (unpersisted) a Child and assigning that directly to a created (persisted) Parent.
def test_new_child_assigned_to_parents_child_persists_association
parent = Parent.create(:name => "parent3")
parent.child = Child.new(:name => "child3")
assert(!parent.child.new_record?)
assert_not_nil(parent.child.id)
found_parent = Parent.find(parent.id)
assert_equal(parent.child, found_parent.child)
end
I find it unusual that assigning an unpersisted Child to a persisted Parent immediately persists the Child. It feels like ‘persistence by touch’ - but as we’ll see shortly persisted objects don’t always persist other objects they touch.
Lets reverse things around - what happens if a created Child persists a new/built Parent?
def test_assigning_new_parent_to_created_child_does_not_persist_association
old_parent = Parent.create(:name => "parent4")
child = Child.create(:name => "child4", :parent => old_parent)
# To create a child we need an existing parent.
child.parent = Parent.new(:name => "new_parent4")
assert(child.parent.new_record?)
assert_nil(child.parent.id)
assert_equal(old_parent, Child.find(child.id).parent)
end
This is a rather unconventional way to attempt to establish the relationship, so ActiveRecord doesn’t support it. It is more logical that the parent exist before the child. In this way, ActiveRecord encourages you to work parent->child when navigating your objects.
Lets now look at the pseudo one-to-one mapping, the order clause.
def test_parent_allows_multiple_children_and_youngest_is_as_expected
parent = Parent.create(:name => "parent5")
oldest_child = Child.create(:name => "many_child_1", :parent => parent)
sleep(1)
youngest_child = Child.create(:name => "many_child_2", :parent => parent)
assert_equal(parent, oldest_child.parent)
assert_equal(parent, youngest_child.parent)
# That's not one-to-one!
assert_equal(youngest_child, parent.youngest_child)
end
So it’s not pure one-to-one, but virtual one-to-one relationships of this type can prove handy.
Now let’s try the conditions clause.
def test_conditions_clause_returns_parent_if_parent_matches_condition
parent = Parent.create(:name => "Mother")
child = Child.create(:name => "child6", :parent => parent)
assert_equal(parent, child.mother)
end
That ends our tour of one-to-one associations.
Before I sign-off, while writing these tests I stumbled across some unusual results usually brought about by newbie syntactic errors. Here’s an example:
def test_child_reload_is_forced_if_the_argument_is_not_a_boolean
parent = Parent.create(:name => "parent7")
parent.child = Child.new(:name => "child7")
child = parent.child
assert_equal(parent.child(child), parent.child(true))
# Both of these methods reload the child. Ouch.
end
OK, so occasionally it would be nice to use a static language ![]()
