简体   繁体   中英

Many to Many delete cascade in NHibernate

I have a scenario in a system which I've tried to simplify as best as I can. We have a table of (lets call them) artefacts, artefacts can be accessed by any number of security roles and security roles can access any number of artefacts. As such, we have 3 tables in the database - one describing artefacts, one describing roles and a many-to-many association table linking artefact ID to Role ID.

Domain wise, we have two classes - one for a role and one for an artefact. the artefact class has an IList property that returns a list of roles that can access it. (Roles however do not offer a property to get artefacts that can be accessed).

As such, the nhibernate mapping for artefact contains the following;

<bag name="AccessRoles" table="ArtefactAccess" order-by="RoleID" 
    lazy="true" access="field.camelcase-underscore" optimistic-lock="false">
    <key column="ArtefactID"/>
    <many-to-many class="Role" column="RoleID"/>
</bag>

This all works fine and if I delete an artefact, the association table is cleaned up appropriately and all references between the removed artefact and roles are removed (the role isn't deleted though, correctly - as we don't want orphans deleted).

The problem is - how to delete a role and have it clear up the association table automatically. If I presently try to delete a role, I get a reference constraint as there are still entries in the association table for the role. The only way to successfully delete a role is to query for all artefacts that link to that role, remove the role from the artefact's role collection, update the artefacts and then delete the role - not very efficient or nice, especially when in the un-simplified system, roles can be associated with any number of other tables/objects.

I need to be able to hint to NHibernate that I want this association table cleared whenever I delete a role - is this possible, and if so - how do I do it?

Thanks for any help.

Since I was looking for this answer and found this thread on google (without an answer) I figured I'd post my solution to this. With three tables: Role, RolesToAccess(ManyToMany), Access.

Create the following mappings: Access:

<bag name="Roles" table="RolesToAccess" cascade="none" lazy="false">
      <key column="AccessId" />
      <many-to-many column="AccessId" class="Domain.Compound,Domain" />
    </bag>

<bag name="RolesToAccess" cascade="save-update" inverse="true" lazy="false">
      <key column="AccessId" on-delete="cascade" />
      <one-to-many class="Domain.RolesToAccess,Domain" />
    </bag>

Roles:

<bag name="Accesses" table="RolesToAccess" cascade="none" lazy="false">
      <key column="RoleId" />
      <many-to-many column="RoleId" class="Domain.Compound,Domain" />
    </bag>

<bag name="RolesToAccess" cascade="save-update" inverse="true" lazy="false">
      <key column="RoleId" on-delete="cascade" />
      <one-to-many class="Domain.RolesToAccess,Domain" />
    </bag>

As mentioned above you can make the RolesToAccess properties protected so they don't pollute your model.

What you say here:

The only way to successfully delete a role is to query for all artefacts that link to that role, remove the role from the artefact's role collection, update the artefacts and then delete the role - not very efficient or nice, especially when in the un-simplified system, roles can be associated with any number of other tables/objects.

Is not necessary. Suppose you don't want to map the association table (make it a domain object), you can still perform deletes on both ends with minimal code.

Let's say there are 3 tables: Role, Artifact, and ArtifactAccess (the link table). In your mapping, you only have domain objects for Role and Artifact. Both have a bag for the many-many association.

Role:

    <bag name="Artifacts" table="[ArtifactAccess]" schema="[Dbo]" lazy="true"
        inverse="false" cascade="none" generic="true">
        <key column="[ArtifactID]"/>

        <many-to-many column="[RoleID]" class="Role" />
    </bag>

Artifact:

    <bag name="Roles" table="[ArtifactAccess]" schema="[Dbo]" lazy="true"
        inverse="false" cascade="none" generic="true">
        <key column="[RoleID]"/>

        <many-to-many column="[ArtifactID]" class="Role" />
    </bag>

As you can see, both ends have inverse=false specified. The NHibernate documentation recommends you to choose one end of your association as the 'inverse' end, but nothing stops you from using both as 'controlling end'. When performing updates or inserts, this works from both directions without a hitch. When performing deletes of either one of the ends, you get a FK violation error because the association table is not updated, true. But you can solve this by just clearing the collection to the other end, before performing the delete, which is a lot less complex than what you do, which is looking in the 'other' end of the association if there are uses of 'this' end. If this is a bit confusing, here is a code example. If you only have one end in control, for your complex delete you need to do:

foreach(var artifact in role.Artifacts)
    foreach(var role in artifact.Roles)
        if(role == roleToDelete)
            artifact.Roles.Remove(role)
    artifact.Save();
roleToDelete.Delete();

What I do when deleting a role is something like

roleToDelete.Artifacts.Clear(); //removes the association record
roleToDelete.Delete(); // removes the artifact record

It's one extra line of code, but this way you don't need to make a decision on which end of the association is the inverse end. You also don't need to map the association table for full control.

You could make a mapping for the association table, and then call delete on that table where the Role_id is the value you are about to delete, and then perform the delete of the role itself. Should be fairly straightforward to do this.

Although I believe NHibernate must provide a way to do this without having the collection in the roles C# class, you can always set this behaviour in SQL. Select on cascade delete for the FK in the database and it should be automatic, just watch out for NHib's cache.

But I strongly advice you to use this as a last resource.

You need to create a mapping from Role to Artifact .

You can make it lazy-loading, and map it to a protected virtual member, so that it never actually gets accessed, but you need that mapping there for NHibernate to know that it has to delete the roles from the ArtefactAccess table

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM