OneToOne relationship? (was Re: Retrieving an object based upon a composed relationship)

0 views
Skip to first unread message

Brian Kotek

unread,
Sep 2, 2008, 8:15:50 AM9/2/08
to transf...@googlegroups.com
This brings up an interesting question, which may be worth a separate thread. It may have been answered already and I may even have heard the answer before, but is there a reason why Transfer doesn't support a "OneToOne" relationship? I know that such relationships are not all that common but I have run into them. They'd also have the benefit of being implicitly bi-directional, right?



On Sun, Aug 31, 2008 at 6:34 AM, James Allen <sling...@googlemail.com> wrote:

Hi Elliot,

The relationship between SearchOptions and User is really onetoone but
obviously we can't model this in Transfer which is why I have to use
manytoone. In this scenario User will always be unique.


Bob Silverberg

unread,
Sep 2, 2008, 9:24:38 AM9/2/08
to transf...@googlegroups.com
This has been discussed a few times before. I know that I have asked
this question at least twice, and the response has always been that
one2one represents an edge case, and that the resources required to
implement it could be used elsewhere to greater benefit. At least
that was the way I interpreted the response.

I didn't agree with this, not entirely anyway, as I have used one2one
on numerous occasions, but that was just my opinion. If anyone else
sees this as a valuable enhancement please speak up, and maybe it can
get added as an enhancement request.

Cheers,
Bob

--
Bob Silverberg
www.silverwareconsulting.com

Jared Rypka-Hauer

unread,
Sep 2, 2008, 9:33:46 AM9/2/08
to transf...@googlegroups.com
I think there's more to it than just the diversion of resources...

one2one is considered an edge case because it's not particularly good
DB design... if you have a one2one join, you should consider putting
the data together in the same table. Since supporting questionable DB
design isn't within Transfer's charter, it's a question of Mark's
time being spent on supporting something that shouldn't really be
necessary in the first place or working on any of the bajillions of
other enhancements or bug fixes that are essential.

Meanwhile, by means of a one2many on the 1:1 tables, you can achieve
what's essentially the same behavior (or at least similar) insofar as
getters and setters are concerned. Since a one2one is the same as a
one2many with the one minor difference that the joining table will
only ever have one related row, there's no particular reason not to
go ahead and use it. If you want to enforce the 1:1 nature of the
tables, just create a decorator, override the generated setFoo()
method to indicate your preferred behavior (throw error? overwrite
original record? backup record then overwrite?).

I guess that brings up yet another point against this... what's the
desired behavior when calling Bar.setFoo(Foo) on a 1:1 relationship?
There's too many viable options in this case to have consistent and
acceptable results. So we're back to using a one2many and overriding
the generated methods in a decorator. If you have a one2one
relationship, this is not just your only option but quite possibly
the most elegant option available.

J

Bob Silverberg

unread,
Sep 2, 2008, 10:52:57 AM9/2/08
to transf...@googlegroups.com
On Tue, Sep 2, 2008 at 9:33 AM, Jared Rypka-Hauer
<armcha...@gmail.com> wrote:
>
> I think there's more to it than just the diversion of resources...
>
> one2one is considered an edge case because it's not particularly good
> DB design... if you have a one2one join, you should consider putting
> the data together in the same table.

I have to disagree with this. One2one is a useful relationship when
attempting to implement supertypes and subtypes. Let's say I have a
User object, with properties like UserId, UserName and Password. I
also have different types of users. Administrators have a Department,
EmployeeCode, etc. Members have a JoinedDate, AffiliateCode, etc.

My belief is that optimal DB design would yield a User table, with the
common fields, an Adminstrator table, with Admin only fields, and a
Member table with member only fields. The PK of each table would be
UserId, thus creating a one2one relationship. The alternative that
you are suggesting would be to put all of the Admin only fields AND
all of the Member only fields into the User table. That would result
in multiple unused fields on each and every record in the table. It's
true that in today's world of cheap hardware having a denormalized
design like that isn't necesarily a bad thing, but I also think it's
not accurate to say that it's better design. I may be among the
minority but I still think that normalization is an admirable goal,
and although I might use a denormalized design like you are describing
to make things simpler, I would feel as if I were making a compromise
on design, not implementing a better design.

