オウルです。
今回の題材はMongoDBです。MongoDBはNoSQLでドキュメント指向データベースです。そのMongoDBをMongoDB C#/.NET Driverを使用して検索してみます。DBMSは慣れてるけどMongoDBは、、、ちょっと、、、という方にも分かり易いように、MongoDBのクエリをDBMSに置き換えたら、こんな感じのSQLというのを並べて紹介します。
App | ASP.NET Core 3.1.302 |
MongoDB | 4.4.3 |
MongoDB C#/.NET Driver | 2.11 |
MongoDBの準備
MongoDBのインストール、MongoDB 用 .NET ドライバーのインストール、MongoDBの構成は、ASP.NET Core と MongoDB で Web API を作成するに丁寧に解説されているため、参照しながら進めてください。
Redis、MongoDB、Kafkaらが相次いで商用サービスを制限するライセンス変更が話題となったのは、まだ記憶に新しいのでライセンス情報のリンクもあわせて載せておきます。
MongoDB.Driverで検索
MongoDBの準備が整ったらMongoDB C#/.NET Driverを使用して検索していきます。まずは、コレクションに格納するドキュメントのスキーマを定義します。
エンティティ(スキーマ)の定義
ドラクエウォークのこころをスキーマとして定義してみます。サンプルデータは前回の記事でも取り上げた「キラーマシン」と「キングスライム」にします。
こころのスキーマ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public class Heart { [BsonId] [BsonRepresentation(MongoDB.Bson.BsonType.ObjectId)] public string Id { get; set; } // なまえ public string Name { get; set; } // さいだいHP public int Hp { get; set; } // さいだいMP public int Mp { get; set; } // 力 public int Power { get; set; } // みのまもり public int Defense { get; set; } // こうげき魔力 public int AtMagical { get; set; } // かいふく魔力 public int DefMagical { get; set; } // すばやさ public int Agility { get; set; } // きようさ public int Dexterity { get; set; } // コスト public int Cost { get; set; } = 1; // 色 public Color Color { get; set; } // 特殊効果 public HeartSpecialEffects[] Effects { get; set; } // 更新日時 [BsonDateTimeOptions(Kind = DateTimeKind.Local)] public DateTime UpdateDateTime { get; set; } } public enum Color { None = 0, Red = 1, Yellow = 2, Blue = 3, Purple = 4, Green = 5 } |
上記クラスで属性しているIdとUpdateDateTimeについて補足です。
IdはMongoDBコレクションにマッピングするために必須です。[BsonId]でドキュメントの主キーを指定しており、[BsonRepresentation(BsonType.ObjectId)]でMongoによってstringからObjectIdへ変換されます。
続いて、[BsonDateTimeOptions(Kind = DateTimeKind.Local)]ですが、まずMongoDBにおけるDateの取り扱いです。
MongoDB stores times in UTC by default, and will convert any local time representations into this form. Applications that must operate or report on some unmodified local time value may store the time zone alongside the UTC timestamp, and compute the original local time in their application logic.
とあるように、MongoDBは、デフォルトでUTCで時刻を格納します。ここでは、UTCから現地時間に変換するようにDateTimeKind.Localを指定しています。
特殊効果のスキーマ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class HeartSpecialEffects { // 特殊効果 public SpecialEffects Effect { get; set; } // 値 public int Value { get; set; } } public enum SpecialEffects { Slashing, Flame, Ice, Heal, ParalysisTolerance, Satisfaction } |
登録データはこんな感じになりました。
オートマッピング
エンティティに列挙型を含めるため、Conventionsを設定します。
1 2 3 4 5 6 |
// 列挙型⇔Stringのマッピングをします var pack = new ConventionPack { new EnumRepresentationConvention(BsonType.String) }; ConventionRegistry.Register("EnumStringConvention", pack, t => true); |
次に検索用のサービスを作成します。
MongoDBの操作
MongoDBの操作はMongoClientを使用します。
It is recommended to store a MongoClient instance in a global place, either as a static variable or in an IoC container with a singleton lifetime.
とあるため、MongoClientを使用するサービスはシングルトンサービスとしてDI登録します。
Startup.ConfigureServices
1 2 3 4 5 6 7 |
services.Configure<MongoDbSettings>( Configuration.GetSection(nameof(MongoDbSettings)) ); services.AddSingleton<MongoDbSettings>(sp => sp.GetRequiredService<IOptions<MongoDbSettings>>().Value ); services.AddSingleton<IHeartService, HeartService>(); |
検索用のサービス
DIされたMongoDbSettingsの情報を元にMongoDB Serverに接続、データベースの取得、コレクションの取得を行います。
1 2 3 4 5 6 7 |
private readonly IMongoCollection<Models.Heart> _collection; public HeartService(MongoDbSettings settings) { var client = new MongoClient(settings.ConnectionStrings); var database = client.GetDatabase(settings.DatabaseName); _collection = database.GetCollection<Models.Heart>(settings.CollectionName); } |
検索ではMongoDBクエリをビルドするのに、次のAPIを使用します。
- Eq(等価演算子)
- Lte|Gte(比較演算子)
- Regex(正規表現)
- In
- ElemMatch
SearchCondition
検索条件クラスを準備しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class SearchCondition { // なまえ public string Name { get; set; } // 色 public Color Color { get; set; } = Color.None; // コスト 下限 public int UpperCost { get; set; } = 1; // 上限 public int LowerCost { get; set; } = 999; // 特殊効果 public SpecialEffects[] Effects { get; set; } } |
IHeartService/HeartService
フィルタを定義するビルダーを準備します。検索条件により動的にクエリをビルドするため、ここではFilterDefinitionの配列を準備します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public IEnumerable<Models.Heart> Find(SearchCondition condition) { // Filter Definition Builder var builder = Builders<Models.Heart>.Filter; // ここでは使用していませんが、Dateを条件に含む場合は、UTCを使用します var nowJst = DateTime.Now.Date; var nowUtc = nowJst.ToUniversalTime(); // Filters // AND条件、OR条件(今回は使用なし)のListを用意しておきます var andFilters = new List<FilterDefinition<Models.Heart>>(); var orFilters = new List<FilterDefinition<Models.Heart>>(); if (condition.Color != Color.None) { // *** Eq(等価演算子) *** // SQL : where Color = 'Red' andFilters.Add(builder.Eq(x => x.Color, condition.Color)); } if (!string.IsNullOrEmpty(condition.Name)) { // *** Regex(正規表現) *** // SQL : where Name Like 'xxx%' // Mongo shell : db.collection.find({"Name": {$regex: /xxx/},{"Name": 1}}) andFilters.Add(builder.Regex(x => x.Name, condition.Name)); } if (condition.Effects != null && 0 < condition.Effects.Length) { // *** in *** // Mongo shell : db.heart.find({"Effects.Effect": { $in: ["ParalysisTolerance"] } },{"Name": 1}) // *** elemMatch *** // Mongo shell : db.heart.find({ "Effects": { $elemMatch: { "Effect": { $eq: "ParalysisTolerance" } } } },{"Name": 1}) andFilters.Add(builder.ElemMatch(x => x.Effects, Builders<HeartSpecialEffects>.Filter.In(c => c.Effect, condition.Effects))); } andFilters.Add(builder.Gte(x => x.Cost, condition.UpperCost)); andFilters.Add(builder.Lte(x => x.Cost, condition.LowerCost)); var filter = builder.And(andFilters.ToArray()); return _collection.Find(filter).ToEnumerable(); } |
おまけ
式ツリーも使えるので、これは完全に自分用のサンプルです。式ツリーをかくときは大体忘れれて式ツリーを見るところから始まるため、、、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public IEnumerable<Models.Heart> FindByExpressionTree(SearchCondition condition) { var type = typeof(Models.Heart); var parameter = Expression.Parameter(type, "x"); var exps = new List<Expression>(); if (!string.IsNullOrEmpty(condition.Name)) { var method = typeof(string).GetMethod("Contains", new[] { typeof(string) }); var member = Expression.MakeMemberAccess(parameter, type.GetProperty("Name")); var args = Expression.Constant(condition.Name); exps.Add(Expression.Call(member, method, args)); } Expression pre = null; foreach (var exp in exps) { pre = (pre == null) ? exp : Expression.AndAlso(pre, exp); } var func = Expression.Lambda<Func<Models.Heart, bool>>(pre, parameter); return _collection.Find(func).ToEnumerable(); } |