Question

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?

Was it helpful?

Solution

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: NHibernate ISession Flush: Where and when to use it, and why?

OTHER TIPS

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.

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top