Question

I have a many-to-many relationship between two objects (Application, Query). I have constructed the map to have a HasManyToMany mapping in both object maps.

The first time I use the SaveOrUpdate the application, it works fine, and the entry is placed correctly in the join table.

However, the second time I use it, I get the error: 'a different object with the same identifier was already associated with the session'.

The exception is thrown in the last couple of lines of the addQuery code shown below.

Here are the entities and the maps. Any suggestions would be greatly appreciated.

public class Application
{
    public virtual int id { get; set; }
    public virtual string name { get; set; }
    public virtual IList<Query> queries { get; set; }

    public Application()
    {
        this.queries = new List<Query>();
    }

    public virtual void AddQuery(Query qry)
    {
        qry.applicationsUsedIn.Add(this);
        queries.Add(qry);
    }
}


public class Query
{
    public virtual int id { get; protected set; }
    public virtual string name { get; set; }
    public virtual string query { get; set; }
    public virtual IList<QueryParameters> parameters { get; set; }
    public virtual IList<Application> applicationsUsedIn { get; set; }

    public Query()
    {
        this.parameters = new List<QueryParameters>();
        this.applicationsUsedIn = new List<Application>();
    }

    public virtual void AddParameter(QueryParameters qp)
    {
        qp.query = this;
        this.parameters.Add(qp);
    }
}


public class ApplicationMap : ClassMap<Application>
{
    public ApplicationMap()
    {
        Table("dbo.Applications");
        Id(x => x.id).Column("id");
        Map(x => x.name).Column("name");
        HasManyToMany(x => x.queries)
            .Table("dbo.ApplicationsQueries")
            .ParentKeyColumn("appid")
            .ChildKeyColumn("qryid")
            .Not.LazyLoad()
            .Cascade.SaveUpdate();
    }
}


public class QueryMap : ClassMap<Query>
{
    public QueryMap()
    {
        Table("dbo.Queries");
        Id(x => x.id);
        Map(x => x.name);
        Map(x => x.query);
        HasMany(x => x.parameters)
            .Cascade.All()
            .Inverse();
        HasManyToMany(x => x.applicationsUsedIn)
            .Table("dbo.ApplicationsQueries")
            .ParentKeyColumn("qryid")
            .ChildKeyColumn("appid")
            .Inverse()
            .Cascade.SaveUpdate()
            .Not.LazyLoad();
    }
}


    public void addQuery(string appname, string qryname, string qrystr)
    {
        Application app = getApplication(appname);
        if (null == app)
        {
            app = addApplication(appname);
        }

        Query qry = getQuery(appname, qryname);

        if (null == qry)
        {
            using (ISessionFactory isf = getSessionFactory())
            {
                using (var sess = isf.OpenSession())
                {
                    using (var tran = sess.Transaction)
                    {
                        tran.Begin();

                        qry = new Query();
                        qry.name = qryname;
                        qry.query = qrystr;
                        sess.Save(qry);

                        tran.Commit();
                    }
                }
            }
        }

        if (!app.queries.Contains(qry))
        {
            using (ISessionFactory isf = getSessionFactory())
            {
                using (var sess = isf.OpenSession())
                {
                    using (var tran = sess.Transaction)
                    {
                        tran.Begin();

                        app.AddQuery(qry);

                        //This is where the exception is thrown
                        sess.SaveOrUpdate(app);

                        tran.Commit();
                    }
                }
            }
        }
    }


UPDATED CODE in case it helps someone else

    public ApplicationMap()
    {
        Table("dbo.Applications");
        Id(x => x.id).Column("id");
        Map(x => x.name).Column("name");
        HasManyToMany(x => x.queries)
            .Table("dbo.ApplicationsQueries")
            .ParentKeyColumn("appid")
            .ChildKeyColumn("qryid")
            .LazyLoad();
    }

    public QueryMap()
    {
        Table("dbo.Queries");
        Id(x => x.id);
        Map(x => x.name);
        Map(x => x.query);
        HasMany(x => x.parameters)
            .Cascade.All()
            .Inverse();
        HasManyToMany(x => x.applicationsUsedIn)
            .Table("dbo.ApplicationsQueries")
            .ParentKeyColumn("qryid")
            .ChildKeyColumn("appid")
            .Inverse()
            .LazyLoad();
    }

    public void addQuery(string appname, string qryname, string qrystr)
    {
        using (ISessionFactory isf = getSessionFactory())
        {
            using (var sess = isf.OpenSession())
            {
                using (var tran = sess.Transaction)
                {
                    tran.Begin();

                    var critapp = sess.CreateCriteria<Application>()
                        .Add(Restrictions.Eq("name", appname));

                    Application app = (Application)critapp.UniqueResult();

                    if (null == app)
                    {
                        app = new Application();
                        app.name = appname;
                        sess.Save(app);
                    }

                    var critqry = sess.CreateCriteria<Query>()
                        .Add(Restrictions.Eq("name", qryname));

                    Query qry = (Query)critqry.UniqueResult();

                    if (null == qry)
                    {
                        qry = new Query();
                        qry.name = qryname;
                        qry.query = qrystr;
                        sess.Save(qry);
                    }

                    if (!app.queries.Contains(qry))
                    {
                        app.AddQuery(qry);
                    }

                    tran.Commit();
                }
            }
        }
    }
Was it helpful?

Solution

The problem here is hidden in many opened sessions for (in fact) one operation. This is not the correct way how to execute this insert/updates. We should always wrap a set of operations relying on each other, into ONE session, one transaction.

So what happened is that here we've got the Query recieved like this:

Query qry = getQuery(appname, qryname);

We have an object, which is (was) part of the session, which just ran out of the scope. The qry instance is fully populated now because

  • it is an existing object (loaded from DB)...
  • the mapping of the collection IList<Application> applicationsUsedIn (see your mapping) is .Not.LazyLoad()
  • and the same is true for other Not.LazyLoad() mappings...

So once we come to the last transaction (and its own session) ... our objects could be deeply populated... having the same IDs as loaded objects inside of that last session

To solve the issue quickly:

  1. Open the session at the begining of the operation
  2. Open the transaction at the begining
  3. Call Save() only, if we do have new object
  4. For objects retrieved via getQuery(), getApplication() (beeing in the same session) DO NOT call the SaveOrUpdate(). They are already in the session, and that's what the SaveOrUpdated in this case mostly does (put them in the session)
  5. a) Call the transaction.Commit() and all the stuff will be properly persisted
    b) Close the session at the end of operation

Note: I would change the mapping of your many-to-many

  • remove the Not.LazyLoad(). Lazy is what we mostly want..
  • remove Cascade because it is about the other end not about the pairing table. If this was intended then leave it
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top