views:

19

answers:

1

Here's the scenario:

I have 3 objects called Person, VideoGame, and Store.

One Person can have many VideoGames

One VideoGame can belong to many Persons

Same M:N relationship is between Store and VideoGames

In the DB, the only thing besides these entities are two simple join tables PersonsVideoGames and StoresVideoGames.

Assume everything has an Id property and they are unique.

Business Rules:

  • I do not want a video game to be in the VideoGames table if it is not associated with anything (an orphan)
  • I do want it in the table if it associated with at least one other object
  • You do not manage VideoGames directly, rather the other sides (Store, Person) manage saving/deleting video games.

Is this possible to do using NHibernate mapping? From my actual project implementation, it doesn't seem to work at the basic Person <-> VideoGame level.

NHibernate currently will delete the VideoGame even though it is still associated with other Persons (it is not truly an orphan).

My mapping looks like:

Person Has M:N of VideoGame as a set, with cascade style all-delete-orphan enabled.

VideoGame Has M:N of Person as a set, with lazy-load, inverse, and cascade style save-update enabled.

The Person does not have a public setter for its VideoGames property. It has a function like so:

Public Overridable Sub SetVideoGames(ByVal games As IEnumerable(Of VideoGame))
    If Me.VideoGames Is Nothing Then Exit Sub

    ' Add if the game isn't in the old list.
    For Each g In games
        If Not Me.VideoGames.Any(Function(g2) g2.Id = g.Id) Then
            Me.VideoGames.Add(usr)
        End If
    Next

    ' Remove if the game isn't in the new list
    For Each g In Me.VideoGames.ToList()
        If Not games.Any(Function(g2) g2.Id = g.Id) Then
            Me.VideoGames.Remove(g)
        End If
    Next
End Sub

Then whenever I save a Person, I get a list of video games (either existing already or brand new), then use that set method:

' listOfVideogames is just a list of ShortTitle strings

Dim result As New List(Of VideoGame)()
Dim newGames As New List(Of VideoGame)()
Dim existingGames As IList(Of VideoGame) ' = Session.Get(blah, gets the existing games for this person)

' Add the existing games to the result set
result.AddRange(existingGames)

' Get any titles from the given list not in the existing list
For Each title In listOfVideogames.Except(existingGames.Select(Function(g) g.ShortTitle))
    Dim newGame As New VideoGame() With {
        .ShortTitle = title 
    }

    newGames.Add(newGame)
Next

' Add them to the resultset
result.AddRange(newGames)

existingPerson.SetVideoGames(result)
' Do other updates
MyNHibernateDataProvider.Save(existingPerson)

A video game can be referred to using a more friendly ID like "ShortTitle" as an example. The saving then takes in a IEnumerable(Of String) like "DA:O,BatmanAA,LBP2" etc. and then finds any existing video games in the DB for that Person that match it or creates a new domain object with that short title (assume only Id and ShortTitle are the only properties).

So, does anyone know what's wrong? How come NHibernate isn't detecting that a VideoGame is already associated with other Persons when it removes a VideoGame from that set?

Furthermore, assuming I got this working, will this scenario even work once I set up more M:N relationships (like Stores)?

+1  A: 

all-delete-orphan is not that powerful. If you have other relationships, you have to manage delete cascading manually.

Diego Mijelshon
I expected that; what would you recommend to solve this, is there a way to check the associations for a VideoGame when the row from PersonVideoGames is removed? I can see a DB-level trigger, what about a .NET/NHibernate method?
subkamran
It's really a responsibility of the Model. When deleting an association, you should check the Count() on the other associations of the related entity.
Diego Mijelshon
That makes sense. I think short of hacking and slashing NHibernate to check associations for orphans, if you specify that (which I think could be done?), handling it in the code is probably the best bet.
subkamran