ORM relations: $has_one and $belongs_to

975 views
Skip to first unread message

Ingo Schommer

unread,
Mar 28, 2009, 10:13:48 PM3/28/09
to silverst...@googlegroups.com
I've recently come across a situation where developers wanted a two-sided has_one relationship, meaning Class1 has_one Class2 and Class2 has_one Class1.
Sure you could normalize this into columns on the same table (or $db properties on a class),
but it might make sense to separate into different models depending on your other datamodel relationships, performance requirements etc.

This is currently not supported very well in our ORM - you basically need to create both $has_one relationships which don't know about each other, hence you get redundant foreign keys on both sides, and no referential integrity on an ORM level.

Do we want a $belongs_to relationship (similiar to $belongs_many_many) for the "other side" of a $has_one? We could use this relationship to do a dynamic lookup for the foreignkey without creating it on both sides, as well as to scaffold appropriate interfaces (dropdown).
The $has_one<->$belongs_to also makes it clear which table would accept responsibility for the foreignkey (as opposed to $has_one<->$has_one).

Consequently, the "other side" of a $has_many relationship would be expressed as a $belongs_to instead of a $has_one as well under the new model, right?
We should validate this connection either way, I often make the mistake of just defining the $has_many without its counterpart, which goes unnoticed by the ORM.

And somehow related to this, the rails way of doing "through" relationships:
http://wiki.rubyonrails.org/howtos/db-relationships/basic?s[]=belongs#has_many_through_hmt
We would currently do the same thing with $many_many_extraFields, although it might be worth considering to deprecate this (clumsy) practice and have many_many optionally expressed as a proper ORM object?

Thoughts?

-------
Ingo Schommer | Senior Developer
SilverStripe

Skype: chillu23

Michael Gall

unread,
Mar 28, 2009, 11:02:58 PM3/28/09
to silverst...@googlegroups.com
Yes. Yes. Yes.
--
Checkout my new website: http://myachinghead.net
http://wakeless.net

Mark Rickerby

unread,
Mar 28, 2009, 11:23:03 PM3/28/09
to silverst...@googlegroups.com
You don't need to be explicit about the "through" relationship a lot
of the time... One thing I do a lot is create "join models", where the
relationship actually becomes a fully fledged model itself. This is
really useful when the relationship implies behavior, and you can
generally get it working with has_one/has_many on both ends.

A good example is cinemas screening movies - clearly a many_many
relationship in theory, but it won't actually work modelled as
many_many in most ORM frameworks.

The trick here is to expand the relationship into an explicit SessionTime model:

Movie -> has_many -> SessionTime
Cinema -> has_many -> SessionTime
SessionTime -> has_one -> Movie
SessionTime -> has_one -> Cinema

Then the SesstionTime can also have a datetime property and other data
which relates to the presentation of a movie at a cinema.

This pattern pops up all over the place, and is surprisingly not
documented or discussed very often.

It would be good to know an explicit use case for "through" that
cannot be solved with either a relationship model, or a normal
many_many connection.

> We should validate this connection either way, I often make the mistake of just defining the $has_many without its
> counterpart, which goes unnoticed by the ORM.

Yes, I make similar mistakes all the time. It would be good to have
some kind of integrity check on dev/build.

Sam Minnee

unread,
Mar 29, 2009, 5:12:00 AM3/29/09
to SilverStripe Development
One of the things that I would like to allow for is the chaining of
many-item relations. In other words, calling a relationship method on
a DataObjectSet instead of just a DataObject.

One example use of this would be getting grandchildren:
$page->Children()->Children()

However, it would also provide an easy way of querying the join models
that you describe, Mark:
$movie->Sessions()->Theatre()

I've been working on a rewrite of the ORM that I really need to post
to the list shortly. Although I haven't implemented this specific
feature yet, the rewrite will make it more feasible.

Hamish Campbell

unread,
Mar 29, 2009, 9:49:06 PM3/29/09
to SilverStripe Development
While you're at it, can we make sure that all DataObject requests for
objects that don't exist return empty objects. You can't chain much of
anything at the moment, because you have to keep testing for an
object.

For example, this has always bothered me:

$ds = DataObject::get("SomeObjectType", "1 = 0");
$ds->exists() // fails because $ds is not an object.

Particularly for methods that are exposed to the view, you should be
able to do this:

function getSomeDeepProperty() {
return DataObject::get_one("SomeObject", "1 = 0")->SomeRelation()-
>SomeProperty; // returns false
}

rather than having to do:

