0

I am developing a client-side app which passes data back to a database. The client has the ability to create an object of type PlaylistItem. I do not wait for the database to respond with a generated ID when creating a PlaylistItem. Instead, I let the client generate the ID, but write the PlaylistItem to the database with a PK of { PlaylistID, PlaylistItemID }. PlaylistID is generated by the server. I followed this approach after talking things over a bit with Jon Skeet

Now, I'm trying to get things jiving in NHibernate, but I'm running into some pretty hefty issues. All the resources I read keep stating, "NHibernate heavily dissuades against the use of composite keys. Only use them if you're working on a legacy DB." I'm not working on a legacy DB, so I assume I should make the change. However, I have no idea what my alternatives would be in such a scenario.

Here's PlaylistItem's NHibernate mapping and corresponding class:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Streamus" namespace="Streamus.Backend.Domain">

  <class name="PlaylistItem" table="[PlaylistItems]" lazy="false" >
    <composite-id>
      <key-property name="Id" />
      <key-property name="PlaylistId"/>
    </composite-id>

    <property name="Title" not-null="true" />

    <many-to-one name="Playlist" column="PlaylistId"/>
  </class>

</hibernate-mapping>

[DataContract]
public class PlaylistItem
{
    [DataMember(Name = "playlistId")]
    public Guid PlaylistId
    {
        get { return Playlist.Id; }
        set { Playlist.Id = value; }
    }

    public Playlist Playlist { get; set; }

    [DataMember(Name = "id")]
    public Guid Id { get; set; }

    //  Store Title on PlaylistItem as well as on Video because user might want to rename PlaylistItem.
    [DataMember(Name = "title")]
    public string Title { get; set; }

    public PlaylistItem()
    {
        //  Id shall be generated by the client. This is OK because it is composite key with 
        //  PlaylistId which is generated by the server. 
        Id = Guid.Empty;
        Title = string.Empty;
    }

    private int? _oldHashCode;
    public override int GetHashCode()
    {
        // Once we have a hash code we'll never change it
        if (_oldHashCode.HasValue)
            return _oldHashCode.Value;

        bool thisIsTransient = Equals(Id, Guid.Empty);

        // When this instance is transient, we use the base GetHashCode()
        // and remember it, so an instance can NEVER change its hash code.
        if (thisIsTransient)
        {
            _oldHashCode = base.GetHashCode();
            return _oldHashCode.Value;
        }
        return Id.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        PlaylistItem other = obj as PlaylistItem;
        if (other == null)
            return false;

        // handle the case of comparing two NEW objects
        bool otherIsTransient = Equals(other.Id, Guid.Empty);
        bool thisIsTransient = Equals(Id, Guid.Empty);
        if (otherIsTransient && thisIsTransient)
            return ReferenceEquals(other, this);

        return other.Id.Equals(Id);
    }
}

NHibernate throws the exception with error message "Invalid index n for this SqlParameterCollection with Count=n." which I understand to arise when there is a duplicate declaration in an hbm.xml file. From my understanding, this would arise because I define PlaylistId as a key-property and again in the many-to-one relationship.

What are my options here? I'm pretty stumped.

Community
  • 1
  • 1
Sean Anderson
  • 27,963
  • 30
  • 126
  • 237
  • How could I "give the client a code" when I do not wish to wait for the server to respond with a successful insert? The client must be able to work without waiting for the server, but I also understand that I should not trust just a client-side generated ID. I am considering having the server send over a bunch of valid IDs that the client can use and then have the server ensure that the key used is valid, but then NHibernate won't be able to tell if a save is a Create or Update because the ID will always be set... – Sean Anderson Apr 02 '13 at 20:31
  • I don't feel like that solves the issue. Saying "GetPlaylistItemByClientID" is the same issue as saying "GetPlaylistItemByPKID" if the client is the one generating the UUID, regardless of whether it is the PK or just another column being used to uniquely identify the value. You cannot rely on the uniqueness of a client-side generated value in a database, right? – Sean Anderson Apr 02 '13 at 20:37
  • If I issue an AJAX request to the server to check if the client ID is unique -- I have to wait for the server's confirmation before the client can react. This is the same as waiting for the server to generate the ID, no? Maybe a split second faster because do not have to wait on the database save, but all the latency comes from the message passing to/from the server and not the action the server is taking. – Sean Anderson Apr 02 '13 at 20:58
  • I think you are getting confused between primary keys and natural keys. In your case the primary key is the `Id` generated by NHibernate on insert. The natural id is the `PlayListId` given by your user. Each of these keys are unique but each serve a different purpose. See [here](http://ayende.com/blog/4061/nhibernate-natural-id) for more info about natural ids. – mickfold Apr 02 '13 at 20:59
  • I think I have them straight -- Id is generated client-side and is serving as a natural key because I do not trust it to be the PK by itself, even though it should probably be unique. PlaylistId is generated by the server and is a PK for the playlist. The PK for a PlaylistItem is the composite of both of those because it incorporates a trusted, server-side value (PlaylistId) I am willing to wait for the server to respond/generate the PlaylistId because creating a Playlist occurs far less frequently than creating a PlaylistItem. (I do agree that the link was nice, though!) – Sean Anderson Apr 02 '13 at 21:03
  • That still defeats the purpose. The server should be completely transparent to the end user when adding a PlaylistItem. If my server starts to lag under heavy load I do not want the user experience to deteriorate. Anywho, I think I am just going to go along with 'trusting' the client generated ID for gets/deletes, but use a server-generated ID for the PK. We'll see if it bites me in the ass in the future. – Sean Anderson Apr 02 '13 at 21:07
  • Using the normal PK generated by the server is always the preferred method... – nathan hayfield Apr 02 '13 at 21:13
  • Just wondering why you don't use `` as part of your composite key? Also remove the other many-to-one. This will probably fix the error you have. – mickfold Apr 02 '13 at 21:57
  • Oh shit, you can do that? One second. Let me try it out! – Sean Anderson Apr 02 '13 at 22:20
  • @penfold I am now receiving a different error message. It states "An association from the table [PlaylistItems] refers to an unmapped class: System.Guid" but it seems like this is definitely the right path. Will continue to play with it. – Sean Anderson Apr 02 '13 at 22:34
  • @penfold this allows the hbm.xml to be compiled, but generates a PropertyAccessException when I run another test case saying "Object does not match target type" because it is grabbing Playlist.Id and trying to convert that to a class. Humm. – Sean Anderson Apr 02 '13 at 22:51
  • AWWWW YEAH IT WORKED!!!!!! Feel free to submit that as an answer. – Sean Anderson Apr 02 '13 at 23:01

1 Answers1

1

You could use a key-many-to-one instead of key-property, i.e.

<class name="PlaylistItem" table="[PlaylistItems]" lazy="false" >
  <composite-id>
    <key-property name="Id" />
    <key-many-to-one name="Playlist" column="PlaylistId"/>
  </composite-id>

  <property name="Title" not-null="true" />

</class>

Then your class would look like...

[DataContract]
public class PlaylistItem
{
  // Your composite key...
  [DataMember(Name = "id")]
  public Guid Id { get; set; }    
  public Playlist Playlist { get; set; }

  //  Store Title on PlaylistItem as well as on Video because user might want to rename PlaylistItem.
  [DataMember(Name = "title")]
  public string Title { get; set; }

  // rest of class...
}
mickfold
  • 2,003
  • 1
  • 14
  • 20