Skip to content

Commit 913b68d

Browse files
committed
Merge branch 'feature/unresolved-reference-handling' into develop
When recursively saving a topic graph, it is possible for a topic being saved to reference a new topic in the graph that hasn't yet been saved. This prevents it from resolving that reference. I.e., while the _object model_ is able to model that reference, it can't be _persisted_ to the underlying SQL database since the target topic doesn't yet have a `Topic.Id`. Previously, this required calling `Save()` twice in this scenario. Now, a new private overload allows a `List<Topic>` to be maintained of `unresolvedTopics`. Then, once the entire graph has been saved, `Save()` will save each individual topic with unresolved references, thus ensuring that they are properly persisted. This works for both `Topic.Relationships` and `Topic.DerivedTopic` references. There are limitations to this implementation. It isn't able to resolve _implicit_ topic pointers (i.e., attributes with an `Id` extension) since those aren't mapped via the object model, but rather runtime interpretation. As such, when working with the **OnTopic Data Transfer** library, which includes special handling for implicit topic pointers, `Save()` must be called twice (alongside `Import()`). That's expected, and there isn't really a clean workaround. Second, obviously any references that are outside the scope of the current recursive `Save()` operation will continue to be unresolved. This shouldn't happen in most use cases, but could occur with some custom configuration scripts. That's also expected, and not something we plan on resolving. Still, this addresses the most common and otherwise unintuitive issue, without adding much overhead to most `Save()` operations. (Obviously, the overhead will depend on how many unresolved references there are.) While I was at it, I also added support for _explicit_ connection handling on recursive saves. This probably provide minimal improvement over performance, but it prevents the need to open and close a connection for every topic on a recursive `Save()` of a large topic graph.
2 parents d64d242 + 9303d80 commit 913b68d

1 file changed

Lines changed: 84 additions & 9 deletions

File tree

OnTopic.Data.Sql/SqlTopicRepository.cs

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
| Project Topics Library
55
\=============================================================================================================================*/
66
using System;
7+
using System.Collections.Generic;
78
using System.Data;
89
using System.Data.SqlTypes;
910
using System.Diagnostics.CodeAnalysis;
@@ -264,6 +265,68 @@ public override Topic Load(int topicId, DateTime version) {
264265
/// <inheritdoc />
265266
public override int Save([NotNull]Topic topic, bool isRecursive = false, bool isDraft = false) {
266267

268+
/*------------------------------------------------------------------------------------------------------------------------
269+
| Establish dependencies
270+
\-----------------------------------------------------------------------------------------------------------------------*/
271+
var unresolvedTopics = new List<Topic>();
272+
var connection = new SqlConnection(_connectionString);
273+
274+
connection.Open();
275+
276+
/*------------------------------------------------------------------------------------------------------------------------
277+
| Handle first pass
278+
\-----------------------------------------------------------------------------------------------------------------------*/
279+
var topicId = Save(topic, isRecursive, isDraft, connection, unresolvedTopics);
280+
281+
/*------------------------------------------------------------------------------------------------------------------------
282+
| Attempt to resolve outstanding relationships
283+
\-----------------------------------------------------------------------------------------------------------------------*/
284+
foreach (var unresolvedTopic in unresolvedTopics) {
285+
Save(unresolvedTopic, false, isDraft, connection, new List<Topic>());
286+
}
287+
288+
/*------------------------------------------------------------------------------------------------------------------------
289+
| Return value
290+
\-----------------------------------------------------------------------------------------------------------------------*/
291+
connection.Close();
292+
connection.Dispose();
293+
return topicId;
294+
295+
}
296+
297+
/// <summary>
298+
/// The private overload of the <see cref="Save"/> method provides support for sharing the <see cref="SqlConnection"/>
299+
/// between multiple requests, and maintaining a list of <paramref name="unresolvedRelationships"/>.
300+
/// </summary>
301+
/// <remarks>
302+
/// <para>
303+
/// When recursively saving a topic graph, it is conceivable that references to other topics—such as <see
304+
/// cref="Topic.Relationships"/> or <see cref="Topic.DerivedTopic"/>—can't yet be persisted because the target <see
305+
/// cref="Topic"/> hasn't yet been saved, and thus the <see cref="Topic.Id"/> is still set to <c>-1</c>. To mitigate
306+
/// this, the <paramref name="unresolvedRelationships"/> allows this private overload to keep track of unresolved
307+
/// relationships. The public <see cref="Save(Topic, Boolean, Boolean)"/> overload uses this list to resave any topics
308+
/// that include such references. This adds some overhead due to the duplicate <see cref="Save"/>, but helps avoid
309+
/// potential data loss when working with complex topic graphs.
310+
/// </para>
311+
/// <para>
312+
/// The connection sharing probably doesn't provide that much of a gain in that .NET does a good job of connection
313+
/// pooling. Nevertheless, there is some overhead to opening a new connection, so sharing an open connection when we
314+
/// doing a recursive save could potentially provide some performance benefit.
315+
/// </para>
316+
/// </remarks>
317+
/// <param name="topic">The source <see cref="Topic"/> to save.</param>
318+
/// <param name="isRecursive">Determines whether or not to recursively save <see cref="Topic.Children"/>.</param>
319+
/// <param name="isDraft">Determines if the <see cref="Topic"/> should be saved as a draft version.</param>
320+
/// <param name="connection">The open <see cref="SqlConnection"/> to use for executing <see cref="SqlCommand"/>s.</param>
321+
/// <param name="unresolvedRelationships">A list of <see cref="Topic"/>s with unresolved topic references.</param>
322+
private int Save(
323+
[NotNull]Topic topic,
324+
bool isRecursive,
325+
bool isDraft,
326+
SqlConnection connection,
327+
List<Topic> unresolvedRelationships
328+
) {
329+
267330
/*------------------------------------------------------------------------------------------------------------------------
268331
| Call base method - will trigger any events associated with the save
269332
\-----------------------------------------------------------------------------------------------------------------------*/
@@ -355,19 +418,31 @@ void addUnmatchedAttribute(string key) {
355418
| Establish database connection
356419
\-----------------------------------------------------------------------------------------------------------------------*/
357420
var isNew = topic.Id == -1;
358-
var connection = new SqlConnection(_connectionString);
359421
var procedureName = isNew? "CreateTopic" : "UpdateTopic";
360422
var command = new SqlCommand(procedureName, connection) {
361423
CommandType = CommandType.StoredProcedure
362424
};
363425
var version = new SqlDateTime(DateTime.Now);
426+
var areReferencesResolved = true;
427+
428+
/*------------------------------------------------------------------------------------------------------------------------
429+
| Handle unresolved references
430+
>-------------------------------------------------------------------------------------------------------------------------
431+
| If it's a recursive save and there are any unresolved relationships, come back to this after the topic graph has been
432+
| saved; that ensures that any relationships within the topic graph have been saved and can be properly persisted. The
433+
| same can be done for DerivedTopics references, which are effectively establish a 1:1 relationship.
434+
\-----------------------------------------------------------------------------------------------------------------------*/
435+
if (isRecursive && (topic.DerivedTopic?.Id < 0 || topic.Relationships.Any(r => r.Any(t => t.Id < 0)))) {
436+
unresolvedRelationships.Add(topic);
437+
areReferencesResolved = false;
438+
}
364439

365440
/*------------------------------------------------------------------------------------------------------------------------
366441
| Establish query parameters
367442
\-----------------------------------------------------------------------------------------------------------------------*/
368443
if (!isNew) {
369444
command.AddParameter("TopicID", topic.Id);
370-
command.AddParameter("DeleteRelationships", true);
445+
command.AddParameter("DeleteRelationships", areReferencesResolved);
371446
}
372447
else if (topic.Parent != null) {
373448
command.AddParameter("ParentID", topic.Parent.Id);
@@ -382,7 +457,6 @@ void addUnmatchedAttribute(string key) {
382457
\-----------------------------------------------------------------------------------------------------------------------*/
383458
try {
384459

385-
connection.Open();
386460
command.ExecuteNonQuery();
387461

388462
topic.Id = command.GetReturnCode();
@@ -392,7 +466,9 @@ void addUnmatchedAttribute(string key) {
392466
"The call to the CreateTopic stored procedure did not return the expected 'Id' parameter."
393467
);
394468

395-
PersistRelations(topic, connection, true);
469+
if (areReferencesResolved) {
470+
PersistRelations(topic, connection, true);
471+
}
396472

397473
topic.VersionHistory.Insert(0, version.Value);
398474

@@ -402,6 +478,7 @@ void addUnmatchedAttribute(string key) {
402478
| Catch exception
403479
\-----------------------------------------------------------------------------------------------------------------------*/
404480
catch (SqlException exception) {
481+
connection?.Dispose();
405482
throw new TopicRepositoryException(
406483
$"Failed to save Topic '{topic.Key}' ({topic.Id}) via '{_connectionString}': '{exception.Message}'",
407484
exception
@@ -413,7 +490,6 @@ void addUnmatchedAttribute(string key) {
413490
\-----------------------------------------------------------------------------------------------------------------------*/
414491
finally {
415492
command?.Dispose();
416-
connection?.Dispose();
417493
attributes.Dispose();
418494
}
419495

@@ -423,7 +499,7 @@ void addUnmatchedAttribute(string key) {
423499
if (isRecursive) {
424500
foreach (var childTopic in topic.Children) {
425501
childTopic.Attributes.SetValue("ParentID", topic.Id.ToString(CultureInfo.InvariantCulture));
426-
Save(childTopic, isRecursive, isDraft);
502+
Save(childTopic, isRecursive, isDraft, connection, unresolvedRelationships);
427503
}
428504
}
429505

@@ -633,14 +709,13 @@ private static string PersistRelations(Topic topic, SqlConnection connection, bo
633709
finally {
634710
command?.Dispose();
635711
targetIds.Dispose();
636-
//Since the SQL connection is being passed in, do not close connection; this allows command pooling.
712+
//Since the SQL connection is being passed in, do not close connection; this allows connection pooling.
637713
}
638714

639715
/*------------------------------------------------------------------------------------------------------------------------
640716
| Return the relationship attributes to append to the XML attributes (unless skipXml is set to true)
641717
\-----------------------------------------------------------------------------------------------------------------------*/
642-
if (skipXml) return "";
643-
else return CreateRelationshipsXml(topic);
718+
return skipXml? "" : CreateRelationshipsXml(topic);
644719

645720
}
646721

0 commit comments

Comments
 (0)