--
Bob Silverberg
www.silverwareconsulting.com

Brian Kotek

unread,
Sep 2, 2008, 12:16:39 PM9/2/08
to transf...@googlegroups.com
On Tue, Sep 2, 2008 at 10:52 AM, Bob Silverberg <bob.sil...@gmail.com> wrote:

I have to disagree with this.   One2one is a useful relationship when
attempting to implement supertypes and subtypes.  Let's say I have a
User object, with properties like UserId, UserName and Password.  I
also have different types of users.  Administrators have a Department,
EmployeeCode, etc.  Members have a JoinedDate, AffiliateCode, etc.


Yes, this is exactly where I ran into the use of this as well. There are definitely situations where you know you want only a one to one relationship between two tables due to limitations in translating an object model into a relational schema. There are also cases where you have extended information that you don't necessarily always need, but that nonetheless exist in a one to one relationship with some other entity.

On Tue, Sep 2, 2008 at 9:33 AM, Jared Rypka-Hauer <armcha...@gmail.com> wrote:

I think there's more to it than just the diversion of resources...

one2one is considered an edge case because it's not particularly good
DB design... if you have a one2one join, you should consider putting
the data together in the same table. Since supporting questionable DB
design isn't within Transfer's charter, it's a question of Mark's
time being spent on supporting something that shouldn't really be
necessary in the first place or working on any of the bajillions of
other enhancements or bug fixes that are essential.

I don't think it should be Transfer's job to determine what is or is not "questionable" database design. The whole point of an ORM is to automate the mapping an object model into relational data. As soon as we start making decisions based on database design, we're back to the old (and, unfortunately, valid) argument that most ORMs force your database structure onto your object model, instead of the other way around. What might be considered a "bad" relational model for a traditional OLTP database goes out the window pretty quickly when you start talking about ORMs. We aren't building an OLTP schema, we're building an object persistence schema, and the two can very easily end up in conflict.
 

Meanwhile, by means of a one2many on the 1:1 tables, you can achieve
what's essentially the same behavior (or at least similar) insofar as
getters and setters are concerned. Since a one2one is the same as a
one2many with the one minor difference that the joining table will
only ever have one related row, there's no particular reason not to
go ahead and use it.

The problem I have with it is that it results in a domain model that is misleading. Say I have a Product table. It contains base information common to all Products, such as price. However, I may sell Books. I may have a Book table, with a ton of extended information about books like author, number of pages, ISBN number, etc. Surely it would be less than ideal to add all of those columns to the Product table when a huge number of the rows won't even use them. But clearly there is indeed a one to one relationship between Book and Product.

Now, say I want to generate a Value Object for my Book. Ideally, Transfer would support multiple-table inheritance mappings, but we don't have that yet (though it is on the feature list). In lieu of that, the optimal way Transfer would let me structure it would be that Book has a property which is a Product. But looking at the metadata, I have no way to determine this. Instead, I have to use a one to many, in which case Book has an ARRAY of Products, despite the fact that there is no multiplicity.

It also results in an API that is unnecessarily difficult to use. Within Book, if I want to get the Product, I have to do something like:

product = getProductArray()
product = product[1]

instead of simply getProduct(). Not only is it more work, but it is misleading code, as well as relying on the "magic number" 1, and magic numbers make my skin crawl. :-)

If you want to enforce the 1:1 nature of the
tables, just create a decorator, override the generated setFoo()
method to indicate your preferred behavior (throw error? overwrite
original record? backup record then overwrite?).

Since I generate a great deal of my code, having to manually write a Decorator to address this makes my life more difficult. The answer to many previous feature requests could have been "write a decorator method to handle it", but that doesn't mean I can't ask for an easier way. ;-)
 

