Skip to content

Commit 972dad8

Browse files
authored
Single record updates / deletes (#73)
* single record updates * single record deletion. * fixing new-line incongruity. * removing erroneous console writeline
1 parent f98f253 commit 972dad8

File tree

11 files changed

+407
-19
lines changed

11 files changed

+407
-19
lines changed

src/Redis.OM/Modeling/RedisCollectionStateManager.cs

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ public class RedisCollectionStateManager
2020
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
2121
};
2222

23-
private readonly DocumentAttribute _documentAttribute;
24-
2523
static RedisCollectionStateManager()
2624
{
2725
JsonSerializerOptions.Converters.Add(new GeoLocJsonConverter());
@@ -34,9 +32,14 @@ static RedisCollectionStateManager()
3432
/// <param name="attr">The document attribute for the type.</param>
3533
public RedisCollectionStateManager(DocumentAttribute attr)
3634
{
37-
_documentAttribute = attr;
35+
DocumentAttribute = attr;
3836
}
3937

38+
/// <summary>
39+
/// Gets the DocumentAttribute for the underlying type.
40+
/// </summary>
41+
public DocumentAttribute DocumentAttribute { get; }
42+
4043
/// <summary>
4144
/// Gets a snapshot from when the collection enumerated.
4245
/// </summary>
@@ -47,19 +50,37 @@ public RedisCollectionStateManager(DocumentAttribute attr)
4750
/// </summary>
4851
internal IDictionary<string, object?> Data { get; set; } = new Dictionary<string, object?>();
4952

53+
/// <summary>
54+
/// Removes the key from the data and snapshot.
55+
/// </summary>
56+
/// <param name="key">The key to remove.</param>
57+
internal void Remove(string key)
58+
{
59+
Snapshot.Remove(key);
60+
Data.Remove(key);
61+
}
62+
63+
/// <summary>
64+
/// Add item to data.
65+
/// </summary>
66+
/// <param name="key">the item's key.</param>
67+
/// <param name="value">the item's value.</param>
68+
internal void InsertIntoData(string key, object value)
69+
{
70+
Data.Remove(key);
71+
Data.Add(key, value);
72+
}
73+
5074
/// <summary>
5175
/// Add item to snapshot.
5276
/// </summary>
5377
/// <param name="key">the item's key.</param>
5478
/// <param name="value">the current value of the item.</param>
5579
internal void InsertIntoSnapshot(string key, object value)
5680
{
57-
if (Snapshot.ContainsKey(key))
58-
{
59-
return;
60-
}
81+
Snapshot.Remove(key);
6182

62-
if (_documentAttribute.StorageType == StorageType.Json)
83+
if (DocumentAttribute.StorageType == StorageType.Json)
6384
{
6485
var json = JToken.FromObject(value, Newtonsoft.Json.JsonSerializer.Create(new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }));
6586
Snapshot.Add(key, json);
@@ -71,14 +92,53 @@ internal void InsertIntoSnapshot(string key, object value)
7192
}
7293
}
7394

