Skip to content

Commit fdd0025

Browse files
committed
Centralized core business logic from SqlTopicRepository.Save()
The `SqlTopicRepository.Save()` implementation had a lot of logic that wasn't specific to SQL Server, and would likely need to be repeated by subsequent implementations. To help avoid that, it has been migrated from `SqlTopicRepository.Save()` and into the underlying `TopicRepository.Save()`. This includes tracking `unresolvedTopics` (i.e., topics whose relationships or references haven't yet been saved) and then resaving them at the end. It also includes the core recursion logic for looping over child topics, if `isRecursive` is set. Finally, it includes establishing a shared `version` to use across all attributes, relationships, and references so all updates that are part of this operation share the same version. While not yet implemented, this will also allow us to extend the unit testing capabilities to evaluate the above features, as the unit tests don't currently cover `SqlTopicRepository.Save()`, but do cover `TopicRepository.Save()`. As part of this, the `Save()` method is marked as `sealed` and the core implementation is moved to the `abstract` `Save(topic, version, persistRelationships)` overload. This prevents the need for derived implementations to call `base.Save()`, and likewise allows the `TopicRepository.Save()` implementation more control over the order of operations—e.g., by placing validation logic above the implementation, while putting topic graph operations after it. This fixes a behavior where an operation would be reflected in the local topic graph even if the database operation failed, which often resulted in confusion since it would appear as though the operation completed based on the cached state of the topic graph. This could also potentially result in bugs in subsequent saves, since those operations would have already been completed. Finally, this includes updates to the `StubTopicRepository` to `override` the updated `Save()` overload, instead of the `ITopicRepository` version.
1 parent 3ce23f6 commit fdd0025

3 files changed

Lines changed: 114 additions & 123 deletions

File tree

OnTopic.Data.Sql/SqlTopicRepository.cs