I guess that brings up yet another point against this... what's the
desired behavior when calling Bar.setFoo(Foo) on a 1:1 relationship?
There's too many viable options in this case to have consistent and
acceptable results.

I don't follow. If I have a Product and a Book, and I call book.setProduct(product), what exactly is the problem? How is this any different than the way one calls setParentFoo() or addFoo()?
 
Obviously I've worked around this issue, but I would argue that it was enough of a pain (especially once you have multiple subtype/supertype relationships in your model) to warrant some consideration. Mark can (and I'm sure will hehe) jump in, but on the surface it seems that unless the inheritance mapping feature is coming in the forseeable future, something like this might be a valid interim solution.



Brian Kotek

unread,
Sep 2, 2008, 12:24:40 PM9/2/08
to transf...@googlegroups.com
Also, note that Hibernate supports one-to-one relationships as a relationship type. I don't say this because I think Mark needs to do things just because Hibernate does them. I say it just to point out that a group of people who clearly have thought about this kind of stuff at great length chose to allow it.

Peter Bell

unread,
Sep 2, 2008, 12:27:15 PM9/2/08
to transf...@googlegroups.com
Just to throw another wrinkle, it might be worth distinguishing between inheritance strategies and true object relationships. All of the examples below are examples of the strengths and weaknesses of single table inheritance (all subtypes in one big table with lots of null field values) vs. single-table-per-class where you have a table for the base class and join tables with the extended data for different sub-classes with unique persisted properties. 

The reason I make this distinction is that it may be worthwhile to look for non-inheritance related uses for one2one - otherwise this issue might go away if transfer ever got into the business of supporting inheritance strategies a la Hibernate.

Best Wishes,
Peter

Jared Rypka-Hauer

unread,
Sep 2, 2008, 1:03:04 PM9/2/08
to transf...@googlegroups.com
I suppose I should say this first of all:

I am in no way against supporting 1:1 joins in Transfer... I'm just
stating what I was told when I wanted it and how I've worked around
it since then. Just wanted to clarify that. Also, I do think that
there is plenty of support for this sort of situation that supporting
them doesn't really need to be a priority for Mark (as far as I'm
concerned) until someone wants to cough up some ching for the
enhancement. If that happened I'm sure he'd be happy to add it.

Until then, I get the impression that demand for it is limited to a
few, albeit vocal, requesters. *g*

But wait! There's more! See inline below:

On Sep 2, 2008, at 11:16 AM, Brian Kotek wrote:

> I don't think it should be Transfer's job to determine what is or
> is not "questionable" database design. The whole point of an ORM is
> to automate the mapping an object model into relational data. As
> soon as we start making decisions based on database design, we're
> back to the old (and, unfortunately, valid) argument that most ORMs
> force your database structure onto your object model, instead of
> the other way around. What might be considered a "bad" relational
> model for a traditional OLTP database goes out the window pretty
> quickly when you start talking about ORMs. We aren't building an
> OLTP schema, we're building an object persistence schema, and the
> two can very easily end up in conflict.

Agreed... I wasn't saying it was Transfer's place to stand in
judgement of people's DB architecture decisions, but that it was
Mark's place to stand in judgement of what he's going to implement
based on the feedback he gets from the user base and the quality of
the product that results. OTOH, supporting getDatasource() was
accepted, and that's something that encountered a great deal of
discussion.

> It also results in an API that is unnecessarily difficult to use.
> Within Book, if I want to get the Product, I have to do something
> like:
>
> product = getProductArray()
> product = product[1]
>
> instead of simply getProduct(). Not only is it more work, but it is
> misleading code, as well as relying on the "magic number" 1, and
> magic numbers make my skin crawl. :-)

Even under the current circumstances you don't need to use magic
numbers. Something like this should even be generatable (I think I
maked up a new werd):