function getSomeDeepProperty() {
$do = DataObject::get_one("SomeObject", "1 = 0");
if($do && $do->SomeRelation()) {
return $do->SomeRelation()->SomeProperty;
} else {
return false;
> > some kind of integrity check on dev/build.- Hide quoted text -
>
> - Show quoted text -

Hamish Campbell

unread,
Mar 29, 2009, 9:51:58 PM3/29/09
to SilverStripe Development
...of course, this change would require lots of rewrites, since all
those if($DataSet) would have to be changed to if($DataSet->exists) or
if($DataSet->count())...
> > - Show quoted text -- Hide quoted text -

Sam Minnee

unread,
Mar 29, 2009, 9:54:44 PM3/29/09
to SilverStripe Development
I don't understand how this would be anything more than renaming
has_one to belongs_to. They're functionally equivalent, and so it
seems like a needless change.

It seems like what we really need is a way of saying which of the
has_one relationships the given has_many is supposed to connect to.

Something like this:

class SiteTree {

static $has_one = array(
"Parent" => "SiteTree",
"FollowFrom" => "SiteTree"
);

static $has_many = array(
"Children" => "SiteTree/Parent",
"FollowOnPages" => "SiteTree/FollowFrom"
);

}

Alternatively, we could automatically create a has_many relationship
for every has_one. If we had this:

class SiteTree {
static $has_one = array(
"Author" => "Member",
);
}

We could automatically generate a relationship such as this:

$member->AuthorOfSiteTree()

But, any automatically generated name is going to be clumsy...
$siteTree->ParentOfSiteTree() instead of $siteTree->Children().
Frankly I'd rather provide an extra line of code to give it a nice
name.

Perhaps you could create a new item, $one_to_many, that had a clear
way of giving a separate name to each half of the relation.

static $one_to_many = array(
"Parent/Children" => "SiteTree",
"Author/PagesWritten" => "Member",
);

But this has the disadvantage that you can't open the Member object
and see which has_many relationships that it has. You have to read
every other data object to see whether they have a one_to_many
relationship pointing to Member. Tools could be developed to look
this information up for you (a data model explorer of some kind), but
that strikes me as a bit of a hack.

I'm all for developments in this area, but it sounds like we need to
get our thinking clearer. Let's get some potential code samples out
in the open.

Ingo Schommer

unread,
Mar 29, 2009, 10:33:57 PM3/29/09
to SilverStripe Development
> While you're at it, can we make sure that all DataObject requests for
objects that don't exist return empty objects. You can't chain much of
anything at the moment, because you have to keep testing for an
object.

Hamish, I think changing get_one() will do more harm than good.
I agree that it gets inconsistent when get_one() is used through
a has_one relationship, which returns false instead of an empty
object.
has_many and many_many return an empty and chainable ComponentSet.
Perhaps we need a get_one_or_empty() which you could call explicitly?
And special has_one getters like ParentOrEmpty() instead of Parent()?
Looks ugly, but would be a solution that doesn't break backwards
compatibility.

On Mar 29, 6:54 pm, Sam Minnee <sam.min...@gmail.com> wrote:
> I don't understand how this would be anything more than renaming
> has_one to belongs_to.  They're functionally equivalent, and so it
> seems like a needless change.

No they're not.

class Student {
$has_one = array('Profile'=>'Profile');
}
class Profile {
$has_one = array('Student'=>'Student');
}

Will create both StudentID and ProfileID foreign keys. I'd like to be
able
to create a has_one with just one foreign key, the other side of the
relationship
should work by looking up this foreign key on the other table.
belongs_to would be a solution for this. Sure, I could mimick this
behaviour
by setting one side to a has_many which happens to contain a single
item,
but that will mess up scaffolding interfaces, return a collection
rather
than a single item, and generally look weird.

From the rails documentation http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/belongs_to.
Also http://guides.rubyonrails.org/association_basics.html,
particularly "2.7 Choosing Between belongs_to and has_one"

>
> It seems like what we really need is a way of saying which of the
> has_one relationships the given has_many is supposed to connect to.

Yeah, thats an issue that won't be solved by belongs_to.
Sam, I think your example with SiteTree having relations to itself is
more confusing
than helpful for the discussion ;) Here's something a bit easier:

class SiteTree {
static $has_one = array(
'Author'=>'Member',
'Publisher'=>'Member'
);
}
class Member {
static $has_many = array(
'PublishedPages' => 'SiteTree'
'AuthoredPages' => 'SiteTree'
);
}

How do you know which pages belong to Author or Publisher?

This is becoming a bit wider discussion than I anticipated,
from what I understand we're talking about these mostly separate
problems:
1. has_one should have a belongs_to counterpart for "two sided
has_ones"
2. We don't have a way to identify which has_one a has_many belongs
to.
3. How can we "auto-detect" a has_one for a has_many?
4. DataObject::get_one() returns are inconsistent when used in
relations

Sam Minnee

unread,
Mar 29, 2009, 10:46:51 PM3/29/09
to SilverStripe Development
> 1. has_one should have a belongs_to counterpart for "two sided
> has_ones"

Aah, the penny drops. Sapphire currently lacks any support for 1-to-1
relationships, and that's what you're wanting to support.

$belongs_to becomes the 1-to-1 counterpart of $has_many.

Okay, sold.

> 2. We don't have a way to identify which has_one a has_many belongs
> to.

This would require a yet-to-be-determined syntax, although I made some
suggestions above.

> 3. How can we "auto-detect" a has_one for a has_many?

To tell you the truth, I'm in favour of requiring a few extra lines of
code rather than magically inferring the 2nd half of the relationship,
to improve the code readability.

> 4. DataObject::get_one() returns are inconsistent when used in
> relations

Yeah, returning an empty object when none exists introduces a whole
host of other bugs. To really resolve this properly we'd need to have
a separate object which represents "A query that may or may not return
a single object" as opposed to "An object".

Alternatively, we could have a separate method that returns an empty
object if none is found, designed for chaining.

Mark Rickerby

unread,
Mar 30, 2009, 12:24:34 AM3/30/09
to silverst...@googlegroups.com
Hamish,

One answer might be to define your own wrapper class and override the
get methods to return some kind of null class or an empty data object
that suits your purposes. Then use this class for queries throughout
your project, rather than the raw DataObject class.

Reply all
Reply to author
Forward
0 new messages