4

I have two simple classes which reference each other as a one-to-many relationship defined below:

public class Project
{
    public virtual int Id { get; set; }
    public virtual string Name { get; set; }
    public virtual IList<Document> Documents { get; set; }
}

public class Document
{
    public virtual int Id { get; set; }
    public string FileName { get; set; }
}

And my mappings are defined as:

public class ProjectMapping : ClassMap<Project>
{
    public ProjectMapping()
    {
        Table("Projects");
        Id(x => x.Id).Column("Project_Id").GeneratedBy.TriggerIdentity();
        HasMany(x => x.Documents)
            .Table("Documents")
            .KeyColumn("Document_Project_Id")
            .Cascade.AllDeleteOrphan()
            .Not.KeyNullable();
        Map(x => x.Name).Column("Project_Name");
    }
}

public class DocumentMapping : ClassMap<Document>
{
    public DocumentMapping()
    {
        Table("Documents");
        Id(x => x.Id).Column("Document_Id").GeneratedBy.TriggerIdentity();
        Map(x => x.FileName).Column("Document_File_Name");
    }
}

Everything seems to be working fine, adding/updating documents and calling session.Save(project) reflects the correct changes in my database, however if I am to delete a document from a list of documents associated with a project and call session.Save(project) the deleted document never gets deleted from the database.

Any ideas why everything else would work except for delete?

EDIT: My MVC 4 project is set up with Fluent NHibernate as follows:

public class SessionFactoryHelper
{
    public static ISessionFactory CreateSessionFactory()
    {
        var c = Fluently.Configure();
        try
        {
            //Replace connectionstring and default schema
            c.Database(OdbcConfiguration.MyDialect.
                ConnectionString(x =>
                x.FromConnectionStringWithKey("DBConnect"))
                .Driver<NHibernate.Driver.OdbcDriver>()
                .Dialect<NHibernate.Dialect.Oracle10gDialect>())
                .ExposeConfiguration(cfg => cfg.SetProperty("current_session_context_class", "web"));
            c.Mappings(m => m.FluentMappings.AddFromAssemblyOf<Project>());
            c.Mappings(m => m.FluentMappings.AddFromAssemblyOf<Document>());
        }
        catch (Exception ex)
        {
            Log.WriteLine(ex.ToString());
        }
        return c.BuildSessionFactory();
    }
}

public class MvcApplication : System.Web.HttpApplication
{
    public static ISessionFactory SessionFactory { get; private set; }

    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        WebApiConfig.Register(GlobalConfiguration.Configuration);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        AuthConfig.RegisterAuth();

        SessionFactory = SessionFactoryHelper.CreateSessionFactory();
    }

    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        var session = SessionFactory.OpenSession();
        CurrentSessionContext.Bind(session);
    }

    protected void Application_EndRequest(object sender, EventArgs e)
    {
        var session = CurrentSessionContext.Unbind(SessionFactory);
        session.Dispose();
    }
}

My repository is defined as follows:

public class Repository<T> : IRepository<T>
{
    public virtual ISession Session
    {
        get { return MvcApplication.SessionFactory.GetCurrentSession(); }
    }

    public T FindById(int iId)
    {
        return Session.Get<T>(iId);
    }

    public void Save(T obj)
    {
        using (var transaction = Session.BeginTransaction())
        {
            try
            {
                Session.Save(obj);
                transaction.Commit();
            }
            catch (Exception ex)
            {
                transaction.Rollback();

                Log.WriteLine(ex.ToString());
            }
        }
    }

    public T SaveOrUpdate(T obj)
    {
        using (var transaction = Session.BeginTransaction())
        {
            try
            {
                Session.SaveOrUpdate(obj);
                transaction.Commit();
            }
            catch (Exception ex)
            {
                transaction.Rollback();

                Log.WriteLine(ex.ToString());
            }
        }

        return obj;
    }

