Modeling one to zero or one relationships (Z cardinality)
Asked Answered
A

4

8

I'm struggling to find the best way to model 1 : 0,1 relationships ("may have one" or "has at most one"). I believe this is called Z cardinality.

For example, suppose I have two classes Widget and WidgetTest. Not all Widgets are tested and the test is destructive so there can be at most one WidgetTest per Widget. Also assume that it's inappropriate to add the WidgetTest fields to Widget.

I would like my public interface to be:

Widget
    WidgetTest { get; set; }

WidgetTest
    Widget { get; }

Model 1: Widget has a WidgetTest property and in the database the Widget table has a uniquely constrained foreign key to WidgetTest. My DBA argues that this would allow a WidgetTest record to exist without a Widget.

WidgetTable
    WidgetTestId (FK, UQ)

Model 2: Widget has a private collection of WidgetTest and enforces the 0,1 relationship by adding or removing a single object from the collection controlled by a public WidgetTest property. The database models this as 1:m with WidgetTest having a uniquely constrained foreign key to Widget. I argue that this means adopting the model to fit the database schema (i.e. more work for me).

WidgetTestTable
    WidgetId (FK, UQ)

Which model is better? Which is easier to achieve with NHibernate? Or is there a third way?

Edit ... Here's what I ended up with:

public class Widget
{
    // This is mapped in NH using a access strategy
    private IList<WidgetTest> _widgetTests = new List<WidgetTest>(1);

    public WidgetTest
    {
        get { return _widgetTests.FirstOrDefault(); }
        set
        {
            _widgetTests.Clear();
            if (value != null)
            {
                _widgetTests.Add(value);
            }
         }
     }
}
Allaround answered 5/2, 2010 at 16:52 Comment(1)
whats wrong with a 1:? enforced by PK-PK. mapping that fairly straight ahead..Chukker
D
4

My approach has been to model a one-to-many relationship in the mappings, but to constrain the "many" to a single item. This allows the optional one-to-one, and also guarantees that your WidgetTest instance is persisted when you save the Widget. For example:

public class Widget
{
    /// <summary>
    /// This property is ignored by the NHibernate mappings.
    /// </summary>
    public virtual WidgetTest WidgetTest { get; set; }

    /// <summary>
    /// For easier persistence with NHibernate, this property repackages the
    /// WidgetTest property as a list containing a single item. If an
    /// attempt is made to set this property to a list containing more than
    /// one item, an exception will be thrown. But why bother? Just use the
    /// WidgetTest property.
    /// </summary>
    public virtual IList<WidgetTest> WidgetTests
    {
        get
        {
            IList<WidgetTest> widgetTests = new List<WidgetTest>();
            if (this.WidgetTest != null)
            {
                widgetTests.Add(this.WidgetTest);
            }
            return widgetTests;
        }
        set
        {
            if (value != null && value.Count > 1)
            {
                throw new Exception("The WidgetTests collection may not contain more than one item.");
            }
            else if (value != null && value.Count == 1)
            {
                this.WidgetTest = value[0];
            }
            else
            {
                this.WidgetTest = null;
            }
        }
    }
}
Dextrocular answered 5/2, 2010 at 22:21 Comment(5)
This is more-or-less my "Model 2" and I've done something similar in the past. I map the collection as a private member and don't expose it however. I was thinking about it a bit more and this model works for a 1:n relationship. I'm getting hung up on it when n=1.Allaround
See the edit to my question for the solution I settled on. This answer is closest so I accepted it.Allaround
I like your variation...very concise. Would you be willing to share the NH mapping you used?Dextrocular
There must be a way to do this with HasOne. Not discovered yet :(Discrepant
I am getting an exception - A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance. I think I know why this is and will post a solution if it works.Oeo
E
1

When you say "assume that it's inappropriate to add the WidgetTest fields to Widget", do you mean in your domain objects or in the database. If you are happy for the fields to be in the same table in the database, how about mapping WidgetTest as a component of Widget? Have the NHibernate mapping file look like:

<class name="Widget" table="Widget">
    ...
    <property name="WidgetProperty"/>
    ...
    <component name="WidgetTest" class="WidgetTest">
        <property name="WidgetTestProperty"/>
    </component>
</class>

Giving the table structure:

WidgetTable
    WidgetProperty
    WidgetTestProperty

Which would still let you have the public interface you've specified, however, WidgetTest would become a value object which you may or may not want.

Edible answered 5/2, 2010 at 17:2 Comment(1)
I mean in the table. Besides adding over a hundred additional fields, they would have to default to null which is undesired. I'm more interested in the best way to model 1:0,1 relationships in general than for a specific application.Allaround
A
0

i have 2 other ideas here

  • Join table and map as Component
  • ignore Id of dependant class
Alfilaria answered 2/2, 2012 at 16:43 Comment(0)
O
0

The answer given by nw. can result in the exception "A collection with cascade=”all-delete-orphan” was no longer referenced by the owning entity instance".

You will find this to be the case if you're using inverse="true" and cascade="all-delete-orphan" in your mapping file.

This is because nw.'s answer creates a new list every time the get accessor is called and doesn't do anything with the list passed in through the set accessor. As such, NHibernate doesn't have the IList<WidgetTest> reference it originally passed in when creating the object and can't proceed with the cascade.

So in order to fix this, we need to do something with that IList<WidgetTest> reference and be careful not to de-reference it.

public class Widget
{
    public Widget()
    {
        _widgetTests = new List<WidgetTest>();
    }

    /// <summary>
    /// This property is ignored by the NHibernate mappings.
    /// </summary>
    public WidgetTest WidgetTest { get; set; }

    /// <summary>
    /// For easier persistence with NHibernate, this property repackages the
    /// WidgetTest property as a list containing a single item. If an
    /// attempt is made to set this property to a list containing more than
    /// one item, an exception will be thrown. But why bother? Just use the
    /// WidgetTest property.
    /// </summary>
    private IList<WidgetTest> _widgetTests;
    protected virtual IList<WidgetTest> WidgetTests
    {
        get
        {
            if (_widgetTests.Count == 0 && WidgetTest != null)
            {
                _widgetTests.Add(WidgetTest);
            } 
            else if (_widgetTests.Count > 0 && WidgetTest == null)
            {
                _widgetTests.Clear();
            } 
            else if (_widgetTests.Count > 0 && WidgetTest != _widgetTests[0])
            {
                _widgetTests.Clear();
                _widgetTests.Add(WidgetTest);
            }
            return _widgetTests;
        }
        set
        {
            if (value != null && value.Count > 1)
            {
                throw new Exception("The WidgetTest collection may not contain more than one item.");
            }
            if (value != null && value.Count == 1)
            {
                WidgetTest = value[0];
            }
            else
            {
                WidgetTest = null;
            }

            //Store the reference
            _widgetTests = value;
        }
    }
}

Mapping:

  <class name="Widget" table="widgets">
    ...
    <id name="Id" type="Guid" column="widgetId">
      ...
    </id>
    ...                 
    <bag name="WidgetTests" inverse="true" cascade="all-delete-orphan" access="property">
      ...
      <key column="widgetId" />
      <one-to-many class="WidgetTest" />
    </bag>

  </class>

Inspiration for the enhancement:

http://www.onkarjoshi.com/blog/188/hibernateexception-a-collection-with-cascade-all-delete-orphan-was-no-longer-referenced-by-the-owning-entity-instance/comment-page-1/

Oeo answered 13/9, 2016 at 8:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.