<cffunction name="getProduct" access="public"
returntype="model.product" output="false">
<cfset props = {bookId:getBookId()}>
<cfreturn getTransfer().readByPropertyMap("product",props) />
</cffunction>

<cffunction name="setProduct" access="public" returntype="void"
output="false">
<cfargument name="product" type="model.product" required="true" />
<cfset arguments.product.setBookId(getId)>
<cfset getTransfer.save(Product)>
</cffunction>

readByPropertyMap() even throws an error if more than one row is
found... which, I guess, is sort of half-way support for 1:1 support,
eh? ;)

>
>
> If you want to enforce the 1:1 nature of the
> tables, just create a decorator, override the generated setFoo()
> method to indicate your preferred behavior (throw error? overwrite
> original record? backup record then overwrite?).
>
> Since I generate a great deal of my code, having to manually write
> a Decorator to address this makes my life more difficult. The
> answer to many previous feature requests could have been "write a
> decorator method to handle it", but that doesn't mean I can't ask
> for an easier way. ;-)

Indeed not, and it would have been very very useful to me when I
first started using Transfer because of the way I designed my
databases back then.

>
>
>
> I guess that brings up yet another point against this... what's the
> desired behavior when calling Bar.setFoo(Foo) on a 1:1 relationship?
> There's too many viable options in this case to have consistent and
> acceptable results.
>
> I don't follow. If I have a Product and a Book, and I call
> book.setProduct(product), what exactly is the problem? How is this
> any different than the way one calls setParentFoo() or addFoo()?

My point was basically that if you were going to implement 1:1s,
given a record where the other side of the relationship already
exists when you call setFoo(Foo), do you overwrite the other record?
Do you update the other record? Is it up to the dev to say "if hasFoo
(), removeFoo(); setFoo()"? Is there just one way to set this up to
work? m2ms and o2ms don't have this issue to begin with... so I am
curious about it. And since I haven't really worried about using 1:1s
since I started using Transfer, I haven't had to mess with it at
all... so I really don't know and am asking how this would work.

> Obviously I've worked around this issue, but I would argue that it
> was enough of a pain (especially once you have multiple subtype/
> supertype relationships in your model) to warrant some
> consideration. Mark can (and I'm sure will hehe) jump in, but on
> the surface it seems that unless the inheritance mapping feature is
> coming in the forseeable future, something like this might be a
> valid interim solution.

Or set up your generator to recognize 1:1 relationships and generate
get/set methods based on the one I've posted above and you're in
business... no biggie. The Decorator API is one of the more
powerfuller things we've got at our disposal in Transfer and there's
almost nothing you can't do with it.

Laterz,
J

Brian Kotek

unread,
Sep 2, 2008, 2:17:37 PM9/2/08
to transf...@googlegroups.com
On Tue, Sep 2, 2008 at 1:03 PM, Jared Rypka-Hauer <armcha...@gmail.com> wrote:

I suppose I should say this first of all:

I am in no way against supporting 1:1 joins in Transfer... I'm just
stating what I was told when I wanted it and how I've worked around
it since then. Just wanted to clarify that. Also, I do think that
there is plenty of support for this sort of situation that supporting
them doesn't really need to be a priority for Mark (as far as I'm
concerned) until someone wants to cough up some ching for the
enhancement. If that happened I'm sure he'd be happy to add it.

Until then, I get the impression that demand for it is limited to a
few, albeit vocal, requesters. *g*

Heh, I hadn't even ever brought this up before, but since it came up in the other thread I figured I'd see what the deal was. It probably is a small number of requesters, but part of that may be because Transfer just doesn't let you do it. Which means people are adjusting their object model right off the bat and working around the issue. Some might not even realize there are other options that just haven't been incorporated yet.
 

Agreed... I wasn't saying it was Transfer's place to stand in
judgement of people's DB architecture decisions, but that it was
Mark's place to stand in judgement of what he's going to implement
based on the feedback he gets from the user base and the quality of
the product that results. OTOH, supporting getDatasource() was
accepted, and that's something that encountered a great deal of
discussion.

