12
votes

i have 3 tables in my database:

  1. Projects (id, name)
  2. Tags (id, name)
  3. ProjectsTagss (id, projectId, tagid)

As you can see the ProjectsTags table is a bridge table

here is my fluent nhibernate mapping

ProjectMap.cs:

 Map(x => x.Name).Not.Nullable();
 HasMany(x => x.ProjectsTags).AsBag().Inverse()
    .Cascade.AllDeleteOrphan().Fetch.Select().BatchSize(80);

ProjectsTagsMap.cs:

 References(x => x.Project).Not.Nullable();
 References(x => x.Tag).Not.Nullable();

TagMap.cs:

  Map(x => x.Name).Not.Nullable();

As you can see, i historically didn't have the Tag table linked to anything else. I now need to generate a report to show Tag and how often that tag is used so i need to join from Tag to ProjectsTag. i tried adding this line into the tagsmap:

 HasMany(x => x.ProjectsTags).AsBag().Inverse()
    .Cascade.AllDeleteOrphan().Fetch.Select().BatchSize(80);

but when i go to update the name on a tag object and commit, i get this error:

A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance

can anyone see anything wrong with what i added that would be causing this nhibernate exception when i simply update the Tag table. Again my goal is to be able to do something like:

 Tag.ProjectTags.Count();

Here is some additional code as requested:

my Tag Class:

 public class Tag
{
    public virtual IList<ProjectTag> ProjectTags { get; set; }
    public virtual string Name { get; set; }
    public virtual string Description { get; set; }
}
2
Show us the code where you update the tags. And please write a meaningful title which expresses the question.Stefan Steinegger
@Stefan Steinegger - updated title to reflect the errorleora
The error happens when you remove an item from the list and reuse it in another list. So please show us the code you wrote to update the data.Stefan Steinegger
I agree with Stefan. You should post the code you are using to modify your object and how you are persisting the changes. In addition to this it may help to see how your Tag class is implemented. Specifically adding/removing/editing any items in your ProjectTags collection.Cole W
@Cole W - i am never explicitally removing ProjecTag objects from the Tag object. (maybe that is the issue). i just take the tag object from the database, update the Name property and do a Repository.Save() and Repository.Committ()leora

2 Answers

19
votes

Somewhere in your code, you should have dereferenced the original collection on your Project domain. I suspect that your code goes like this:

var project = Session.Get<Project>();
project.ProjectsTags = new List<ProjectsTags> { someProjectsTagsInstance };
Session.Save(project);

If this is the case, you should do this instead:

var project = Session.Get<Project>();
project.ProjectsTags.Clear();
project.ProjectsTags.Add(someProjectsTagsInstance);
Session.Save(project);
7
votes

While a collection is not modified, NH can still think that it is. Something like this could be caused by a ghost update. From NHibernate 3.0 Cookbook, Jason Dentler (page 184): "As part of automatic dirty checking, NHibernate compares the original state of an entity to its current state. An otherwise unchanged entity may be updated unnecessarily because a type conversion caused this comparison to fail".

Ghost update of collection can be caused by code that looks like this:

public class Tag
{
    private IList<ProjectTag> projectsTags;

    public virtual IEnumerable<ProjectTag> ProjectsTags
    {
        get
        {
            return new ReadOnlyCollection<ProjectTag>(projectsTags);
        }

        set
        {
            projectsTags = (IList<ProjectTag>)value;
        }
    }
}

ProjectsTags property returns the collection in readonly wrapper, so client code cannot add or remove elements to/from the collection.

The error will appear even when name of a tag is not changed:

private void GhostTagUpdate(int id)
{
    using (var session = OpenSession())
    {
        using (var transaction = session.BeginTransaction())
        {
            var tag = session.Get<Tag>(id);

            transaction.Commit();
        }
    }
}

ProjectsTags collection should be mapped with CamelCaseField access strategy to avoid ghost updated:

HasMany(x => x.ProjectsTags)
    .Access.CamelCaseField()
    .AsBag().Inverse().Cascade.AllDeleteOrphan().Fetch.Select().BatchSize(80);

Anyway...

Your association seems to be diabolically complex. If ProjectsTags table should contains only id of tag and id of project, then it would be simpler to use FNH many-to-many bidirectional mapping:

public class Tag2Map : ClassMap<Tag2>
{
    public Tag2Map()
    {
        Id(x => x.Id);
        Map(x => x.Name);
        HasManyToMany(x => x.Projects)
            .AsBag()
            .Cascade.None()
            .Table("ProjectsTags")
            .ParentKeyColumn("TagId")
            .ChildKeyColumn("ProjectId");
    }
}

public class Project2Map : ClassMap<Project2>
{
    public Project2Map()
    {
        Id(x => x.Id);
        Map(x => x.Name);
        HasManyToMany(x => x.Tags)
            .AsBag()
            .Cascade.None()
            .Inverse()
            .Table("ProjectsTags")
            .ParentKeyColumn("ProjectId")
            .ChildKeyColumn("TagId");
    }
}

Now there is no need for ProjectTag entity in the model. The count of how many times is given tag used can be retrieved in two ways:

Direct way: tag.Projects.Count() - but it retrieves all projects from database.

Query way:

var tag = session.Get<Tag2>(tagId);
var count = session.Query<Project2>().Where(x => x.Tags.Contains(tag)).Count();