どのような業界においても、データは何よりの資産です。データを分析することで、顧客動向や行動予測に関する適切な意思決定を行うことができ、ひいては事業の収益性が高まります。
しかし、データベースソフトがなければ、レコードを保持するシステムから平均値を求めるような簡単な作業も厄介に。幸い、データベースの関数や演算子によって、データを簡単かつ素早く分析することができます。
この記事では、データベースソフトMongoDB内で使用される重要な演算子をご紹介します。
MongoDBの演算子
MongoDBはドキュメント指向の情報を管理する、NoSQLデータベースソフトウェアです。
主な特徴の1つは、そのスピード。特定の機能を実行する演算子を使用して、即座にクエリを返します。
演算子とは、算術タスクや論理タスクの実行においてコンパイラを助ける特別な記号です。MongoDBにはデータベースと対話するいくつかの演算子が用意されています。
MongoDB演算子の種類
MongoDBの演算子には9種類あり、それぞれ機能に応じた名前がついています。例えば、論理演算子は、その名の通り、論理的な演算を使用します。演算子を実行するには特定のキーワードを使用し、決められた構文に従わなければなりませんが、非常にシンプルなのでご安心を。
この記事を通して、各演算子の基本的な使い方と機能を理解できるはずです。
論理演算子
論理演算子は、条件に基づいたデータのフィルタリングに使用されます。後ほど詳しくご説明しますが、さまざまな条件を指定することができます。
以下、論理演算子をいくつか見ていきましょう。
$and
「and」条件は2つ以上の式からなる配列に対して、論理積の操作を実行します。式のすべての条件を満たすドキュメントを選択します。
$and
式の標準的な構文は以下のとおりです。
{ $and: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] }
例えば、価格(price)が10ドル、かつ数量(quantity)が15以下のドキュメントを選択するには、以下のようにクエリを入力します。
db.inventory.find( { $and: [ { quantity: { $lt: 15 } }, { price: 10 } ] } )
$or
「or」条件は、2つ以上の式からなる配列に対して、論理和の操作を実行します。少なくとも1つの式が真であるドキュメントを選択します。
$or
式の標準的な構文は以下のとおり。
{ $or: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] }.
例えば、価格が10ドル、または数量が15以下のドキュメントを選択するには、以下のようにクエリを入力します。
db.inventory.find( { $or: [ { quantity: { $lt: 15 } }, { price: 10 } ] } )
式の条件は2つに限らず、いくつでも追加可能です。例えば、以下のクエリは、価格が10ドル、または数量が15個以下、またはタグ(tag)が「stationary」のドキュメントを選択します。
db.inventory.find( { $or: [ { quantity: { $lt: 15 } }, { price: 10 }, { tag: "stationary" }] } )
それぞれの句を実行する際、MongoDBはコレクションスキャンかインデックススキャンのどちらかで$or
式をチェックします。すべてのインデックスがそれぞれの句をサポートしている場合は後者、そうでなければ前者が使用されます。
同じフィールドの条件をテストする場合は、$or
演算子ではなく、$in
演算子を使用するのが得策です。例えば、数量が10、または、数量が20のドキュメントを選択するには、代替として以下の$in
クエリを実行します。
db.inventory.find ( { quantity: { $in: [20, 50] } } )
$in
演算子については、後で詳しくご説明します。
$nor
この演算子は1つ以上の式を使用して、配列の否定論理和の操作を実行し、クエリ式に失敗するドキュメントを選択します。すなわち、$or
条件の逆の役割を果たします。
一般的な構文は、以下のとおりです。
{ $nor: [ { <expression1> }, { <expression2> }, ... { <expressionN> } ] }
以下、例を見てみましょう。
db.inventory.find( { $nor: [ { price: 3.99 }, { sale: true } ] } )
このクエリは、以下の条件を満たすドキュメントを選択します。
- 「priceフィールドの値が$3.99でない、かつ、saleフィールドの値がtrueでない」または
- 「priceフィールドの値が$3.99でない、かつ、saleフィールドが空か存在しない」または
- 「priceフィールドがない、かつ、saleフィールドがtrueでない」または
- 「priceフィールドもsaleフィールドも、入力されていないか存在しない」
$not
この演算子は、指定された式に対して配列の論理否定演算を実行し、クエリ式に一致しないドキュメントを選択します。また、結果にはそのフィールドを含まないドキュメントも含まれます。
一般的な構文は以下のとおり。
{ field: { $not: { <operator-expression> } } }
以下、例を見てみましょう。
db.inventory.find( { price: { $not: { $lt: 3.99 } } } )
このクエリは、以下を含むドキュメントを選択します。
- priceフィールドの値が3.99ドル以上
- priceフィールドが未入力か存在しない
比較演算子
比較演算子を使用すると、1つまたは複数のドキュメント内の値を比較することができます。
以下は、スーパーマーケットの簡易的なinventory(在庫)コレクションのコード例です。
{ _id: 1, item: { name: "apple", code: "123" }, qty: 15, tags: [ "A", "B", "C" ] },
{ _id: 2, item: { name: "banana", code: "123" }, qty: 20, tags: [ "B" ] },
{ _id: 3, item: { name: "spinach", code: "456" }, qty: 25, tags: [ "A", "B" ] },
{ _id: 4, item: { name: "lentils", code: "456" }, qty: 30, tags: [ "B", "A" ] },
{ _id: 5, item: { name: "pears", code: "000" }, qty: 20, tags: [ [ "A", "B" ], "C" ] },
{ _id: 6, item: { name: "strawberry", code: "123" }, tags: [ "B" ] }
この例を使用して、比較演算子について掘り下げていきます。
等しい($eq)
この演算子は、フィールドの値が指定された値と等しいドキュメントを選択します。
{ <field>: { $eq: <value> } }
例えば、inventoryコレクションから数量の値が20のドキュメントを取得するには、以下のようなコマンドを入力します。
db.inventory.find( { qty: { $eq: 20 } } )
このクエリは以下の結果を返します。
{ _id: 2, item: { name: "banana", code: "123" }, qty: 20, tags: [ "B" ] },
{ _id: 5, item: { name: "pears", code: "000" }, qty: 20, tags: [ [ "A", "B" ], "C" ] }
より大きい($gt)
この演算子は、フィールドの値が指定された値よりも大きいドキュメントを選択します。
{ field: { $gt: value } }
以下の例は、数量が15より大きいドキュメントを取得します。
db.inventory.find({"qty": { $gt: 15}})
このクエリは以下の結果を返します。
{ _id: 2, item: { name: "banana", code: "123" }, qty: 20, tags: [ "B" ] }
{ _id: 3, item: { name: "spinach", code: "456" }, qty: 25, tags: [ "A", "B" ] }
{ _id: 4, item: { name: "lentils", code: "456" }, qty: 30, tags: [ "B", "A" ] }
{ _id: 5, item: { name: "pears", code: "000" }, qty: 20, tags: [ [ "A", "B" ], "C" ] }
より小さい($lt)
この演算子は、フィールドの値が指定された値よりも小さいドキュメントを選択します。
{ field: { $lt: value } }
以下の例は、数量が25より少ないドキュメントを検索します。
db.inventory.find({"qty": { $lt: 25}})
このクエリは以下の結果を返します。
{ _id: 1, item: { name: "apple", code: "123" }, qty: 15, tags: [ "A", "B", "C" ] }
{ _id: 2, item: { name: "banana", code: "123" }, qty: 20, tags: [ "B" ] }
{ _id: 5, item: { name: "pears", code: "000" }, qty: 20, tags: [ [ "A", "B" ], "C" ] }
以上($gte)
この演算子は、フィールドの値が指定された値より大きいか等しいドキュメントを選択します。
{ field: { $gte: value } }
以下の例は、数量が25以上のドキュメントを検索します。
db.inventory.find({"qty": { $gte: 25}})
このクエリは以下の結果を返します。
{ _id: 3, item: { name: "spinach", code: "456" }, qty: 25, tags: [ "A", "B" ] }
{ _id: 4, item: { name: "lentils", code: "456" }, qty: 30, tags: [ "B", "A" ] }
以下($lte)
この演算子は、フィールドの値が指定された値より小さいか等しいドキュメントを選択します。
{ field: { $lte: value } }
以下の例は、数量が25以下のドキュメントを検索します。
db.inventory.find({"qty": { $lte: 25}})
このクエリは以下の結果を返します。
{ _id: 1, item: { name: "apple", code: "123" }, qty: 15, tags: [ "A", "B", "C" ] }
{ _id: 2, item: { name: "banana", code: "123" }, qty: 20, tags: [ "B" ] }
{ _id: 3, item: { name: "spinach", code: "456" }, qty: 25, tags: [ "A", "B" ] }
{ _id: 5, item: { name: "pears", code: "000" }, qty: 20, tags: [ [ "A", "B" ], "C" ] }
含む($in)
この演算子は、フィールドの値が指定された値に一致するドキュメントを選択します。
{ field: { $in: [<value1>, <value2>, ... <valueN> ] } }
フィールドの値は、指定された配列内の任意の値と等しくなります。例えば、inventoryコレクションで数量の値が30、または、15のドキュメントを取得するには、以下のクエリを実行します。
db.inventory.find({ "qty": { $in: [30, 15]}})
出力は以下のとおりです。
{ _id: 1, item: { name: "apple", code: "123" }, qty: 15, tags: [ "A", "B", "C" ] }
{ _id: 4, item: { name: "lentils", code: "456" }, qty: 30, tags: [ "B", "A" ] }
含まない($nin)
この演算子は、フィールドの値が指定された値に一致しないドキュメントを選択します。以下は、$nin
演算子の基本構文です。
{ field: { $nin: [ <value1>, <value2> ... <valueN> ]
$nin
は、以下のドキュメントを選択します。
- 「フィールドの値が、指定された配列にない」または
- 「フィールドが存在しない」
フィールドが配列であれば、valueセクションで指定された要素が存在しない配列を選択します。例えば、数量が20、または、15のいずれでもないドキュメントを選択したいとします。
以下のクエリは、qtyフィールドを含まないドキュメントも選択します。
db.inventory.find({ "qty": { $nin: [ 20, 15 ]}})
出力は以下のとおりです。
{ _id: 3, item: { name: "spinach", code: "456" }, qty: 25, tags: [ "A", "B" ] }
{ _id: 4, item: { name: "lentils", code: "456" }, qty: 30, tags: [ "B", "A" ] }
{ _id: 6, item: { name: "strawberry", code: "123" }, tags: [ "B" ] }
等しくない($ne)
$ne
演算子は、指定された値が等しくないドキュメントを返します。
{ $ne: value } }
例えば、数量が20でないすべてのドキュメントを選択したいとします。
db.inventory.find( { qty: { $ne: 20 } } )
出力は以下のようになります。
{ _id: 1, item: { name: "apple", code: "123" }, qty: 15, tags: [ "A", "B", "C" ] }
{ _id: 3, item: { name: "spinach", code: "456" }, qty: 25, tags: [ "A", "B" ] }
{ _id: 4, item: { name: "lentils", code: "456" }, qty: 30, tags: [ "B", "A" ] }
{ _id: 6, item: { name: "strawberry", code: "123" }, tags: [ "B" ] }
上の出力から、このクエリがqtyフィールドのないドキュメントも選択していることがわかります。
要素演算子
要素クエリ演算子は、フィールドを使用してドキュメントを識別できます。要素演算子には、$exist
と$type
があります。
$exists
この演算子は、指定されたフィールドを含むドキュメントを選択します。この演算子には、true
またはfalse
のどちらかを取るブール値があります。
true
を指定すると、フィールドの値がnullのものも含めて、そのフィールドを含むドキュメントを選択します。ブール値がfalse
であれば、クエリはそのフィールドを含まないドキュメントだけを返します。
以下は標準の構文です。
{ field: { $exists: <boolean> } } )
例えば、bagofmarbles(ビー玉の袋)コレクション内の配列データには、それぞれ異なる色のビー玉が入っているとします。
{ red: 5, green: 5, blue: null }
{ red: 3, green: null, blue: 8 }
{ red: null, green: 3, blue: 9 }
{ red: 1, green: 2, blue: 3 }
{ red: 2, blue: 5 }
{ red: 3, green: 2 }
{ red: 4 }
{ green: 2, blue: 4 }
{ green: 2 }
{ blue: 6 }
赤いビー玉を含む袋だけを返すクエリを作成します。ブール値には、以下のようにtrue
を指定します。
db.bagofmarbles.find( { red: { $exists: true } } )
結果は、redフィールドを含むドキュメントになります。これにはredフィールドの値がnull
のものも含まれますが、redフィールドが存在しないドキュメントは含まれません。
{ red: 5, green: 5, blue: null }
{ red: 3, green: null, blue: 8 }
{ red: null, green: 3, blue: 9 }
{ red: 1, green: 2, blue: 3 }
{ red: 2, blue: 5 }
{ red: 3, green: 2 }
{ red: 4 }
赤いビー玉を含まない袋を選択する場合は、以下のようになります。
db.bagofmarbles.find( { red: { $exists: false} )
結果は、redフィールドを含まないドキュメントになります。
{ green: 2, blue: 4 }
{ green: 2 }
{ blue: 6 }
$type
この演算子は、指定されたフィールド型のドキュメントを選択します。高度な非構造化データや、データ型を予測できない場合に便利です。フィールド型はBSON型として指定され、型番号またはエイリアスで定義できます。
$type
の一般的な構文は以下のとおりです。
{ field: { $type: <BSON type> } }
例として、以下のようなドキュメントを含むアドレス帳があるとします。
db={
addressBook: [
{
"_id": 1,
address: "2100 Jupiter Spot",
zipCode: "9036325"
},
{
"_id": 2,
address: "25 Moon Place",
zipCode: 26237
},
{
"_id": 3,
address: "2324 Neptune Ring",
zipCode: NumberLong(77622222)
},
{
"_id": 4,
address: "33 Saturns Moon",
zipCode: NumberInt(117)
},
{
"_id": 5,
address: "1044 Venus Lane",
zipCode: [
"99883637232",
"73488976234"
]
}
]
}
上のドキュメントを見ると、zipCode(郵便番号)フィールドには、long、double、integer、stringと様々なデータ型があります。
郵便番号として指定されたデータ型(この例ではstringとする)を含むドキュメントだけがほしい場合は、コンパイラに以下のクエリを入力します。
db.addressBook.find({
"zipCode": {
$type: "string"
}
})
すると、以下のドキュメントが返されます。
[
{
"_id": 1,
"address": "2100 Jupiter Spot",
"zipCode": "9036325"
},
{
"_id": 5,
"address": "1044 Venus Lane",
"zipCode": [
"99883637232",
"73488976234"
]
}
]
また、すべてのlong、integer、doubleの値を含むnumber(数値)型があります。以下は数値型の郵便番号を含むドキュメントを選択する例です。
db.addressBook.find( { "zipCode" : { $type : "number" } } )
出力は以下のとおりです。
[
{
"_id": 2,
address: "25 Moon Place",
zipCode: 26237
},
{
"_id": 3,
address: "2324 Neptune Ring",
zipCode: NumberLong(77622222)
},
{
"_id": 4,
address: "33 Saturns Moon",
zipCode: NumberInt(117)
}
]
ドキュメントに配列のフィールド型が含まれる場合、$type
演算子は、少なくとも1つの配列の要素が$type演算子に指定された型と一致するドキュメントを選択します。
配列演算子
MongoDBには配列演算子もあり、配列を含むドキュメントを照会できます。
主な演算子には、$all
、$elemMatch
、$size
の3つがあります。それぞれについて詳しくご説明します。
$all
$all
演算子は、フィールドの値が指定された要素を含む配列のドキュメントを選択します。
{ : { $all: [ <value1> , <value2> ... ] } }
例えば、あるアパレルショップのinventoryコレクションの中に、以下のドキュメントがあるとします。
{
_id: ObjectId("5234cc89687ea597eabee675"),
code: "shirt",
tags: [ "sale", "shirt", "button", "y2k", "casual" ],
qty: [
{ size: "S", num: 10, color: "blue" },
{ size: "M", num: 45, color: "blue" },
{ size: "L", num: 100, color: "green" }
]
},
{
_id: ObjectId("5234cc8a687ea597eabee676"),
code: "pant",
tags: [ "y2k", "trendy", "shine" ],
qty: [
{ size: "6", num: 100, color: "green" },
{ size: "6", num: 50, color: "blue" },
{ size: "8", num: 100, color: "brown" }
]
},
{
_id: ObjectId("5234ccb7687ea597eabee677"),
code: "pant2",
tags: [ "trendy", "shine" ],
qty: [
{ size: "S", num: 10, color: "blue" },
{ size: "M", num: 100, color: "blue" },
{ size: "L", num: 100, color: "green" }
]
},
{
_id: ObjectId("52350353b2eff1353b349de9"),
code: "shirt2",
tags: [ "y2k", "trendy" ],
qty: [
{ size: "M", num: 100, color: "green" }
]
}
タグ「y2k」と「trendy」にリンクされたドキュメント(ここでは服)をinventoryから取得したいとします。以下のクエリは、$all
演算子を使用して、tagsフィールドの値が、要素に「y2k」と「trendy」を含む配列となるドキュメントを選択します。
db.inventory.find( { tags: { $all: [ "y2k", "trendy" ] } } )
このクエリを実行すると、以下のドキュメントが返されます。
{
_id: ObjectId("5234cc8a687ea597eabee676"),
code: "pant",
tags: [ "y2k", "trendy", "shine" ],
qty: [
{ size: "6", num: 100, color: "green" },
{ size: "6", num: 50, color: "blue" },
{ size: "8", num: 100, color: "brown" }
]
}
{
_id: ObjectId("52350353b2eff1353b349de9"),
code: "shirt2",
tags: [ "y2k", "trendy" ],
qty: [
{ size: "M", num: 100, color: "green" }
]
}
上の例から、$all
演算子は$and
演算子と同じ機能を果たすこともわかります。
代替として、以下のようなクエリでも同様の出力が得られます。
db.inventory.find({
$and: [
{
tags: "y2k"
},
{
tags: "trendy"
}
]
})
$elemMatch
$elemMatch
演算子は、配列フィールドに指定されたすべてのクエリ条件に一致する要素を、少なくとも1つ含むドキュメントを選択します。
{ : { $elemMatch: { <query1>, <query2>, ... } } }
$elemMatch
の中で$lte
や$gte
のような比較演算子を使うこともできますが、もし単一のクエリ条件のみを指定し、$not
や$ne
演算子を使わないなら、本質的に同じ機能を果たすため$elemMatch
を省略できます。
なお、$elemMatch
演算子を使用する際は、以下の点に注意してください。
$elemMatch
演算子内には$where
式を指定できません。$elemMatch
演算子内には$text
クエリ式を指定できません。
例えば、studentresults(生徒の成績)コレクションに以下のドキュメントが含まれるとします。
{ _id: 1, results: [ 92, 89, 98 ] }
{ _id: 2, results: [ 85, 99, 99 ] }
以下のクエリは、結果の配列に90以上、かつ、95未満の要素が少なくとも1つ含まれるドキュメントのみを選択します。
db.studentresults.find( { results: { $elemMatch: { $gte: 90, $lt: 95 } } })
このクエリは、以下のドキュメントを返します。要素92は、90以上、かつ、95未満です。
{ "_id" : 1, "results" :[ 92, 89, 98 ] }
$size
$size
演算子は、配列のサイズが引数で指定された要素数と一致するドキュメントを返します。
{ field: { $size: value } }
以下のようになります。
db.collection.find( { field: { $size: 2 } });
このクエリは、指定されたコレクションの中から、「field」が2つの要素の配列である、すべてのドキュメントを返します。{ field: [ orange, apple] }
と{ field: [ blue, red] }
は返されますが、{ field: blue}
や{ field: [ raspberry, lemon, grapefruit ] }
は返されません。
なお、サイズに特定の値を指定することはできますが、値の範囲は指定できません。
地理空間演算子
MongoDBでは、GeoJSON型の形で地理空間データを保存できます。GeoJSONはJavaScriptオブジェクト記法に基づいたオープンスタンダードなフォーマットで、地理的な特徴を表現でき、非空間的な属性もサポートしています。今回は、ジオメトリ指定子とクエリセレクタの2つの種類を見ていきます。
$geometry
この演算子は、地理空間クエリ演算子$geoIntersects
、$geoWithin
、$nearSphere
、$near
で使用されるGeoJSONジオメトリを指定します。$geometry
はデフォルトの座標参照系(CRS)として、EPSG:4326を使用します。
デフォルトのCRSでGeoJSONオブジェクトを指定するには、$geometry
の以下のスニペットを使用します。
$geometry: {
type: "<GeoJSON object type>",
coordinates: [ <coordinates> ]
}
MongoDB CRSで単一リングのGeoJSONポリゴンを指定するには、以下のコードを使用します($geoWithin
と$geoIntersects
に対してのみ使用可能)。
$geometry: {
type: "Polygon",
coordinates: [ <coordinates> ],
crs: {
type: "name",
properties: { name: "urn:x-mongodb:crs:strictwinding:EPSG:4326" }
}
}
$polygon
$polygon
演算子を使用すると、以前の(つまりGeoJSONでない)座標ペアに対する$geoWithin
クエリに、ポリゴン(多角形)を指定することができます。このクエリは、ポリゴンの範囲内にあるペアを返します。ただし、$polygon
はGeoJSONオブジェクトに対しては照会しません。ポリゴンを定義するには、以下のように座標点の配列を指定します。
{
: {
$geoWithin: {
$polygon: [ [ <x1> , <y1> ], [ <x2> , <y2> ], [ <x3> , <y3> ], ... ]
}
}
}
最後の点は最初の点と接続されます。また、点や辺は制限なく指定可能です。
例えば、以下のクエリは、[0,0]、[1,5]、[3,3]で定義されるポリゴン内に存在する座標を含む、すべてのドキュメントを返します。
db.places.find(
{
loc: {
$geoWithin: { $polygon: [ [ 0 , 0 ], [ 1 , 5 ], [ 3 , 3 ] ] }
}
}
)
$geoWithin
この演算子を使用すると、特定の形状内に完全に収まっている地理空間データを含むドキュメントを選択できます。指定可能な形状は、GeoJSONマルチポリゴン、GeoJSONポリゴン(複数のリングまたは単一のリング)、または従来の座標ペアで定義できるもののいずれかです。
$geoWithin
演算子は$geometry
演算子を利用して、GeoJSONオブジェクトを指定します。
デフォルトの座標参照系(CRS)を介して、GeoJSONマルチポリゴンまたはGeoJSONポリゴンを指定するには、以下の構文を使用します。
{
: {
$geoWithin: {
$geometry: {
type: <"Polygon" or "MultiPolygon"> ,
coordinates: [ <coordinates> ]
}
}
}
}
デフォルトのCRSを使用して、$geoWithin
クエリに半球より大きな領域のGeoJSONジオメトリを指定すると、補完的なジオメトリ(指定されたポリゴンを除いた全範囲)に対するクエリへと変換されます。
独自のMongoDB CRSを使用して、単一リングのGeoJSONポリゴンを指定するには、以下のような$geometry
式を使用できます。
{
: {
$geoWithin: {
$geometry: {
type: "Polygon" ,
coordinates: [ <coordinates> ],
crs: {
type: "name",
properties: { name: "urn:x-mongodb:crs:strictwinding:EPSG:4326" }
}
}
}
}
}
以下の例は、半球よりも小さなポリゴン領域のGeoJSONポリゴン内に完全に含まれる、すべてのlocデータを選択します。
db.places.find(
{
loc: {
$geoWithin: {
$geometry: {
type : "Polygon" ,
coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ] ]
}
}
}
}
)
$box
地理空間的な$geoWithin
クエリに$box
を使用して矩形を指定すると、点を基準とする位置データに基づき、その矩形の範囲内にあるドキュメントを選択できます。$geoWithin
を$box
と使用すると、クエリの座標に基づいてドキュメントを取得することができます。この場合、$geoWithin
はGeoJSONの形状を照会しません。
$box
演算子を利用するには、配列オブジェクトで矩形の右上と左下の角を指定します。
{ <location field> : { $geoWithin: { $box: [ [ <bottom left coordinates> ],
[ <upper right coordinates> ] ] } } }
前述のクエリは、平面(フラット)ジオメトリを利用して距離を計算します。以下のクエリは、点[0,0]、[0,30]、[30,0]、[30,30]からなる矩形の中のすべてのドキュメントを返します。
db.places.find ( {
loc: { $geoWithin: { $box: [ [ 0,0 ], [ 30,30 ] ] } }
} )
$nearSphere
地理空間クエリに$nearSphere
を使用してある点を指定すると、その点から最も近い点から最も遠い点までに含まれるドキュメントを選択できます。
MongoDBは、球体幾何学を使用して$nearSphere
の距離を計算します。これには以下の地理空間インデックスが必要になります。
- 2dインデックス:従来の座標ペアとして記述された位置データ用(GeoJSONの点で2dインデックスを利用するには、GeoJSONオブジェクトのcoordinates(座標)フィールドにインデックスを生成する必要がある)
- 2dsphereインデックス:GeoJSONの点として記述される位置情報データ用
GeoJSONの点を指定するには、以下の構文を使用します。
{
$nearSphere: {
$geometry: {
type : "Point",
coordinates : [ <longitude>, <latitude> ]
},
$minDistance: <distance in meters>,
$maxDistance: <distance in meters>
}
}
$minDistance
と$maxDistance
は任意で、$minDistance
は、結果を中心から指定した距離以上離れているドキュメントに限定できます。$maxDistance
はどちらのインデックスにも使用可能です。
例として、2dsphereインデックスの付いたlocationフィールドを含むドキュメントからなるplaces(場所)コレクションがあるとします。以下の構文は、選択した点から少なくとも2,000メートル、最大6,000メートルの位置にある点を近いものから順に返します。
db.places.find(
{
location: {
$nearSphere: {
$geometry: {
type : "Point",
coordinates : [ -43.9532, 50.32 ]
},
$minDistance: 2000,
$maxDistance: 6000
}
}
}
)
$geoIntersects
$geoIntersects
演算子を使用すると、地理空間データが特定のGeoJSONオブジェクトと交差する、つまり指定したオブジェクトとデータの収束が空でないドキュメントを選択できます。GeoJSONオブジェクトを指定するには、$geometry
演算子を利用します。
デフォルトの座標参照系(CRS)を介して、GeoJSONマルチポリゴンまたはポリゴンを指定するには、以下の構文を使用します。
{ <location field>: {
$geoIntersects: {
$geometry: {
type: "<GeoJSON object type>" ,
coordinates: [ <coordinates> ]
}
}
}
}
以下の例では$geoIntersects
を使用して、座標配列で記述されたポリゴンと交差するすべてのlocデータを選択します。
db.places.find(
{
loc: {
$geoIntersects: {
$geometry: {
type: "Polygon" ,
coordinates: [
[ [ 0, 0 ], [ 2, 6 ], [ 4, 1 ], [ 0, 0 ] ]
]
}
}
}
}
)
$center
$geoWithin
クエリに$center
演算子を使用して円を指定すると、その円の範囲内にある従来の座標ペアが返されます。
$center
は、GeoJSONオブジェクトを返しません。$center
演算子を利用するには、以下を含む配列を指定します。
- 座標系で使用される単位で測定された円の半径
- 円の中心点のグリッド座標
{
<location field> : {
$geoWithin: { $center: [ [ <x> , <y> ] , <radius> ] }
}
}
以下のクエリは、[2,3]を中心とした半径40の円内で見つかる座標を含む、すべてのドキュメントを返します。
db.places.find(
{ loc: { $geoWithin: { $center: [ [2, 3], 40 ] } } }
)
射影演算子
射影演算子を使用すると、操作によって返されるフィールドを指定できます。MongoDBの射影演算子では、データフィルタリング用の引数としてfind()
関数を使用できます。これにより、ドキュメントから必要なデータフィールドのみを抽出できます。つまり、データベース全体のパフォーマンスに影響を与えることなく、透過的で簡潔なデータを射影することができます。
$elemMatch(射影)
$elemMatch
演算子は、クエリ結果のフィールドの内容を$elemMatch
条件に合致する最初の要素のみに制限します。
$elemMatch
を使用する前に、以下の点に留意してください。
- MongoDB 4.4以降については、ドキュメント内のフィールドの順序に関係なく、既存のフィールドの
$elemMatch
射影は、他の既存のフィールドの後にフィールドを返します。 $elemMatch
演算子と$
演算子は、どちらも指定された条件に基づいて配列から最初に一致した要素を射影します。$
演算子は、クエリ文の条件に基づいて、コレクション内のすべてのドキュメントから最初に一致する配列要素を射影します。一方、$elemMatch
射影演算子は、明示的な条件引数を取ります。これにより、クエリ内にない条件に基づいて射影したり、配列に埋め込まれたドキュメントの様々なフィールドに基づいて射影したりできます。
$elemMatch
演算子をデータで使用する前に、以下の点に留意してください。
$elemMatch
演算子内では$text
クエリ式を指定できません。- ビューに対する
db.collection.find()
操作は、$elemMatch
射影演算子をサポートしていません。
$elemMatch
射影演算子の例として、schools
(学校)コレクションに以下のドキュメントが含まれるとします。
{
_id: 1,
zipcode: "63108",
students: [
{ name: "mark", school: 102, age: 9 },
{ name: "geoff", school: 101, age: 13 },
{ name: "frank", school: 104, age: 12 }
]
}
{
_id: 2,
zipcode: "63110",
students: [
{ name: "harry", school: 103, age: 14 },
{ name: "george", school: 103, age: 7 },
]
}
{
_id: 3,
zipcode: "63108",
students: [
{ name: "harry", school: 103, age: 14 },
{ name: "george", school: 103, age: 7 },
]
}
{
_id: 4,
zipcode: "63110",
students: [
{ name: "jim", school: 103, age: 9 },
{ name: "michael", school: 103, age: 12 },
]
}
以下のクエリは、find()
操作でzipcode(郵便番号)フィールドの値が63110であるすべてのドキュメントを照会します。$elemMatch
射影は、school
フィールドの値が103であるstudents
配列の最初に一致した要素のみを返します。
db.schools.find( { zipcode: "63110" },
{ students: { $elemMatch: { school: 103 } } } )
結果は以下のとおりです。
{ "_id" : 2, "students" : [ { "name" : "harry", "school" : 103, "age" : 14 } ] }
{ "_id" : 4, "students" : [ { "name" : "jim", "school" : 103, "age" : 9 } ] }
$slice(射影)
$slice
射影演算子を使用すると、クエリ結果に返す配列の要素数を指定できます。
db.collection.find(
<query> ,
{ <arrayField> : { $slice: <number> } }
);
次のようにも書けます。
db.collection.find(
<query> ,
{ <arrayField> : { $slice: [ <number> , <number> ] } }
);
例として、以下のドキュメントを使用してツイート(Twitter)のコレクションを作成します。
db.posts.insertMany([
{
_id: 1,
title: "Nuts are not blueberries.",
comments: [ { comment: "0. true" }, { comment: "1. blueberries aren't nuts."} ]
},
{
_id: 2,
title: "Coffee please.",
comments: [ { comment: "0. Indubitably" }, { comment: "1. Cuppa tea please" }, { comment: "2. frappucino" }, { comment: "3. Mocha latte" }, { comment: "4. whatever" } ]
}
])
以下のクエリは、ツイートの配列に$slice
射影演算子を使用して、配列の最初の2つの要素を返します。配列の要素が2つ以下であれば、その配列のすべての要素を返します。
db.posts.find( {}, { comments: { $slice: 2 } } )
以下のドキュメントが返されます。
{
"_id" : 1,
"title" : "Nuts are not blueberries.",
"comments" : [ { "comment" : "0. true" }, { "comment" : "1. blueberries aren't nuts." } ]
}
{
"_id" : 2,
"title" : "Coffee please.",
"comments" : [ { "comment" : "0. Indubitably" }, { "comment" : "1. Cuppa tea please" } ]
}
$(射影)
位置演算子$
は、配列の内容を制限し、その配列のクエリ条件に合致する最初の要素を返します。選択されたドキュメントで特定の配列要素を1つだけ必要とするときは、find()
メソッドやfindOne()
メソッドのドキュメント内で$
を使用できます。
$
演算子の構文は以下のとおりです。
db.collection.find( { <array>: <condition> ... },
{ "<array>.$": 1 } )
db.collection.find( { <array.field>: <condition> ...},
{ "<array>.$": 1 } )
例えば、students
(生徒)コレクションに以下のドキュメントがあるとします。
{ "_id" : 1, "semester" : 2, "grades" : [ 75, 67, 93 ] }
{ "_id" : 2, "semester" : 2, "grades" : [ 60, 68, 72 ] }
{ "_id" : 3, "semester" : 2, "grades" : [ 95, 82, 67 ] }
{ "_id" : 4, "semester" : 3, "grades" : [ 89, 95, 70 ] }
{ "_id" : 5, "semester" : 3, "grades" : [ 68, 98, 82 ] }
{ "_id" : 6, "semester" : 3, "grades" : [ 65, 70, 76 ] }
以下のクエリで、射影{ "grades.$": 1 }
は、grades
(成績)フィールドの89以上の最初の要素のみを返します。
db.students.find( { semester: 2, grades: { $gte: 89 } },
{ "grades.$": 1 } )
以下のドキュメントが返されます。
{"_id": 1, "grades": [93] }
評価演算子
MongoDBの評価演算子を使用すると、ドキュメント内のデータ構造全体や個々のフィールドを評価することができます。
以下、一般的な評価演算子を見ていきます。
$mod
この演算子を使用すると、指定したフィールドの値が、指定した値で割った余りと等しいドキュメントを選択できます。
{ field: { $mod: [ divisor, remainder ] } }
例えば、ショールーム内の様々なブランドの自動車に対応する、carsコレクションがあるとします。以下のクエリを実行すると、在庫数が250の倍数であるすべての自動車ブランドが表示されます。
db.cars.find ( { qty: { $mod: [ 250,0 ] } } )
$jsonSchema
$jsonSchema
を使用すると、指定したJSONスキーマに一致するドキュメントを選択できます。MongoDBのJSONスキーマの実装にはbsonType
キーワードが追加され、$jsonSchema
演算子の中ですべてのBSON型を使用できます。
bsonType
は、型演算子に使用するものと同じ文字列のエイリアスを受け入れます。$jsonSchema
の構文は以下のとおりです。
{ $jsonSchema: <JSON Schema object> }
ここで、JSONスキーマオブジェクトは、JSONスキーマ標準ドラフト4に基づいてフォーマットされます。
{ <keyword1>: <value1>, ... }
以下は、$jsonSchema
の動作を示す例です。
{ $jsonSchema: {
required: [ "name", "major", "gpa", "address" ],
properties: {
name: {
bsonType: "string",
description: "must be a string and is required"
},
address: {
bsonType: "object",
required: [ "zipcode" ],
properties: {
"street": { bsonType: "string" },
"zipcode": { bsonType: "string" }
}
}
}
}
}
また、ドキュメントバリデーションの中で$jsonSchema
を使用すると、更新や挿入操作の際に、指定したスキーマを強制することができます。
db.createCollection(<collection> , { validator: { $jsonSchema: <schema> } } )
db.runCommand( { collMod: <collection>, validator:{ $jsonSchema: <schema> } } )
なお、$jsonSchema
演算子では、以下がサポート対象外です。
- 整数型(BSONのlong型またはint型をbsonTypeキーワードとして利用する必要あり)
- 未知のキーワード
- JSONスキーマのリンクプロパティとハイパーメディア(JSONリファレンスとJSONポインタの利用も含む)
$text
$text
演算子は、指定されたフィールドのコンテンツ内でインデックス付きの文字列を検索します。
{
$text:
{
$search: <string>,
$language: <string>,
$caseSensitive: <boolean>,
$diacriticSensitive: <boolean>
}
}
以下のコードスニペットは、carsコレクションから文字列「Porsche」を含む自動車をフィルタリングします。
db.cars.find( { $text: { $search: "Porsche" } } )
$regex
$regex
演算子を使用すると、クエリ内で正規表現を使用して文字列をパターンマッチできます。MongoDBではPerl互換の正規表現を使用します。
{<field> : /pattern/ <options>}
以下のクエリは、文字列「$78900」を含むすべての自動車をフィルタリングします。
db.cars.find( { price: { $regex: /$78900/ } } )
$expr
$expr
演算子は、クエリ言語内で集計式を活用できます。
{ $expr: { <expression> } }
また$expr
を使用すると、$match
ステージ内で同じドキュメントのフィールドを比較するクエリ式も構築できます。もし$match
ステージが$lookup
ステージの一部なら、$expr
はlet変数でフィールドを比較できます。
$where
$where
演算子を使用すると、完全なJavaScript関数か、式のどちらかを含む文字列をクエリシステムに渡せます。$where
演算子には高い柔軟性がありますが、コレクション内のすべてのドキュメントに対して、データベースがJavaScriptの関数や式を処理できる必要があります。JavaScriptの関数や式では、obj
またはthis
を使用してこのドキュメントを参照できます。
以下は構文例です。
{ $where: <string|JavaScript Code> }
$where
演算子の使用例をご紹介する前に、いくつか注意点があります。
$where
クエリ演算子は先頭レベルのドキュメントにのみ使用してください。$elemMatch
クエリとは異なり、入れ子のドキュメントでは機能しません。$where
は、基本的に他の演算子でクエリを表現できない場合にのみ使用してください。$where
を使わなければならない場合も、少なくとも1つの他の標準的なクエリ演算子を加えて結果をフィルタリングしてください。$where
を単独で使用すると、コレクションスキャンが必要になります。
以下は使用例です。
db.cars.find( { $where: function() {
return (hex_md5(this.name)== "9a43e617b50cd379dca1bc6e2a8")
} } );
ビット演算子
ビット演算子は、ビット位置の条件に基づいてデータを返します。簡単に言えば、ビット位置集合内の任意のビットの数値またはバイナリ値が1か0かの判定に使用されます。
$bitsAllSet
この演算子はフィールド内の、クエリで指定されたすべてのビット位置が1、つまりセットされているすべてのドキュメントとマッチします。
{ <field> : { $bitsAllSet: <numeric bitmask> } }
{ <field> : { $bitsAllSet: < BinData bitmask> } }
{ <field> : { $bitsAllSet: [ <position1> , <position2> , ... ] } }
$bitsAllSet
が現在のドキュメントと一致するには、フィールドの値はBinDataのインスタンスか、数値でなければなりません。
例として、以下のドキュメントを含むコレクションがあるとします。
db.collection.save({ _id: 1, a: 54, binaryValueofA: "00110110" })
db.collection.save({ _id: 2, a: 20, binaryValueofA: "00010100" })
db.collection.save({ _id: 3, a: 20.0, binaryValueofA: "00010100" })
db.collection.save({ _id: 4, a: BinData(0, "Zg=="), binaryValueofA: "01100110" })
以下のクエリは、$bitsAllSet
演算子を使用して、フィールドaのビット位置1と位置5がセットされているかどうかをテストします。このとき最下位のビットは、位置0です。
db.collection.find( { a: { $bitsAllSet: [ 1, 5 ] } })
このクエリは、以下のドキュメントと一致します。
{ "_id" : 1, "a" : 54, "binaryValueofA" : "00110110" }
{ "_id" : 4, "a" : BinData(0,"Zg=="), "binaryValueofA" : "01100110" }
$bitsAllClear
$bitsAllClear
演算子は、クエリで指定されたすべてのビット位置がクリア(つまり0
)であるドキュメントと一致します。
{ <field> : { $bitsAllClear: <numeric bitmask> } }
{ <field> : { $bitsAllClear: < BinData bitmask> } }
{ <field> : { $bitsAllClear: [ <position1> , <position2> , ... ] } }
$bitsAllSet
の例を使用して、$bitsAllClear
の使い方を見てみましょう。以下のクエリは、フィールドaの1番目と5番目のビットがクリアされているかどうかをチェックします。
db.collection.find( { a: { $bitsAllClear: [ 1, 5 ] } } )
このクエリは、以下のドキュメントと一致します。
{ "_id" : 2, "a" : 20, "binaryValueofA" : "00010100" }
{ "_id" : 3, "a" : 20, "binaryValueofA" : "00010100" }
メタ演算子
MongoDBには、クエリの動作や出力を変更する様々なクエリ修飾子があります。ドライバインターフェースの中には、便宜のため、クエリ修飾子をラップするカーソルメソッドを実装するものもあります。
$hint
$hint
は、MongoDB v3.2以降、非推奨になっていますが、Go、Java、Scala、Ruby、SwiftなどのMongoDBドライバでは、まだ使用できる可能性があります。この演算子を使用すると、ドキュメントやインデックス名を指定して、クエリオプティマイザに特定のインデックスを利用するように指示することができます。
また、$hint
演算子を使用して、インデックスの戦略やクエリのパフォーマンスをテストすることも。以下、例を見てみましょう。
db.users.find().hint( { age: 1 } )
この操作では、age
フィールドのインデックスを利用して、コレクション内のすべてのusers
ドキュメントが返されます。
また、以下のいずれかの形式でヒントを指定可能です。
db.users.find()._addSpecial( "$hint", { age : 1 } )
db.users.find( { $query: {}, $hint: { age : 1 } } )
クエリ形にインデックスフィルタが存在する場合、MongoDBは$hint
を無視します。
$comment
$comment
演算子を使用すると、$query
が表示されるあらゆるコンテキストでクエリにコメントを記述することができます。コメントはプロフィールログに出力されるため、コメントを付けるとプロフィールの解釈や追跡が簡単です。
$comment
を利用するには、以下3つの方法があります。
db.collection.find( { <query> } )._addSpecial( "$comment", <comment> )
db.collection.find( { <query> } ).comment( <comment> )
db.collection.find( { $query: { <query> }, $comment: <comment> } )
db.collection.update()
などの他のコンテキストでクエリ式にコメントを記述するには、メタ演算子ではなく$comment
クエリ演算子を利用してください。
$max
$max
値を使用して特定のインデックスに排他的な上限を指定すると、find()
の結果を制約することができます。この演算子は、インデックス内の特定の順番のすべてのキーに対して上限を指定します。
Mongoshには、以下のようなmax()
ラッパーメソッドがあります。
db.collection.find( { <query> } ).max( { field1: <max value> , ... fieldN: <max valueN> } )
また、以下2つの形式でも$max
を指定可能です。
db.collection.find( { <query> } )._addSpecial( "$max", { field1: <max value1> ,
... fieldN: <max valueN> } )
db.collection.find( { $query: { <query> }, $max: { field1: <max value1> ,
... fieldN: <max valueN> } } )
インデックス{ age: 1 }
を含む「collection」コレクションに排他的上限を指定するには、以下のように操作します。
db.collection.find( { <query> } ).max( { age: 100 } ).hint( { age: 1 } )
この操作は、age
フィールドが100未満のドキュメントにクエリを限定し、{ age: 1 }
インデックスをminKey
から100まででスキャンするクエリプランを強制的に作成します。
$explain
この演算子を使用すると、クエリプランに関する情報を取得できます。すなわち、クエリの実行に使用されるインデックスとプロセスを記述したドキュメントが返されます。これは、クエリを最適化する際に便利です。
以下のいずれかの形式で$explain
を指定することができます。
db.collection.find()._addSpecial( "$explain", 1 )
db.collection.find( { $query: {}, $explain: 1 } )
MongoDB演算子のベストプラクティス
最後に、MongoDB演算子を使用する際のベストプラクティスをご紹介します。
埋め込みと参照
埋め込みはデータモデリングの自然なかたちでの拡張と言うことができるでしょう。埋め込みを使用するとアプリケーションの結合を回避でき、結果として更新やクエリを減らすことができます。
1つのドキュメント内に「1:1」の関係のデータを埋め込めます。また親ドキュメント内に多くのオブジェクトが現れる「多:1」の関係のデータも、埋め込みの良い対象となります。
このような多量のデータは、同じドキュメント内に格納したほうが良いように思われますが、データの局所性がある読み取り操作では、埋め込みの方がパフォーマンスが向上します。
また、単一ドキュメントの書き込みはトランザクション操作になるため、埋め込みデータモデルでは関連するデータを一度の書き込みで更新でき、開発作業が効率化できます。
一方、以下のような状況では、参照の利用を検討してください。
- ドキュメントの一部の更新のみに時間がかかり、他の部分の更新がない。
- ドキュメントを照会しても、一部のデータはほとんど使用されない。データを埋め込むとメモリ要件が増えるため参照を使用すること。
- ドキュメントのサイズがMongoDBの16MBの制限を超える。「多:1」の関係(例「社員:部署」)をモデリングすると発生する可能性がある。
プロファイリングとクエリパターンの検討
パフォーマンスを最適化する最初のステップとして、実際のクエリパターンと予想されるクエリパターンを把握することが重要です。アプリケーションのクエリパターンが十分に理解できれば、データモデルを作成し適切なインデックスを選択することができます。
MongoDBの開発者向けに、パフォーマンスの向上を目的とした高度なツールがありますが、クエリプロファイルやパターンを無視できるわけではありません。
パフォーマンスを高める簡単な方法の1つとして、クエリパターンの分析とデータの埋め込み場所を把握することが挙げられます。あるいは、主なクエリパターンを特定した上で、以下を行うのも手です。
- 照会する対象フィールドにインデックスがあることを確認する
- 頻繁に行うサブクエリの結果をドキュメントに保存して、読み込み負荷を軽減する
- ログを参照して遅いクエリを調べ、インデックスをチェックする
データのインデックスとモデリングのレビュー
データモデルを作成する際には、データ間の関係をどのようにモデリングするかを決定することになります。アプリケーション固有の考慮事項としては、例えばドキュメントへの埋め込みにするか、あるいは異なるコレクションにある別々のドキュメント間で参照を作成するかの選択です。
JSONドキュメントの大きなメリットは、アプリケーションの要件に基づいてデータをモデリングできることです。サブドキュメントや配列の入れ子を使用すると、シンプルなテキストドキュメントを活用して、データ間の複雑な関係をモデリングできます。
MongoDBを使用して、以下のモデリングも行うことができます。
- 地理空間データ
- 表構造、フラット構造、柱状構造
- シンプルなキーとバリューのペア
- 時系列データ
- 連結グラフデータ構造などのエッジとノード
シャーディングとレプリケーションの監視
レプリケーションは、パフォーマンスの向上において極めて重要です。水平方向のスケーリングによってデータの可用性が高まり、冗長性によって優れたパフォーマンスと高いセキュリティを実現することができます。
パフォーマンス監視をスムーズに行うには、追加のリソースと時間を要します。そこで役立つのが、特定の要件を満たす既存のパフォーマンス監視ツールです。
例えば、Kinsta APMは無料で利用でき、WordPressサイトのMySQLデータベースクエリ、PHPプロセス、外部HTTPコールなどに関するタイムスタンプ付きの情報を取得することができます。また、以下のようなデバッグも可能です。
- 時間のかかるAPI呼び出し
- 時間のかかっている外部URLリクエスト
- 低速なデータベースクエリ
MongoDBのレプリケーションの実装では、レプリカセットを使用してプライマリノードやサーバーのデータを、複数のセカンダリにコピーします。クエリの一部をプライマリではなくセカンダリで実行することで、競合を回避し、より良い負荷分散を実現できます。
MongoDBのシャーディングクラスタもパフォーマンスの向上に有用です。レプリケーションと同様にシャーディングを使用すると、大きなデータセットを複数のサーバーに分散することができます。
シャードキーを利用すると、シャードやデータの断片を複数のサーバーにコピーすることができます。複数のサーバーが稼働し、すべてのデータをカバーします。
シャーディングには、書き込み/読み込みの水平スケーリング、可用性の向上、ストレージ容量の増加など、さまざまな利点があります。
メモリ使用量の把握
MongoDBは、アプリケーションのワーキングセット(頻繁にアクセスされるデータやインデックスなど)が問題なくメモリに収まる場合に最高のパフォーマンスを発揮します。パフォーマンスでは他の要素も重要ですが、インスタンスのサイジングにおいてはRAMサイズが最も重要になります。
アプリケーションのワーキングセットがRAMに収まる場合は特に問題はありませんが、ワーキングセットがインスタンスサーバーのRAMやサイズを超えると、読み取り操作がかなり重要になります。
このような状況に立たされた場合は、より多くのメモリを搭載した大きなインスタンスに移行することで解決するかもしれません。
マルチバリューフィールドの末尾への配置
もし複数のフィールドにインデックスを作成し、照会したいフィールドの1つが「マルチバリュー」演算子の1つを使用している場合は、この演算子はインデックスの最後に配置してください。インデックスの並びは、まず値でクエリされるフィールドを最初に置き、「マルチバリュー」演算子が最後に来るようにします。
例外はフィールドのソート。「マルチバリュー」フィールドと値でクエリされるフィールドの間にソートを配置すると、必要なメモリ内ソート量を削減することができます。
まとめ
MongoDB最大の強みはスピードにあります。演算子を活用して数学的、論理的なタスクを実行し、クエリを高速で返します。そのような理由から、MongoDBを使いこなすためには、MongoDBの演算子を理解することが重要です。
この記事では、主要なMongoDB演算子、例えば比較演算子、論理演算子、メタ演算子、射影演算子などをご紹介しました。また、演算子の使用例やベストプラクティスも参考にしてみてください。
あなたがよく使用するMongoDBの演算子とその理由は何ですか?以下のコメント欄でぜひお聞かせください。
コメントを残す