Surely the final call is Mark's. But as you say, it people don't bring it up, it will almost certainly never get added. And if or when inheritance mapping gets implemented it will probably be much less of an issue.
 
Even under the current circumstances you don't need to use magic
numbers. Something like this should even be generatable (I think I
maked up a new werd):

       <cffunction name="getProduct" access="public"
returntype="model.product" output="false">
               <cfset props = {bookId:getBookId()}>
               <cfreturn getTransfer().readByPropertyMap("product",props) />
       </cffunction>

       <cffunction name="setProduct" access="public" returntype="void"
output="false">
               <cfargument name="product" type="model.product" required="true" />
               <cfset arguments.product.setBookId(getId)>
               <cfset getTransfer.save(Product)>
       </cffunction>

readByPropertyMap() even throws an error if more than one row is
found... which, I guess, is sort of half-way support for 1:1 support,
eh? ;)

Probably true (haven't tried it but it looks like it would work). The real issue is that I have no way to determine if a One To Many is actually a One To Many or is one of my "One To Many But Actually a One To One" relationship from looking at the Transfer configuration/metadata. However, a custom attribute in the XML would probably work (also on the feature list).

Being the code-gen guy that I am, I try to avoid as much manual stuff as I can. I could also envision a custom method in an Abstract Decorator that takes the name of the related one-to-one table as an argument and gets the related object dynamically, but then I'm into non-standard code (sometimes you get a related object like this, other times you get it like this) which I also avoid as much as possible.
 
My point was basically that if you were going to implement 1:1s,
given a record where the other side of the relationship already
exists when you call setFoo(Foo), do you overwrite the other record?
Do you update the other record? Is it up to the dev to say "if hasFoo
(), removeFoo(); setFoo()"? Is there just one way to set this up to
work? m2ms and o2ms don't have this issue to begin with... so I am
curious about it. And since I haven't really worried about using 1:1s
since I started using Transfer, I haven't had to mess with it at
all... so I really don't know and am asking how this would work.

I suppose ideally I could do book.setProduct(product), and then if I save Book it would automatically save Product. If new, it would save the Product, if existing, it would update the Product, etc. There could be a possibility of orphan records if I were to set a different Product, but the ORM probably can't be expected to stop me from doing something stupid. I could see a couple of options:

new Product is saved leaving orphan Product
error thrown
old Product is deleted and new product is Saved

I'd probably be fine with any of the three, though the last two would avert the orphan record. Since this would only happen in cases where you're trying to do something that shouldn't be done anyway (it would be akin to trying to replace the superclass with another object), which one happens is less of a concern to me.
 

Or set up your generator to recognize 1:1 relationships and generate
get/set methods based on the one I've posted above and you're in
business... no biggie.

Again, it's dynamically differentiating between real One to Many and pseudo One To Many that would be the problem.
 
The Decorator API is one of the more
powerfuller things we've got at our disposal in Transfer and there's
almost nothing you can't do with it.

Very true, Decorators rock, hehe.

Jade

unread,
Sep 5, 2008, 10:18:26 AM9/5/08
to transfer-dev
While I'm certainly out of my league in this discussion, I felt I had
to chime in mostly due to the inference about 1:1's maybe not being a
great db design practice, or that it's a edge case, or that it would
be a better practice to combine the tables.

I have 1:1's regularly. And I thought the comment about people finding
their workarounds early was most appropriate. It was a challenge for
me from day 1 when starting to use Transfer. My solution has been to
use the M:1 relationship to manage my 1:1s.

I use a lot of 1:M (as I expect most people do) but I have yet to find
myself using the M:1. I don't know if this means I've missed the boat
on where M:1s are appropriate, but that's the way it's been working
out for me. The nice thing about the M:1 is as mentioned above you
have to do your getFoo(1) whereas the M:1 is just getFoo().

It still requires a check for existing so I just do: if (hasFoo())
{removeFoo()} setFoo() whenever I'm setting it.

Using the M:1 also helps set apart the obfuscation of using the 1:M to
manage both 1:Ms and 1:1s. Of course if you had a business need for
the M:1 then you would be right back into the same obfuscation
problem.

I've googled this topic many times previously trying to figure out my
best practices and only ran into a lot of discussion about whether it
should be handled which is why I thought I would contribute this time.

ike

unread,
Sep 7, 2008, 8:25:06 PM9/7/08
to transfer-dev
I hope this isn't seen as poaching... There certainly have been a few
ideas that I've taken from Transfer recently, although much of what's
in my ORM already existed before Mark started working on it. I hadn't
originally figured out how to automate the server type property for
example, which Mark pointed out is actually a pretty simple thing, I
had just overlooked some available metadata.

Anyway after reading this thread and seeing that there seems to be a
fair amount of discussion about this subject of 1-1 relationships and
inheritance mapping, it seemed like some folks might appreciate
hearing that the DataFaucet ORM added support for multi-table
inheritance mapping a couple months ago.

DataFaucet is not however a copy of Hibernate done with ColdFusion. I
started working on it with ColdFusion 5 during a time when nobody was
doing ORM and the people who saw it mostly gave me strange looks for
doing it. :P I don't know if Hibernate was around at the time, but I
wasn't involved in the Java community and the ColdFusion community
certainly wasn't talking about Java projects with nearly the kind of
frequency we do today. So I knew nothing of Hibernate. As a result the
DataFaucet ORM is going to map to any given problem domain a bit
differently than Hibernate or Transfer. And I figured it was worth
mentioning that there are options beyond using decorators or changing
your schema if you're not already committed to using the current
release of Transfer (or the way Transfer works).

This is really not intended as any kind of disrespect to Mark. While I
personally don't prefer the approach that Hibernate or Transfer have
to the challenge of persistence, I have nothing against Mark or
Transfer in general. And I think there's plenty of diversity in the
community for both approaches to appeal to different groups of people
and proliferate. As I mentioned before, I've taken ideas from Mark and
when I gave a recent DataFaucet presentation to the the online CFUG I
recall Mark mentioning that he was fairly impressed by an implicit and/
or keyword searching feature in the recently added gateway object. If
I remember correctly the exact quote was "/me steals". :) Heck
whenever I give presentations on DataFaucet I mention Transfer, but I
also mention Steve Bryant's DataMgr tools.

So I hope this reply is received in the spirit of "simply sharing with
the community". :)

For anyone interested, the DataFaucet ORM can be found at www.datafaucet.com.

Thanks,
ike

Brian Kotek

unread,
Sep 7, 2008, 9:46:57 PM9/7/08
to transf...@googlegroups.com
Ike, I looked around for a few minutes but don't see anything related to inheritance mapping in the documentation.

ike

unread,
Sep 7, 2008, 11:08:34 PM9/7/08
to transfer-dev
I might need to add a page to the documentation like "DataFaucet for
Transfer / Hibernate users" or the like. :) I've seen similar
documents for other software packages in the past, like "XXX for
WordPerfect Users" a long time ago. Thing is I'm not familiar enough
with the Hibernate / Transfer lingo to really know how best to write a
document like that... but I'd love to get some help -- and you can
offer any kind of suggestions on the wiki http://datafaucet.wikispaces.com
. (And we can reverse-engineer it for "Transfer for DataFaucet users"
too.)

There's a function in the ActiveRecord object (which may not be
entirely what you expect an "activerecord" to be either) called
addTable(alttable). There's some documentation for it currently in the
ActiveRecord page. It's described in the TOC on this page as "join-
subclass". http://www.datafaucet.com/activerecord.cfm

On Sep 7, 9:46 pm, "Brian Kotek" <brian...@gmail.com> wrote:
> Ike, I looked around for a few minutes but don't see anything related to
> inheritance mapping in the documentation.
>
Reply all
Reply to author
Forward
0 new messages