Skip to content

Commit afa1e9b

Browse files
committed
Merge branch 'feature/track-dirty-relationships' into develop
Introduced support to track whether (any) relationships are "dirty"—i.e., whether or not they've been saved since they were last loaded from the database. Unlike attributes, which have a special `AttributeValue` container that can be decorated with an `IsDirty` property, relationships are just direct pointers to the target `Topic`. Instead, `IsDirty` is placed on the `NamedTopicCollection` which acts as the container for _all_ of the relationships (4762694). In addition, the `RelatedTopicCollection`—which tracks multiple relationships associated with a topic—has an `IsDirty()` method which rolls up this functionality (2adef58). Finally, an `isDirty` parameter was added to `RelatedTopicCollection.SetTopic()` to prevent the setting of a relationship from tripping the `IsDirty` status (2f1e2bf), which enables us to disable that functionality on `SqlTopicRepository.Load()` (7081c39) and reset it on `SqlTopicRepository.Save()` (8147562). This introduces a number of other capabilities which have the potential to greatly reduce unnecessary database access and, therefore, performance during a recursive `Save()` of a large topic graph. Previously, _all_ topics would be saved, regardless of whether any content had changed. This is because we didn't know which relationships, if any, had changed, so we would delete them all (via `UpdateTopic`'s `@DeleteRelationships` parameter), then recreate them via the `PersistRelations` stored procedure. With `RelatedTopicCollection.IsDirty()` in place, we can now conditionally ignore those if no relationships have changed (1fc3a6f)—and, better, ignore the entire `Save()` if `AttributeValueCollection.IsDirty()` is _also_ false (1fc3a6f). In fact, we can even bypass much of the logic in `Save()`, except for that necessary to determine the state (3fd9720). This resolves "Track whether relationships have changed" (#23). In addition, this also factors in the bug fix to a closely related bug, "Handle attribute configuration mismatch on `Save()`" (#24), in which attributes whose configuration `AttributeDescriptor.IsExtendedAttribute` had changed, but whose _value_ (and, therefore, `IsDirty` flag) had not changed could potentially be orphaned. This was resolved in a previous bug fix (d64d242), but needs to be accounted for here to ensure that topics are still `Save()` even if `AttributeValueCollection.IsDirty()` and `RelatedTopicCollection.IsDirty()` are both `false` if there are any _attribute configuration mismatches`, as detected by `TopicRepositoryBase.IsExtendedAttributeMismatch()` (a3d4798). Finally, while I was at it, I made a handful of other optimizations and refactorings to the `SqlTopicRepository` code base: - Implemented C# 8's new inline `using` syntax instead of explicitly calling `Dispose()` in a `finally` block (f85aab2) - Introduced `AttributeValuesDataTable` (313fe71) and `TopicLIstDataTable` (7a37dc9) to model the user-defined table-valued types in SQL. - Extended `SqlCommant.AddParameter()` to support `DataTable` objects, and derivatives (0cd2506). - Removed unused legacy support for `CreateRelationshipsXml()` and `skipXml` (7bea7d3) - Introduced `Topic.IsSaved` property for determining if a topic has been saved (1678523)
2 parents 27d9bfe + 114a429 commit afa1e9b

File tree

14 files changed

+568
-240
lines changed

14 files changed

+568
-240
lines changed

OnTopic.Data.Caching/CachedTopicRepository.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,6 @@ public override int Save(Topic topic, bool isRecursive = false, bool isDraft = f
142142
| METHOD: MOVE
143143
\-------------------------------------------------------------------------------------------------------------------------*/
144144
/// <inheritdoc />
145-
[System.Diagnostics.CodeAnalysis.SuppressMessage(
146-
"Microsoft.Contracts",
147-
"TestAlwaysEvaluatingToAConstant",
148-
Justification = "Sibling may be null from overloaded caller."
149-
)]
150145
public override void Move(Topic topic, Topic target, Topic? sibling) => _dataProvider.Move(topic, target, sibling);
151146

152147
/*==========================================================================================================================
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*==============================================================================================================================
2+
| Author Ignia, LLC
3+
| Client Ignia, LLC
4+
| Project Topics Library
5+
\=============================================================================================================================*/
6+
using System.Data;
7+
using OnTopic.Attributes;
8+
9+
namespace OnTopic.Data.Sql.Models {
10+
11+
/*============================================================================================================================
12+
| CLASS: ATTRIBUTE VALUES (DATA TABLE)
13+
\---------------------------------------------------------------------------------------------------------------------------*/
14+
/// <summary>
15+
/// Extends <see cref="DataTable"/> to model the schema for the <c>AttributeValues</c> user-defined table type.
16+
/// </summary>
17+
internal class AttributeValuesDataTable: DataTable {
18+
19+
/*==========================================================================================================================
20+
| CONSTRUCTOR
21+
\-------------------------------------------------------------------------------------------------------------------------*/
22+
/// <summary>
23+
/// Establishes a new <see cref="DataTable"/> with the appropriate schema for the <c>AttributeValues</c> user-defined
24+
/// table type.
25+
/// </summary>
26+
internal AttributeValuesDataTable() {
27+
28+
/*------------------------------------------------------------------------------------------------------------------------
29+
| COLUMN: Attribute Key
30+
\-----------------------------------------------------------------------------------------------------------------------*/
31+
Columns.Add(
32+
new DataColumn("AttributeKey") {
33+
MaxLength = 128
34+
}
35+
);
36+
37+
/*------------------------------------------------------------------------------------------------------------------------
38+
| COLUMN: Attribute Value
39+
\-----------------------------------------------------------------------------------------------------------------------*/
40+
Columns.Add(
41+
new DataColumn("AttributeValue") {
42+
MaxLength = 255
43+
}
44+
);
45+
46+
}
47+
48+
/*==========================================================================================================================
49+
| ADD ROW
50+
\-------------------------------------------------------------------------------------------------------------------------*/
51+
/// <summary>
52+
/// Provides a convenience method for adding a new <see cref="DataRow"/> based on the expected column values.
53+
/// </summary>
54+
/// <param name="attributeKey">The <see cref="AttributeValue.Key"/>.</param>
55+
/// <param name="attributeValue">The <see cref="AttributeValue.Value"/>.</param>
56+
internal DataRow AddRow(string attributeKey, string? attributeValue = null) {
57+
58+
/*------------------------------------------------------------------------------------------------------------------------
59+
| Define record
60+
\-----------------------------------------------------------------------------------------------------------------------*/
61+
var record = NewRow();
62+
record["AttributeKey"] = attributeKey;
63+
record["AttributeValue"] = attributeValue;
64+
65+
/*------------------------------------------------------------------------------------------------------------------------
66+
| Add record
67+
\-----------------------------------------------------------------------------------------------------------------------*/
68+
Rows.Add(record);
69+
70+
/*------------------------------------------------------------------------------------------------------------------------
71+
| Return record
72+
\-----------------------------------------------------------------------------------------------------------------------*/
73+
return record;
74+
75+
}
76+
77+
} //Class
78+
} //Namespaces
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*==============================================================================================================================
2+
| Author Ignia, LLC
3+
| Client Ignia, LLC
4+
| Project Topics Library
5+
\=============================================================================================================================*/
6+
using System.Data;
7+
8+
namespace OnTopic.Data.Sql.Models {
9+
10+
/*============================================================================================================================
11+
| CLASS: TOPIC LIST (DATA TABLE)
12+
\---------------------------------------------------------------------------------------------------------------------------*/
13+
/// <summary>
14+
/// Extends <see cref="DataTable"/> to model the schema for the <c>TopicList</c> user-defined table type.
15+
/// </summary>
16+
internal class TopicListDataTable: DataTable {
17+
18+
/*==========================================================================================================================
19+
| CONSTRUCTOR
20+
\-------------------------------------------------------------------------------------------------------------------------*/
21+
/// <summary>
22+
/// Establishes a new <see cref="DataTable"/> with the appropriate schema for the <c>TopicList</c> user-defined
23+
/// table type.
24+
/// </summary>
25+
internal TopicListDataTable() {
26+
27+
/*------------------------------------------------------------------------------------------------------------------------
28+
| COLUMN: Topic ID
29+
\-----------------------------------------------------------------------------------------------------------------------*/
30+
Columns.Add(
31+
new DataColumn("TopicID", typeof(int))
32+
);
33+
34+
}
35+
36+
/*==========================================================================================================================
37+
| ADD ROW
38+
\-------------------------------------------------------------------------------------------------------------------------*/
39+
/// <summary>
40+
/// Provides a convenience method for adding a new <see cref="DataRow"/> based on the expected column values.
41+
/// </summary>
42+
/// <param name="topicId">The <see cref="Topic.Id"/> of the related <see cref="Topic"/></param>
43+
internal DataRow AddRow(int topicId) {
44+
45+
/*------------------------------------------------------------------------------------------------------------------------
46+
| Define record
47+
\-----------------------------------------------------------------------------------------------------------------------*/
48+
var record = NewRow();
49+
record["TopicID"] = topicId;
50+
51+
/*------------------------------------------------------------------------------------------------------------------------
52+
| Add record
53+
\-----------------------------------------------------------------------------------------------------------------------*/
54+
Rows.Add(record);
55+
56+
/*------------------------------------------------------------------------------------------------------------------------
57+
| Return record
58+
\-----------------------------------------------------------------------------------------------------------------------*/
59+
return record;
60+
61+
}
62+
63+
} //Class
64+
} //Namespaces

OnTopic.Data.Sql/SqlCommandExtensions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ internal static void AddParameter(this SqlCommand command, string sqlParameter,
8181
internal static void AddParameter(this SqlCommand command, string sqlParameter, DateTime fieldValue)
8282
=> AddParameter(command, sqlParameter, fieldValue, SqlDbType.DateTime);
8383

84+
/// <summary>
85+
/// Wrapper function that adds a SQL parameter to a command object.
86+
/// </summary>
87+
/// <param name="command">The SQL command object.</param>
88+
/// <param name="sqlParameter">The SQL parameter.</param>
89+
/// <param name="fieldValue">The SQL field value.</param>
90+
internal static void AddParameter(this SqlCommand command, string sqlParameter, DataTable fieldValue)
91+
=> AddParameter(command, sqlParameter, fieldValue, SqlDbType.Structured);
92+
8493
/// <summary>
8594
/// Wrapper function that adds a SQL parameter to a command object.
8695
/// </summary>
@@ -145,6 +154,7 @@ private static void AddParameter(
145154
SqlDbType.DateTime => (DateTime)fieldValue,
146155
SqlDbType.Int => (int)fieldValue,
147156
SqlDbType.Xml => (string)fieldValue,
157+
SqlDbType.Structured => (DataTable)fieldValue,
148158
_ => (string)fieldValue,
149159
};
150160
}

OnTopic.Data.Sql/SqlDataReaderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ private static void SetRelationships(this SqlDataReader reader, Dictionary<int,
318318
/*------------------------------------------------------------------------------------------------------------------------
319319
| Set relationship on object
320320
\-----------------------------------------------------------------------------------------------------------------------*/
321-
current.Relationships.SetTopic(relationshipKey, related);
321+
current.Relationships.SetTopic(relationshipKey, related, isIncoming: false, isDirty: false);
322322

323323
}
324324

0 commit comments

Comments
 (0)