Removing content item doesn't remove related part records

Topics: Administration, General, Troubleshooting, Writing modules
Developer
May 25, 2011 at 10:39 PM
Edited May 25, 2011 at 10:42 PM

I noticed that after deleting a content item (the ordinary way - via IContentManager) all the attached content part records still sit in the database. After some time it makes a huge amount of orphaned records that need to be cleaned up by hand. And also it creates issues with parts having unique columns (so recreating an item with the same parameters is not possible).

StorageFilter applied to IRepository for given content part doesn't seem to deal with OnRemoving/OnRemoved event at all.

Is this a normal behavior? It rather looks like an issue to me.

May 25, 2011 at 10:44 PM

Yeah, I noticed this also while trying to develop a Forum module, while verifying that my removal methods for the individual parts were properly removing them.

Coordinator
May 25, 2011 at 10:47 PM

This is because modules need to apply the [CascadeAllDeleteOrphan] attribute to any relationship trigger orphan records. It's done by default on ContentItemRecord, but might be forgotten on some others. In what modules have you seen that ?

Developer
May 25, 2011 at 10:56 PM

In my own:) I haven't tracked the problem deeper and checked other modules, but will do.

I use only ContentPartRecord's for storing content part data - nothing fancy (simple properties, no relation to other records). ContentPartRecord has [CascadeAllDeleteOrphan] applied too, so somehow this isn't working as it should.

Coordinator
May 25, 2011 at 11:40 PM
Edited May 25, 2011 at 11:40 PM

Could it be a difference between the idea of Remove and Delete? When you remove a content item through ContentManager it's clearing the Published and Latest flags on all versions, leaving the records intact but transparent to any query. But it's like a recycle bin more than a record delete - the data is still there in case removing content isn't the end of your workflow. It's the same general idea as the version records left behind when you publish versionable matierial - the older version records are neither Latest nor Published, but they are still physically present for scenarios like wiki, historic auditing, revert-to-past, etc.

Part of this is my background in a financial news site kicking in, :), for legal reasons it's critical. When you are accused of having published (or repeated) certain text online at a previous date it's vital that your systems enable you to repudiate those claims no matter how heavily edited, or renamed, or removed the content has been since then.

I think what you're missing is an additional module that deletes old version records on existing content, and deletes removed content altogether.

 

Developer
May 26, 2011 at 12:27 AM

Ok, I get it - you're right:)

