We strive to provide our users with a swift and responsive user interface. No one likes waiting around for their computer to do something, especially if user input is not necessary. One such case would be when a user asks for something to be deleted. There is no need to have the user wait for the delete to finish. This work can be done in the background.
In the interest of increasing performance on the front-end, we utilize a seperate delete queue rather than deleting entities in the user’s session. Deletions seem almost instantaneous to the user even if they take a while due to cascading deletes and the creation of numerous audit logs. Of course we don't want the deleted entities showing up in the user interface while the delete queue is doing it's work. So, we just need to filter the results so we do not present any entities which have been marked for deletion. What we are looking for is a way to perform a soft delete with the ability to find deleted entities when necessary; in the delete queue for instance.
In the interest of increasing performance on the front-end, we utilize a seperate delete queue rather than deleting entities in the user’s session. Deletions seem almost instantaneous to the user even if they take a while due to cascading deletes and the creation of numerous audit logs. Of course we don't want the deleted entities showing up in the user interface while the delete queue is doing it's work. So, we just need to filter the results so we do not present any entities which have been marked for deletion. What we are looking for is a way to perform a soft delete with the ability to find deleted entities when necessary; in the delete queue for instance.
We use Nhibernate for our Data Access Layer (DAL). So, the brute force approach to this problem would be to add a where clause to
the end of each query. With the number of queries used by the system, this
seems to be a rather poor plan. Also, collections and foreign key relations
would not include the option to add a where clause. This approach would give us
great performance, since we would only need to perform the "where" where needed.
At the same time, there would be a good chance of missing a query.
The standard way to perform soft deletes in NHibernate seems
to be to add a where clause to the class tag in the mappings. This makes it much more difficult to access data marked for deletion but not yet deleted. With the
class mapping containing the where clause, no where will ever return a marked
entity. One could add a subclass with the where clause, allowing access to the deleted
data via the parent. Unfortunately, this also seemed to add unnecessary code and complexity.
So, in our research, we found another really nice feature of NHibernate. We discovered,
while working on the "where" filters, NHibernate filters. This feature “allows us to very easily
create global where clauses that we can flip on and off at the touch of a
switch.” Perfect! Just what we needed. Turn the filter on by default and then
turn it off as needed by simply calling session.DisableFilter. So we added the
filter to one object and enabled it in the OpenSession call.
The filter def:
<filter-def name="NorMarkedForDeletion"> </filter-def>
The code to add to each class:
<class name="User" table="Users"> ... <property name="MarkedForDeletion"> <filter condition="MarkedForDeletion <> 1" name="NotMarkedForDeletion"> </class>
Add the filter to the OpenSession call.
public static ISession OpenSession(string sessionName = "Nexport Session") { var session = SessionFactory.OpenSession(); session.FlushMode = FlushMode.Commit; if (AppSettingsKeys.Cluster.EnableNHibernateProfiler) HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.RenameSessionInProfiler(session, sessionName); session.EnableFilter("NotMarkedForDeletion"); return session; }
Nothing like an
elegant solution. But oh, look, about a third of the unit tests fail, with rather
strange and random error messages, when the test harness is run. Maybe this was
not the best solution.
After the filters approach failed and an attempt at fixing
the tests turns out to be not a quick fix, it is back to the drawing board.
Adding a where clause to the class mapping seemed to work rather well. It just made
it impossible for us to access the marked data afterwards, preventing us from
actually deleting it from the system. But if, instead, we create two instances of
the session factory, one with the where clause on the mappings, and the other without,
we would be able to control the visibility of marked objects. We are losing some of the flexibility we
would have had with the filters, allowing us to switch at any point in the
session, but we still have session by session control.
Just need to create two session factories:
private static ISessionFactory SessionFactory { get; set; } private static ISessionFactory DeleteSessionFactory { get; set; } public static void Init() { ... DeleteSessionFactory = cfg.BuildSessionFactory(); foreach (Type deleteMarkable in System.Reflection.Assembly.GetExecutingAssembly().GetTypes().Where(mytype => mytype.GetInterfaces().Contains(typeof(IDeleteMarkable)))) { var classMapping = cfg.GetClassMapping(deleteMarkable); try { classMapping.Where = "MarkedForDeletion != 1"; } catch (Exception ex) { } } SessionFactory = cfg.BuildSessionFactory(); }
One thing to touch on is cascading. When marking an entities for deletion, we want
to mark all children for deletion, too. However, at that point, just deleting all the child entities
would be just as fast. As such, we mark some, but not all, of the
children as deleted, as well. Some entities are impossible or at least almost
impossible to reach without the parent entity.
In the end, creating two session factories worked rather
well. A few tests failed at first, but they were quickly rectified by replacing
the default session with one that can access marked entities in a few places,
mostly in the audit logging triggered by a delete call on the object. It is still a
mystery as to why the filters caused the issues they did. Now we have a system which filters out objects marked for deletion such that the user is not able to see them, while still giving us the flexibility of NHibernate when deleting these objects. Furthermore, having other entities implement the deletable interface should now be even simpler. They need only to implement the interface and add the MarkedForDeletion property and column, and everything else will handled automatically.
These changes greatly increase the speed of deletions from the users' perspective. Rather than waiting for the entity and all of its associations to be deleted, a delete will now be nothing more then a few SQL update calls with everything else happening on the backend.
These changes greatly increase the speed of deletions from the users' perspective. Rather than waiting for the entity and all of its associations to be deleted, a delete will now be nothing more then a few SQL update calls with everything else happening on the backend.
About NexPort Solutions Group
NexPort Solutions Group is a division of Darwin Global, LLC, a systems and software engineering company that provides innovative, cost-effective training solutions and support for federal, state and local government, as well as the private sector.
0 comments :
Post a Comment