Lines changed: 13 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using System.Linq;
1313
using System.Text;
1414
using Microsoft.Data.SqlClient;
15+
using OnTopic.Collections;
1516
using OnTopic.Data.Sql.Models;
1617
using OnTopic.Internal.Diagnostics;
1718
using OnTopic.Querying;
@@ -335,79 +336,16 @@ public override void Refresh(Topic referenceTopic, DateTime since) {
335336
/*==========================================================================================================================
336337
| METHOD: SAVE
337338
\-------------------------------------------------------------------------------------------------------------------------*/
338-
/// <inheritdoc />
339-
public override void Save([NotNull]Topic topic, bool isRecursive = false) {
340-
341-
/*------------------------------------------------------------------------------------------------------------------------
342-
| Establish dependencies
343-
\-----------------------------------------------------------------------------------------------------------------------*/
344-
var version = DateTime.UtcNow;
345-
var unresolvedTopics = new List<Topic>();
346-
347-
using var connection = new SqlConnection(_connectionString);
348-
349-
connection.Open();
350-
351-
/*------------------------------------------------------------------------------------------------------------------------
352-
| Handle first pass
353-
\-----------------------------------------------------------------------------------------------------------------------*/
354-
Save(topic, isRecursive, connection, unresolvedTopics, version);
355-
356-
/*------------------------------------------------------------------------------------------------------------------------
357-
| Attempt to resolve outstanding relationships
358-
\-----------------------------------------------------------------------------------------------------------------------*/
359-
foreach (var unresolvedTopic in unresolvedTopics) {
360-
Save(unresolvedTopic, false, connection, unresolvedTopics, version);
361-
}
362-
363-
/*------------------------------------------------------------------------------------------------------------------------
364-
| Close shared connection
365-
\-----------------------------------------------------------------------------------------------------------------------*/
366-
connection.Close();
367-
368-
}
369-
370-
/// <summary>
371-
/// The private overload of the <see cref="Save"/> method provides support for sharing the <see cref="SqlConnection"/>
372-
/// between multiple requests, and maintaining a list of <paramref name="unresolvedRelationships"/>.
373-
/// </summary>
374-
/// <remarks>
375-
/// <para>
376-
/// When recursively saving a topic graph, it is conceivable that references to other topics—such as <see
377-
/// cref="Topic.Relationships"/> or <see cref="Topic.DerivedTopic"/>—can't yet be persisted because the target <see
378-
/// cref="Topic"/> hasn't yet been saved, and thus the <see cref="Topic.Id"/> is still set to <c>-1</c>. To mitigate
379-
/// this, the <paramref name="unresolvedRelationships"/> allows this private overload to keep track of unresolved
380-
/// relationships. The public <see cref="Save(Topic, Boolean, Boolean)"/> overload uses this list to resave any topics
381-
/// that include such references. This adds some overhead due to the duplicate <see cref="Save"/>, but helps avoid
382-
/// potential data loss when working with complex topic graphs.
383-
/// </para>
384-
/// <para>
385-
/// The connection sharing probably doesn't provide that much of a gain in that .NET does a good job of connection
386-
/// pooling. Nevertheless, there is some overhead to opening a new connection, so sharing an open connection when we
387-
/// doing a recursive save could potentially provide some performance benefit.
388-
/// </para>
389-
/// </remarks>
390-
/// <param name="topic">The source <see cref="Topic"/> to save.</param>
391-
/// <param name="isRecursive">Determines whether or not to recursively save <see cref="Topic.Children"/>.</param>
392-
/// <param name="connection">The open <see cref="SqlConnection"/> to use for executing <see cref="SqlCommand"/>s.</param>
393-
/// <param name="unresolvedRelationships">A list of <see cref="Topic"/>s with unresolved topic references.</param>
394-
private void Save(
339+
/// <inheritdoc/>
340+
protected override sealed void Save(
395341
[NotNull]Topic topic,
396-
bool isRecursive,
397-
SqlConnection connection,
398-
List<Topic> unresolvedRelationships,
399-
DateTime version
342+
DateTime version,
343+
bool persistRelationships
400344
) {
401345

402-
/*------------------------------------------------------------------------------------------------------------------------
403-
| Call base method - will trigger any events associated with the save
404-
\-----------------------------------------------------------------------------------------------------------------------*/
405-
base.Save(topic, isRecursive);
406-
407346
/*------------------------------------------------------------------------------------------------------------------------
408347
| Define variables
409348
\-----------------------------------------------------------------------------------------------------------------------*/
410-
var areReferencesResolved = true;
411349
var isTopicDirty = topic.IsDirty();
412350
var areRelationshipsDirty = topic.Relationships.IsDirty();
413351
var areReferencesDirty = topic.References.IsDirty();
@@ -440,7 +378,6 @@ DateTime version
440378
| Bypass is not dirty
441379
\-----------------------------------------------------------------------------------------------------------------------*/
442380
if (!isDirty) {
443-
recurse();
444381
return;
445382
}
446383

@@ -494,29 +431,15 @@ DateTime version
494431
/*------------------------------------------------------------------------------------------------------------------------
495432
| Establish database connection
496433
\-----------------------------------------------------------------------------------------------------------------------*/
434+
using var connection = new SqlConnection(_connectionString);
497435
var procedureName = topic.IsNew? "CreateTopic" : "UpdateTopic";
498436

437+
connection.Open();
438+
499439
using var command = new SqlCommand(procedureName, connection) {
500440
CommandType = CommandType.StoredProcedure
501441
};
502442

503-
/*------------------------------------------------------------------------------------------------------------------------
504-
| Handle unresolved references
505-
>-------------------------------------------------------------------------------------------------------------------------
506-
| If it's a recursive save and there are any unresolved relationships, come back to this after the topic graph has been
507-
| saved; that ensures that any relationships within the topic graph have been saved and can be properly persisted. The
508-
| same can be done for DerivedTopics references, which are effectively establish a 1:1 relationship.
509-
\-----------------------------------------------------------------------------------------------------------------------*/
510-
if (
511-
isRecursive &&
512-
( topic.Relationships.Any(r => r.Values.Any(t => t.Id < 0)) ||
513-
topic.References.Values.Any(t => t.Id < 0)
514-
)
515-
) {
516-
unresolvedRelationships.Add(topic);
517-
areReferencesResolved = false;
518-
}
519-
520443
/*------------------------------------------------------------------------------------------------------------------------
521444
| Establish query parameters
522445
\-----------------------------------------------------------------------------------------------------------------------*/
@@ -553,18 +476,14 @@ DateTime version
553476
"The call to the CreateTopic stored procedure did not return the expected 'Id' parameter."
554477
);
555478

556-
if (areReferencesResolved && areRelationshipsDirty) {
479+
if (persistRelationships && areRelationshipsDirty) {
557480
PersistRelations(topic, version, connection);
558481
}
559482

560-
if (areReferencesResolved && areReferencesDirty) {
483+
if (persistRelationships && areReferencesDirty) {
561484
PersistReferences(topic, version, connection);
562485
}
563486

564-
if (!topic.VersionHistory.Contains(version)) {
565-
topic.VersionHistory.Insert(0, version);
566-
}
567-
568487
topic.Attributes.MarkClean(version);
569488

570489
}
@@ -580,20 +499,10 @@ DateTime version
580499
}
581500

582501
/*------------------------------------------------------------------------------------------------------------------------
583-
| Recuse over any children
502+
| Close connection
584503
\-----------------------------------------------------------------------------------------------------------------------*/
585-
recurse();
586-
587-
/*------------------------------------------------------------------------------------------------------------------------
588-
| Function: Recurse
589-
\-----------------------------------------------------------------------------------------------------------------------*/
590-
void recurse() {
591-
if (isRecursive) {
592-
foreach (var childTopic in topic.Children) {
593-
childTopic.Attributes.SetValue("ParentID", topic.Id.ToString(CultureInfo.InvariantCulture));
594-
Save(childTopic, isRecursive, connection, unresolvedRelationships, version);
595-
}
596-
}
504+
finally {
505+
connection.Close();
597506
}
598507

599508
}

OnTopic.TestDoubles/StubTopicRepository.cs

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
\=============================================================================================================================*/
66
using System;
77
using System.Collections.Generic;
8+
using System.Diagnostics.CodeAnalysis;
89
using System.Globalization;
910
using OnTopic.Attributes;
1011
using OnTopic.Internal.Diagnostics;
@@ -111,29 +112,15 @@ public override void Refresh(Topic referenceTopic, DateTime since) { }
111112
| METHOD: SAVE
112113
\-------------------------------------------------------------------------------------------------------------------------*/
113114
/// <inheritdoc />
114-
public override void Save(Topic topic, bool isRecursive = false) {
115+
protected override void Save([NotNull]Topic topic, DateTime version, bool persistRelationships) {
115116

116117
/*------------------------------------------------------------------------------------------------------------------------
117-
| Call base method - will trigger any events associated with the save
118-
\-----------------------------------------------------------------------------------------------------------------------*/
119-
base.Save(topic, isRecursive);
120-
121-
/*------------------------------------------------------------------------------------------------------------------------
122-
| Recurse through children
118+
| Assign faux identity
123119
\-----------------------------------------------------------------------------------------------------------------------*/
124120
if (topic.IsNew) {
125121
topic.Id = _identity++;
126122
}
127123

128-
/*------------------------------------------------------------------------------------------------------------------------
129-
| Recurse through children
130-
\-----------------------------------------------------------------------------------------------------------------------*/
131-
if (isRecursive) {
132-
foreach (var childTopic in topic.Children) {
133-
Save(childTopic, isRecursive);
134-
}
135-
}
136-
137124
}
138125

139126
/*==========================================================================================================================

0 commit comments

Comments
 (0)