But there should be a possibility to also permanently Delete a content item in case you just doesn't need it. It's a must-have in many scenarios (eg. if you want to have a unique constraint on some part record's column). Unfortunately the ContentManager doesn't provide such functionality. The best way now would be to plug in to content handler's OnRemoved event and directly call ContentItem repository Delete method (which would fire a cascade).

Maybe the better option would be to provide a patch that would add Delete method to the content manager?

Jun 2, 2011 at 3:10 PM
Edited Jun 2, 2011 at 3:12 PM

I don't think you should have any unique constraints on a ContentPartRecord - it creates potential problems with versioning and localization (since I believe a complete copy of the content item is created for each locale, and its certainly the case if you wanted to convert your part to respect versioning at any point). The problem is when you're dealing with modular content (as opposed to a tightly-controlled business model) you can never guarantee that duplicated values won't be desired by someone at some point!

Aug 3, 2011 at 11:43 AM

I am new to Orchard and have been developing my own module.  I have been reading the documentation and found this concept interesting.  From what I gather Remove works exactly as loudej says.  All of the records are still in the database.  The code essentially just marks the version to not latest and not published.  So this leaves me with a concern and a question.

The concern is the size of the database over time.  This could be a problem on hosted environments where space is expensive.  Part of this concern is probably because of some of my background where my work has been on devices that have very little storage, so cleanup was always a must and records were moved to a central database for historical purposes (and a job ran to clean the historical data every so often).  Most modules it probably would not matter for most sites, but something like Taxonomies could have a lot of removed but existing records potentially.

The question is how do you fully delete these items?  I am not sure if the delete is possible within the framework (I have not specifically looked for this in the code and the code is complex so it takes a bit of time to get through all of the pieces) or if you need to do some database work directly or through nHibernate specifically.

There is a Version Manager module which says it only works in 1.1, but it is working fine for me in the latest release.  I am not sure if it is in active development or not, but it has a recycle bin (a tab under content) and you can see the number of versions and item has and can publish an older one if desired.

I admit I was a bit dumbfounded when I wired up my delete and checked the database and saw all of the records still there, but the item removed from the list.  I think tried it on other modules and it was the same result, which lead me to this discussion and explanation.

Coordinator
Aug 4, 2011 at 12:03 AM

Well, disk space nowadays is really cheap and even on hosted plans you would likely have to work for a long long time on your site to reach your host's space limits. *if* space becomes a concern, someone could write a module that does cleanup. They would probably have to do some dirty stuff directly with nHibernate but it should be fairly simple. But in general, soft deletes are increasingly common because they offer a number of advantages.

Jul 10, 2012 at 1:24 PM

Hey,

Did you guys ever find out how to hard delete content items? (I have a many to many relation between 2 content items) and I'm running into lot's of trouble because the records aren't deleted from the database and also off course not from the junction table)

How can I solve this?

Borrie

Jul 10, 2012 at 2:22 PM
bertrandleroy wrote:

Well, disk space nowadays is really cheap and even on hosted plans you would likely have to work for a long long time on your site to reach your host's space limits. *if* space becomes a concern, someone could write a module that does cleanup. They would probably have to do some dirty stuff directly with nHibernate but it should be fairly simple. But in general, soft deletes are increasingly common because they offer a number of advantages.

So you're saying that because we can neglect the amount of space required to keep these records, we should ignore the fact that it will not help queries vs such tables?

Afaik a query vs a table with 100 rows or 10000 rows I can safely say that querying vs the first will be faster no?

So yes, a module could be nice for this, but it should be a core module imho..

Jul 10, 2012 at 3:28 PM
Edited Jul 10, 2012 at 3:29 PM

OMG!!!

I've found it! :)

Ok, I'll explain it to you noobs like me out there :)

First you have to reference your module into Orchard.core (right click, browse to bin folder of your module and add the dll)

Goto Orchard.Core.Contents.Controllers.AdminController

Add: using YourmoduleName.Models;

Add your repositories, for example:

 

public class AdminController : Controller, IUpdateModel {
        private readonly IContentManager _contentManager;
        private readonly IContentDefinitionManager _contentDefinitionManager;
        private readonly ITransactionManager _transactionManager;
        private readonly ISiteService _siteService;
        private readonly IRepository _schoolPartRecordRepository;
        private readonly IRepository _employeePartRecordRepository;

        public AdminController(
            IOrchardServices orchardServices,
            IContentManager contentManager,
            IContentDefinitionManager contentDefinitionManager,
            ITransactionManager transactionManager,
            ISiteService siteService,
            IShapeFactory shapeFactory,
            IRepository schoolPartRecordRepository,
            IRepository employeePartRecordRepository)
        {
            Services = orchardServices;
            _contentManager = contentManager;
            _contentDefinitionManager = contentDefinitionManager;
            _transactionManager = transactionManager;
            _siteService = siteService;
            T = NullLocalizer.Instance;
            Logger = NullLogger.Instance;
            Shape = shapeFactory;
            _schoolPartRecordRepository = schoolPartRecordRepository;
            _employeePartRecordRepository = employeePartRecordRepository;
        }

Then in the remove section I've added this code:

 [HttpPost]
        public ActionResult Remove(int id, string returnUrl) {
            var contentItem = _contentManager.Get(id, VersionOptions.Latest);

            if (!Services.Authorizer.Authorize(Permissions.DeleteContent, contentItem, T("Couldn't remove content")))
                return new HttpUnauthorizedResult();

            if (contentItem != null) {
                _contentManager.Remove(contentItem);
                if (contentItem.ContentType.ToString() == "School" ){
                    var school = _schoolPartRecordRepository.Get(id);
                    _schoolPartRecordRepository.Delete(school);
                }

                if (contentItem.ContentType.ToString() == "Employee")
                {
                    varemployee = _employeePartRecordRepository.Get(id);
                    _employeePartRecordRepository.Delete(employee);
                }

                Services.Notifier.Information(string.IsNullOrWhiteSpace(contentItem.TypeDefinition.DisplayName)
                    ? T("That content has been removed.")
                    : T("That {0} has been removed.", contentItem.TypeDefinition.DisplayName));
            }

            return this.RedirectLocal(returnUrl, () => RedirectToAction("List"));
        }
So now the records are deleted from the database and also from the junction table!

I'm happy! :)

Borrie
Coordinator
Jul 10, 2012 at 4:53 PM

If you want to be happier, please don't change the Core, but create a Content Handler with OnRemove(). When it's triggered do the same thing.

Jul 10, 2012 at 6:09 PM

Sebastien,

Tx for the tip, I'll do that, this was just to see if it would work, I also need to include some more errorhandling.

Borrie

Jul 10, 2012 at 6:39 PM

Sebastien,

I was looking at my content handler but how can I get the id of the record I want to delete from the repository with the OnRemove() function?

I'm sorry, i'm very new to Orchard.

Borrie

    public class SchoolHandler : ContentHandler
    {
        public SchoolHandler(IRepository schoolPartRepository)
        {
            Filters.Add(StorageFilter.For(schoolPartRepository));
         }

       
    }
}

Developer
Jul 10, 2012 at 9:04 PM
Edited Jul 10, 2012 at 9:50 PM

Altering the core code is in most cases not the good idea. You can do almost everything using Orchard extension points, like eg. ContentHandlers. That being said, when you really think you need to alter the core, rethink it - you'll usually find a simpler solution:)

This is how it should look like. You should simply call Delete method of appropriate repository. The related content item record will be removed too (through cascade), so the whole item will cease to exist.

You can also add appropriate error handling etc. within the OnRemoved handler.

public class SchoolHandler : ContentHandler
{
        public SchoolHandler(IRepository<SchoolPartRecord> schoolPartRepository)
        {
            Filters.Add(StorageFilter.For(schoolPartRepository));
            OnRemoved<SchoolPart>((context, part) => {
                schoolPartRepository.Delete(part.Record);
            })
        }
}
Jul 10, 2012 at 9:24 PM

pszmyd,

Thanks for your answer but I get an error on part =>

Error    1    Delegate 'System.Action<Orchard.ContentManagement.Handlers.RemoveContentContext,CLB.SchoolModule.Models.SchoolPart>' does not take 1 arguments  

It doesn't seem to recognize part.

Borrie

Developer
Jul 10, 2012 at 9:30 PM

Sorry, there should be 2 params - context and part for OnRemoved action. Will fix the snippet above,

Jul 10, 2012 at 9:47 PM

pszmyd,

It wouldn't recognize context now so I added extra hooks:

public class SchoolHandler : ContentHandler
    {
        public SchoolHandler (IRepository<SchoolPartRecord> schoolPartRepository)
        {
            Filters.Add(StorageFilter.For(schoolPartRepository));
            OnRemoved<SchoolPart>((context, part) =>
            {
                schoolPartRepository.Delete(part.Record);
            });
        }
    }

Now I get no errors but the records are still there after I remove an item, do you know what can be wrong?

Borrie

Jul 11, 2012 at 9:16 AM

pszmyd,

Today it's workingn maybe yesterday I had a DB problem...

So this code works:

public class SchoolHandler : ContentHandler
    {
        public SchoolHandler (IRepository schoolPartRepository)
        {
            Filters.Add(StorageFilter.For(schoolPartRepository));
            OnRemoved((context, part) =>
            {
                schoolPartRepository.Delete(part.Record);
            });
        }
    }

So please ignore my post where I was changing the core :)

Thanks agian for all the help!

Borrie