views:

182

answers:

3

I have a many-to-many relationship between photos and tags: A photo can have multiple tags and several photos can share the same tags.

I have a loop that scans the photos in a directory and then adds them to NHibernate. Some tags are added to the photos during that process, e.g. a 2009-tag when the photo is taken in 2009.

The Tag class implements Equals and GetHashCode and uses the Name property as the only signature property. Both Photo and Tag have surrogate keys and are versioned.

I have some code similar to the following:

public void Import() {
    ...
    foreach (var fileName in fileNames) {
        var photo = new Photo { FileName = fileName };
        AddDefaultTags(_session, photo, fileName);
        _session.Save(photo);
    }
    ...
}

private void AddDefaultTags(…) {
    ...
    var tag =_session.CreateCriteria(typeof(Tag))
                    .Add(Restriction.Eq(“Name”, year.ToString()))
                    .UniqueResult<Tag>();

    if (tag != null) {
        photo.AddTag(tag);
    } else {
        var tag = new Tag { Name = year.ToString()) };
        _session.Save(tag);
        photo.AddTag(tag);
    }
}

My problem is when the tag does not exist, e.g. the first photo of a new year. The AddDefaultTags method checks to see if the tag exists in the database and then creates it and adds it to NHibernate. That works great when adding a single photo but when importing multiple photos in the new year and within the same unit of work it fails since it still doesn’t exist in the database and is added again. When completing the unit of work it fails since it tries to add two entries in the Tags table with the same name...

My question is how to make sure that NHibernate only tries to create a single tag in the database in the above situation. Do I need to maintain a list of newly added tags myself or can I set up the mapping in such a way that it works?

A: 

If you want the new tag to be in the database when you check it each time you need to commit the transaction after you save to put it there.

Another approach would be to read the tags into a collection before you process the photos. Then like you said you would search local and add new tags as needed. When you are done with the folder you can commit the session.

You should post your mappings as i may not have interpreted your question correctly.

Maggie
My problem with this approach is that I would like to run the import as a unit of work and not commit anything to the database before the entire unit is complete. Ideally, running in a unit of work should be transparent for the AddDefaultTags method but I guess that isn't possible...I think you understand my problem just fine. I don't think the mappings themselves are an issue - it it rather deciding on this correct pattern to use the mappings.
HakonB
+1  A: 

You need to run _session.Flush() if your criteria should not return stale data. Or you should be able to do it correctly by setting the _session.FlushMode to Auto.

With FlushMode.Auto, the session will automatically be flushed before the criteria is executed.

EDIT: And important! When reading the code you've shown, it does not look like you're using a transaction for your unit of work. I would recommend wrapping your unit of work in a transaction - that is required for FlushMode.Auto to work if you're using NH2.0+ !

Read further here: http://stackoverflow.com/questions/43320/nhibernate-isession-flush-where-and-when-to-use-it-and-why

asgerhallas
I am sorry that I didn't explain that part well. My FlushMode is set to Never. I am working within a unit of work where I do an explicit flush when complete. Oh, and running within a transaction as well.
HakonB
Oh. Ok. If you are absolutely against having a call to the DB at the point in the code, where you insert a new tag, I believe that theres is no other way than to keep track of the tags yourself. But I don't see why you would not flush it, you'll still be able to rollback on exceptions within the unit of work.
asgerhallas
Take a look here, for the rollback goodness and caveats: http://objectissues.blogspot.com/2004/12/understanding-hibernate-transaction.html
asgerhallas
I guess that I am going to allow NHibernate to flush when it needs to. It does make my life a lot easier if I don't have to keep track of new Tags myself.
HakonB
A: 

This is that typical "lock something that is not there" problem. I faced it already several times and still do not have a simple solution for it.

This are the options I know until now:

  • Optimistic: have a unique constraint on the name and let one of the sessions throw on commit. Then you try it again. You have to make sure that you don't end in a infinite loop when another error occurs.
  • Pessimistic: When you add a new Tag, you lock the whole Tag table using TSQL.
  • .NET Locking: you synchronize the threads using .NET locks. This only works if you parallel transactions are in the same process.
  • Create Tags using a own session (see bellow)

Example:

public static Tag CreateTag(string name)
{
  try
  {
    using (ISession session = factors.CreateSession())
    {
      session.BeginTransaction();
      Tag existingTag = session.CreateCriteria(typeof(Tag)) /* .... */
      if (existingtag != null) return existingTag;
      {
        session.Save(new Tag(name));
      }
      session.Transaction.Commit();
    }
  }
  // catch the unique constraint exception you get
  catch (WhatEverException ex)
  {
    // try again
    return CreateTag(name);
  }
}

This looks simple, but has some problems. You get always a tag, that is either existing or created (and committed immediately). But the tag you get is from another session, so it is detached for your main session. You need to attach it to your session using cascades (which you probably don't want to) or update.

Creating tags is not coupled to your main transaction anymore, this was the goal but also means that rolling back your transaction leaves all created tags in the database. In other words: creating tags is not part of your transaction anymore.

Stefan Steinegger