    public T Update(T obj)
    {
        using (var transaction = Session.BeginTransaction())
        {
            try
            {
                Session.Update(obj);
                transaction.Commit();
            }
            catch (Exception ex)
            {
                transaction.Rollback();

                Log.WriteLine(ex.ToString());
            }
        }

        return obj;
    }
}

I have 2 Actions defined in my ProjectsController as follows:

private IRepository<Project> repository;

public ProjectsController()
{
    repository = new Repository<Project>();
}

public ActionResult Edit(int iId)
{
    Project project = repository.FindById(iId);

    if (project == null)
        return HttpNotFound();

    return View(project);
}

[HttpPost]
public ActionResult Edit(Project project)
{
    project = repository.Update(project);

    return View(project);
}

If I am to delete a document in my first action (without HttpPost):

project.Documents.RemoveAt(0);
repository.Update(project);

The correct row is removed from the database. However, if I am to do the very same in the action with HttpPost attribute, the row is never removed.

Also I should note that if I add a document to project.Documents in the action with HttpPost attribute, repository.Update(project) successfully adds the row with the correct foreign key reference to the project. This is only failing when I remove a document.

2 Answers2

3

Have you tried adding .Inverse to your HasMany mapping?

Also, I'm not familiar with the Not.KeyNullable. I don't think it's necessary here.

Origin
  • 1,943
  • 13
  • 33
2

The cascade setting seems to be correct. The issue mentioned could be elsewhere:

however if I am to delete a document from a list of documents associated with a project

Suspected to me is a session flush mode, or missing explicit call to update parent entity Project, which was previously detached. Assure:

First, that the Flush() was called. In case that project instance is still kept in Session, the default behavior of flushing could be changed. (e.g. session.FlushMode = FlushMode.Never; or Commit without having transaction...)

// 1) checking the explicit Flush()
project.Documents.Remove(doc);
Session.Flush(); // this will delete that orphan

The second could be evicted project instance, needing the explicit update call

// 2) updating evicted project instance
project.Documents.Remove(doc);
Session.Update(project);
//Session.Flush(); // Session session.FlushMode = FlushMode.Auto

The setting inverse will in this case (only) help to reduce one trip to database with UPDATE statement, resetting reference to doc.Project = null, then executing DELETE.

Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
  • I've updated my question with hopefully some more helpful information. I could not get any of your suggestions to work. Do you have any more suggestions? – Christopher.Cubells Nov 27 '12 at 17:28
  • As an issue I see that in `HttpPost` the MVC run-time instantiate new Project instance for you and binds it with FORM values. I would suggest to `Get()` an instance from NHibernate session by `Id` and then bind it. is this possible in your case? Because right now is NHibernate provided with **new** collection, no information about removed items... – Radim Köhler Nov 27 '12 at 18:00
  • Do you have any suggestions on how to do this? If I try `Project entity = repository.FindById(project.Id); entity = project; repository.Update(entity);` I am getting errors: **a different object with the same identifier value was already associated with the session: 1, of entity** – Christopher.Cubells Nov 27 '12 at 18:09
  • If I take away `.Not.KeyNullable()` from my mapping, upon deleting a document from the project simply changes the foreign key 'Document_Project_Id' to null and disassociates the document from the project. This is not what I want, I want the document row to be deleted entirely. Am I missing something in my mapping? – Christopher.Cubells Nov 27 '12 at 18:22
  • Statement `entiy = project;` will just change the target of the *entity* reference (to project), while the original instance will still be in the session. Try to bind entity received from the session `entity = FindById(id);` `UpdateModel(entity);` `Repository.Udpate(entity);` – Radim Köhler Nov 27 '12 at 18:25
  • I was able to solve this (using your suggestion) by executing the following in my HttpPost action: `Project entity = (Project)repository.Merge(project); repository.Update(entity);` Thank you for your help. – Christopher.Cubells Nov 27 '12 at 18:35