95+
/// <summary>
96+
/// Builds a diff for a single object from what's currently in the snapshot.
97+
/// </summary>
98+
/// <param name="key">the key of the object in redis.</param>
99+
/// <param name="value">The current value.</param>
100+
/// <param name="differences">The detected differences.</param>
101+
/// <returns>Whether a diff could be constructed.</returns>
102+
internal bool TryDetectDifferencesSingle(string key, object value, out IList<IObjectDiff>? differences)
103+
{
104+
if (!Snapshot.ContainsKey(key))
105+
{
106+
differences = null;
107+
return false;
108+
}
109+
110+
if (DocumentAttribute.StorageType == StorageType.Json)
111+
{
112+
var dataJson = JsonSerializer.Serialize(value, JsonSerializerOptions);
113+
var current = JsonConvert.DeserializeObject<JObject>(dataJson, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
114+
var snapshot = (JToken)Snapshot[key];
115+
var diff = FindDiff(current!, snapshot);
116+
differences = BuildJsonDifference(diff, "$");
117+
}
118+
else
119+
{
120+
var dataHash = value.BuildHashSet();
121+
var snapshotHash = (IDictionary<string, string>)Snapshot[key];
122+
var deletedKeys = snapshotHash.Keys.Except(dataHash.Keys).Select(x => new KeyValuePair<string, string>(x, string.Empty));
123+
var modifiedKeys = dataHash.Where(x =>
124+
!snapshotHash.Keys.Contains(x.Key) || snapshotHash[x.Key] != x.Value);
125+
differences = new List<IObjectDiff>
126+
{
127+
new HashDiff(modifiedKeys, deletedKeys.Select(x => x.Key)),
128+
};
129+
}
130+
131+
return true;
132+
}
133+
74134
/// <summary>
75135
/// Detects the differences.
76136
/// </summary>
77137
/// <returns>a difference dictionary.</returns>
78138
internal IDictionary<string, IList<IObjectDiff>> DetectDifferences()
79139
{
80140
var res = new Dictionary<string, IList<IObjectDiff>>();
81-
if (_documentAttribute.StorageType == StorageType.Json)
141+
if (DocumentAttribute.StorageType == StorageType.Json)
82142
{
83143
foreach (var key in Snapshot.Keys)
84144
{
@@ -267,8 +327,12 @@ private static JObject FindDiff(JToken currentObject, JToken snapshotObject)
267327

268328
break;
269329
default:
270-
diff["+"] = currentObject;
271-
diff["-"] = snapshotObject;
330+
if (currentObject.ToString() != snapshotObject.ToString())
331+
{
332+
diff["+"] = currentObject;
333+
diff["-"] = snapshotObject;
334+
}
335+
272336
break;
273337
}
274338

src/Redis.OM/RedisCommands.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,5 +363,44 @@ public static async Task<IDictionary<string, string>> HGetAllAsync(this IRedisCo
363363
/// <param name="key">the key to unlink.</param>
364364
/// <returns>the status.</returns>
365365
public static string Unlink(this IRedisConnection connection, string key) => connection.Execute("UNLINK", key);
366+
367+
/// <summary>
368+
/// Unlinks a key.
369+
/// </summary>
370+
/// <param name="connection">the connection.</param>
371+
/// <param name="key">the key to unlink.</param>
372+
/// <returns>the status.</returns>
373+
public static async Task<string> UnlinkAsync(this IRedisConnection connection, string key) => await connection.ExecuteAsync("UNLINK", key);
374+
375+
/// <summary>
376+
/// Unlinks the key and then adds an updated value of it.
377+
/// </summary>
378+
/// <param name="connection">The connection to redis.</param>
379+
/// <param name="key">The key.</param>
380+
/// <param name="value">The value.</param>
381+
/// <param name="storageType">The storage type of the value.</param>
382+
/// <typeparam name="T">The type of the value.</typeparam>
383+
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
384+
internal static async Task UnlinkAndSet<T>(this IRedisConnection connection, string key, T value, StorageType storageType)
385+
{
386+
_ = value ?? throw new ArgumentNullException(nameof(value));
387+
if (storageType == StorageType.Json)
388+
{
389+
await connection.CreateAndEvalAsync(nameof(Scripts.UnlinkAndSendJson), new[] { key }, new[] { JsonSerializer.Serialize(value, Options) });
390+
}
391+
else
392+
{
393+
var hash = value.BuildHashSet();
394+
var args = new List<string>((hash.Keys.Count * 2) + 1);
395+
args.Add(hash.Keys.Count.ToString());
396+
foreach (var pair in hash)
397+
{
398+
args.Add(pair.Key);
399+
args.Add(pair.Value);
400+
}
401+
402+
await connection.CreateAndEvalAsync(nameof(Scripts.UnlinkAndSetHash), new[] { key }, args.ToArray());
403+
}
404+
}
366405
}
367406
}

src/Redis.OM/RedisObjectHandler.cs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Reflection;
55
using System.Runtime.CompilerServices;
6+
using System.Text;
67
using System.Text.Json;
78
using Redis.OM.Contracts;
89
using Redis.OM.Modeling;
@@ -95,6 +96,43 @@ internal static string GetId(this object obj)
9596
return string.Empty;
9697
}
9798

99+
/// <summary>
100+
/// Gets the fully formed key name for the given object.
101+
/// </summary>
102+
/// <param name="obj">the object to pull the key from.</param>
103+
/// <returns>The key.</returns>
104+
/// <exception cref="ArgumentException">Thrown if type is invalid or there's no id present on the key.</exception>
105+
internal static string GetKey(this object obj)
106+
{
107+
var type = obj.GetType();
108+
var documentAttribute = (DocumentAttribute)type.GetCustomAttribute(typeof(DocumentAttribute));
109+
if (documentAttribute == null)
110+
{
111+
throw new ArgumentException("Missing Document Attribute on Declaring class");
112+
}
113+
114+
var id = obj.GetId();
115+
if (string.IsNullOrEmpty(id))
116+
{
117+
throw new ArgumentException("Id field is not correctly populated");
118+
}
119+
120+
var sb = new StringBuilder();
121+
if (documentAttribute.Prefixes.Any())
122+
{
123+
sb.Append(documentAttribute.Prefixes.First());
124+
sb.Append(":");
125+
}
126+
else
127+
{
128+
sb.Append(type.FullName);
129+
sb.Append(":");
130+
}
131+
132+
sb.Append(id);
133+
return sb.ToString();
134+
}
135+
98136
/// <summary>
99137
/// Set's the id of the given field based off the objects id strategy.
100138
/// </summary>
@@ -214,7 +252,8 @@ internal static IDictionary<string, string> BuildHashSet(this object obj)
214252
var hash = new Dictionary<string, string>();
215253
foreach (var property in properties)
216254
{
217-
var type = property.PropertyType;
255+
var type = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
256+
218257
var propertyName = property.Name;
219258
ExtractPropertyName(property, ref propertyName);
220259
if (type.IsPrimitive || type == typeof(decimal) || type == typeof(string) || type == typeof(GeoLoc) || type == typeof(Ulid) || type == typeof(Guid))
@@ -286,7 +325,7 @@ private static string SendToJson(IDictionary<string, string> hash, Type t)
286325
var ret = "{";
287326
foreach (var property in properties)
288327
{
289-
var type = property.PropertyType;
328+
var type = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
290329
var propertyName = property.Name;
291330
ExtractPropertyName(property, ref propertyName);
292331
if (!hash.Any(x => x.Key.StartsWith(propertyName)))

src/Redis.OM/Scripts.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,31 @@ local second_op
6060
internal const string Unlink = @"
6161
return redis.call('UNLINK',KEYS[1])";
6262

63+
/// <summary>
64+
/// Unlinks and sets a key for a Hash model.
65+
/// </summary>
66+
internal const string UnlinkAndSetHash = @"
67+
redis.call('UNLINK',KEYS[1])
68+
local num_fields = ARGV[1]
69+
local end_index = num_fields * 2 + 1
70+
local args = {}
71+
for i = 2, end_index, 2 do
72+
args[i-1] = ARGV[i]
73+
args[i] = ARGV[i+1]
74+
end
75+
redis.call('HSET',KEYS[1],unpack(args))
76+
return 0
77+
";
78+
79+
/// <summary>
80+
/// Unlinks a JSON object and sets the key again with a fresh new JSON object.
81+
/// </summary>
82+
internal const string UnlinkAndSendJson = @"
83+
redis.call('UNLINK', KEYS[1])
84+
redis.call('JSON.SET', KEYS[1], '.', ARGV[1])
85+
return 0
86+
";
87+
6388
/// <summary>
6489
/// The scripts.
6590
/// </summary>
@@ -68,6 +93,8 @@ local second_op
6893
{ nameof(JsonDiffResolution), JsonDiffResolution },
6994
{ nameof(HashDiffResolution), HashDiffResolution },
7095
{ nameof(Unlink), Unlink },
96+
{ nameof(UnlinkAndSetHash), UnlinkAndSetHash },
97+
{ nameof(UnlinkAndSendJson), UnlinkAndSendJson },
7198
};
7299

73100
/// <summary>

src/Redis.OM/Searching/IRedisCollection.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,19 @@ public interface IRedisCollection<T> : IOrderedQueryable<T>, IAsyncEnumerable<T>
6868
/// <param name="expression">the expression to be matched.</param>
6969
/// <returns>Whether anything matching the expression was found.</returns>
7070
bool Any(Expression<Func<T, bool>> expression);
71+
72+
/// <summary>
73+
/// Updates the provided item in Redis. Document must have a property marked with the <see cref="RedisIdFieldAttribute"/>.
74+
/// </summary>
75+
/// <param name="item">The item to update.</param>
76+
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
77+
Task Update(T item);
78+
79+
/// <summary>
80+
/// Deletes the item from Redis.
81+
/// </summary>
82+
/// <param name="item">The item to be deleted.</param>
83+
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
84+
Task Delete(T item);
7185
}
7286
}

