Redis是一個開源的、高效能的、基於記憶體的鍵值資料庫,它支援多種資料結構,如字串、列表、集合、雜湊、有序集合等。其中,Redis的雜湊(Hash)結構是一個常用的結構,今天跟大家分享一個我的日常操作,如何使用Redis的雜湊(Hash)結構來快取和查詢物件的屬性值,以及如何用Lambda表示式樹來簡化這個過程。
Redis Hash結構是一種鍵值對的集合,它可以儲存一個物件的多個欄位和值。例如,我們可以用一個Hash結構來儲存一個人的資訊,如下所示:
HSET person:1 id 1 HSET person:1 name Alice HSET person:1 age 20
上面的命令將一個人的資訊儲存到了一個名為person:1的Hash結構中,其中每個欄位都有一個名稱和一個值。我們可以使用HGET命令來獲取某個欄位的值,例如:
HGET person:1 name#Alice
我們也可以使用HGETALL命令來獲取所有欄位的值,例如:
HGETALL person:1id 1name Aliceage 20
為了在C#中操作Redis Hash結構,我們需要使用一個第三方庫:StackExchange.Redis。這個庫提供了一個ConnectionMultiplexer類,用於建立和管理與Redis伺服器的連線,以及一個IDatabase介面,用於執行各種命令。例如,我們可以使用以下程式碼來建立一個連線物件和一個資料庫物件:
// 連線Redis伺服器 ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); // 獲取資料庫物件IDatabase db = redis.GetDatabase();
然後,我們可以使用db物件的HashSet方法和HashGet方法來儲存和獲取Hash結構中的欄位值。
// 建立一個HashEntry陣列,存放要快取的物件屬性 HashEntry[] hashfield = new HashEntry[3]; hashfield[0] = new HashEntry("id", "1"); hashfield[1] = new HashEntry("name", "Alice"); hashfield[2] = new HashEntry("age", "20"); // 使用HashSet方法將物件屬性快取到Redis的雜湊(Hash)結構中 db.HashSet("person:1", hashfield); // 使用HashGetAll方法從Redis的雜湊(Hash)結構中查詢物件屬性 HashEntry[] result = db.HashGetAll("person:1"); // 遍歷結果陣列,列印物件屬性 foreach (var item in result) { Console.WriteLine(item.Name + ": " + item.Value); }
但是,這種方式有一些缺點:
有沒有更優雅的方法來解決這個問題呢?答案是肯定的。
Lambda表示式是一種匿名函數,可以用來表示委託或表示式樹。在.NET中,我們可以使用Lambda表示式來操作實體類的屬性,比如獲取屬性的值或者更新屬性的值。
我們可以利用 Lambda表示式來指定要儲存或獲取的物件的屬性,而不是使用字串。使用表示式樹來遍歷Lambda表示式,提取出屬性名和屬性值,並轉換為HashEntry陣列或RedisValue陣列,使其更易於使用。例如:
Get<Person>(p => new { p.Name, p.Age });
如果我們只想選擇一個屬性,就可以直接寫:
Get<Person>(p => p.Name)
如果要更新物件指定的屬性,可以這樣寫了:
Update<Person>(p => p .SetProperty(x => x.Name, "Alice") .SetProperty(x => x.Age, 25));
怎麼樣,這樣是不是優雅多了,這樣做有以下好處:
那麼,我們如何實現上面的方法呢?
這個方法的目的是從快取中獲取物件的一個或多個屬性值,使用一個泛型方法和一個Lambda表示式來實現。
private static TResult Get<T, TResult>(IDatabase db, int id, Expression<Func<T, TResult>> selector) { if (selector == null) throw new ArgumentNullException(nameof(selector)); // 使用擴充套件方法獲取要查詢的屬性名陣列 var hashFields = selector.GetMemberNames().Select(m => new RedisValue(m)).ToArray(); // 從快取中獲取對應的屬性值陣列 var values = db.HashGet($"person:{id}", hashFields); // 使用擴充套件方法將HashEntry陣列轉換為物件 var obj = values.ToObject<T>(hashFields); // 返回查詢結果 return selector.Compile()(obj); } private static TResult Get<TResult>(IDatabase db, int id, Expression<Func<Person, TResult>> selector) => Get<Person, TResult>(db, id, selector);
這個類的作用是遍歷一個表示式樹,收集其中的成員表示式的名稱,並儲存到一個列表中。
public class MemberExpressionVisitor : ExpressionVisitor { private readonly IList<string> _names; public MemberExpressionVisitor(IList<string> list) { _names = list; } protected override Expression VisitMember(MemberExpression node) { var name = node.Member.Name; if (node.Expression is MemberExpression member) { Visit(member); name = member.Member.Name + "." + name; } _names.Add(name); return base.VisitMember(node); } }
這個方法目的是將一個物件指定的屬性名和值更新到快取中,使用一個泛型方法和一個委託函數來實現。
public static Dictionary<string, object> Update<TSource>(Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>> setPropertyCalls) { if (setPropertyCalls == null) throw new ArgumentNullException(nameof(setPropertyCalls)); var nameValues = new Dictionary<string, object>(100); // 建立一個字典用於儲存屬性名和值 var calls = new SetPropertyCalls<TSource>(nameValues); // 建立一個SetPropertyCalls物件 setPropertyCalls(calls); // 呼叫傳入的函數,將屬性名和值新增到字典中 return nameValues; // 返回字典 } private static void Update(IDatabase db, int id, Func<SetPropertyCalls<Person>, SetPropertyCalls<Person>> setPropertyCalls) { var hashEntries = Update(setPropertyCalls) .Select(kv => new HashEntry(kv.Key, kv.Value != null ? kv.Value.ToString() : RedisValue.EmptyString)) .ToArray(); // 將HashEntry陣列儲存到快取中,使用物件的Id作為鍵 db.HashSet(id.ToString(), hashEntries); }}
這個類的作用是收集一個源物件的屬性名稱和值的對應關係,並提供一個鏈式呼叫的方法,用於設定屬性的值。
public class SetPropertyCalls<TSource> { private readonly Dictionary<string, object> _nameValues; public SetPropertyCalls(Dictionary<string, object> nameValues) { _nameValues = nameValues; } public SetPropertyCalls<TSource> SetProperty<TProperty>(Expression<Func<TSource, TProperty>> propertyExpression, TProperty valueExpression) { if (propertyExpression == null) throw new ArgumentNullException(nameof(propertyExpression)); if (propertyExpression.Body is MemberExpression member && member.Member is PropertyInfo property) { if (!_nameValues.TryAdd(property.Name, valueExpression)) { throw new ArgumentException($"The property '{property.Name}' has already been set."); } } return this; } }
這樣,我們就可以得到一個包含所有要更新的屬性名和值的字典,然後我們就可以根據這些屬性名和值來更新實體類的屬性了。
讓我們來看一下程式碼範例,為了方便演示和閱讀,這是臨時碼的,實際中大家可以根據自己習慣來進行封裝,簡化呼叫,同時也可以使用靜態字典來快取編譯好的委託及物件屬性,提高效能。