One-on-One with ActiveRecord One-to-One

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 ;-)

Leave a Reply