src/Redis.OM/Searching/RedisCollection.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,43 @@ public bool Any(Expression<Func<T, bool>> expression)
8484
return res.Documents.Values.Any();
8585
}
8686

87+
/// <inheritdoc />
88+
public async Task Update(T item)
89+
{
90+
var key = item.GetKey();
91+
IList<IObjectDiff>? diff;
92+
var diffConstructed = StateManager.TryDetectDifferencesSingle(key, item, out diff);
93+
if (diffConstructed)
94+
{
95+
if (diff!.Any())
96+
{
97+
var args = new List<string>();
98+
var scriptName = diff!.First().Script;
99+
foreach (var update in diff!)
100+
{
101+
args.AddRange(update.SerializeScriptArgs());
102+
}
103+
104+
await _connection.CreateAndEvalAsync(scriptName, new[] { key }, args.ToArray());
105+
}
106+
}
107+
else
108+
{
109+
await _connection.UnlinkAndSet(key, item, StateManager.DocumentAttribute.StorageType);
110+
}
111+
112+
StateManager.InsertIntoSnapshot(key, item);
113+
StateManager.InsertIntoData(key, item);
114+
}
115+
116+
/// <inheritdoc />
117+
public async Task Delete(T item)
118+
{
119+
var key = item.GetKey();
120+
await _connection.UnlinkAsync(key);
121+
StateManager.Remove(key);
122+
}
123+
87124
/// <inheritdoc/>
88125
public IEnumerator<T> GetEnumerator()
89126
{
@@ -120,7 +157,6 @@ public void Save()
120157
public async ValueTask SaveAsync()
121158
{
122159
var diff = StateManager.DetectDifferences();
123-
Console.WriteLine(diff);
124160
var tasks = new List<Task<int?>>();
125161
foreach (var item in diff)
126162
{

src/Redis.OM/Searching/RedisCollectionEnumerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ private void ConcatenateRecords()
182182
continue;
183183
}
184184

185-
_stateManager.Data.Add(record.Key, record.Value);
185+
_stateManager.InsertIntoData(record.Key, record.Value);
186186
_stateManager.InsertIntoSnapshot(record.Key, record.Value);
187187
}
188188
}

0 commit comments

Comments
 (0)