F#でMongoDBクライアント操作実験 - クエリ式でのアクセスとコレクションのJOIN
F#でMongoDBクライアント操作実験 - クエリ式でのアクセスとコレクションのJOIN
ここではF#でMongoDBのクライアントを起動し、クエリ式でアクセスし、さらに2つのコレクションのJOINを実験的にやってみます。MongoDBの公式ドライバはC#向けとなっていますので、F#はサポート外でしょうが、同じ.NET環境でのプログラミングが可能なF#であえて実験してみようというわけです。
F#について
MongoDBについて
MongoDBはリレーショナルデータベースとは異なる形式のデータを扱うNoSQLデータベースのひとつです。データベースにはコレクションと呼ばれるデータの保存場所が作成され、そこにJSONのような階層化された構造を持つデータであるドキュメントが保存されます。そして保存されたデータに対しては検索などの操作ができます。
ここではPCにインストール可能なMongoDB Community Editionを利用しますが、クラウドサービスのMongoDB Atlasも提供されています。
実行環境について
F#とMongoDBの実行環境についてはF#でMongoDBクライアント操作実験 - クライアントの起動からドキュメント操作までをご覧ください。そして使用するデータについてはF#でMongoDBクライアント操作実験 - CSVファイルを読み込んでコレクションに保存もご覧ください。そしてこちらではドライバAPIの非同期メソッドを使用することにします。
F#のクエリ式について
F#のクエリ式は、C#の統合言語クエリ (LINQ)と同様に、複数のデータを保持するオブジェクトに対しSQLに似た構文を使用してアクセスするものです。ただしF#の場合はクエリ式の部分をquery { ... }
で囲むことと、結果がseq
になることに注意してください。
let scores = [73; 85; 91; 78; 95]
query {
for score in scores do
where (score > 80)
select score
} |> printfn "%A" // seq [85; 91; 95]
MongoDBのコレクションにクエリ式でアクセス
まずMongoDBのコレクションにデータを追加しておきます。データはF#でMongoDBクライアント操作実験 - クライアントの起動からドキュメント操作までと同じものを使用します。
MongoDBのコレクションにドキュメントを追加
#if INTERACTIVE
#r "nuget: MongoDB.Driver"
#endif
open MongoDB.Driver
open MongoDB.Bson
open MongoDB.Bson.Serialization.Attributes
// ドキュメントの型
type Bookmark = {
[<BsonId>] id: ObjectId
title: string
url: string
tags: string seq
}
// コレクションに保存されるデータ
let bm = [
{
id = ObjectId.GenerateNewId() // id生成
title = "MongoDB"
url = "https://www.mongodb.com"
tags = seq {"MongoDB"; "Database"}
}
{
id = ObjectId.GenerateNewId()
title = "F# ドキュメント"
url = "https://learn.microsoft.com/ja-jp/dotnet/fsharp"
tags = seq {"Fsharp"; "Programming"}
}
{
id = ObjectId.GenerateNewId()
title = "pg_walker"
url = "https://zenn.dev/pgwalker"
tags = seq {"Fsharp"; "Zig"; "Programming"}
}
]
let db_name = "fsmongo" // データベース名
let col_name = "bookmark" // コレクション名
// MongoDBのクライアント起動
let client = new MongoClient()
// データベースの取得
let db = client.GetDatabase(db_name)
// コレクションの取得
let col = db.GetCollection<Bookmark>(col_name)
// コレクションにドキュメントを追加
col.InsertManyAsync(bm).Wait()
printfn $"{col_name}に{col.EstimatedDocumentCountAsync().Result}件のドキュメントが保存されました"
bookmarkに3件のドキュメントが保存されました
client.DropDatabase(db_name)
コレクションにドキュメントを追加できたらクエリ式でアクセスしてみます。まずドキュメントのtitle
をソートしてから取得してみます。クエリ式で用いるコレクションにはAsQueryable
メソッドを実行しておくのがポイントです。
// クエリ式でアクセスするコレクション
let bookmarks = col.AsQueryable()
// クエリ式でMongoDBにアクセス
query {
for bookmark in bookmarks do // bookmarksが持つ要素の1つをbookmarkとする
sortBy bookmark.title // bookmark.titleをソートする
select bookmark.title // bookmark.titleを要素とするseqを生成
} |> Seq.iter (fun title -> printfn "%s" title) // クエリ式で取得したbookmark.titleを出力
// 結果
// F# ドキュメント
// MongoDB
// pg_walker
クエリ式の結果はseq
ですが、select
による結果は様々な型に対応します。以下はタプルにした例です。
query {
for bookmark in bookmarks do // bookmarksが持つ要素の1つをbookmarkとする
sortBy bookmark.title // bookmark.titleをソートする
select (bookmark.title, bookmark.url) // 2つの要素を持つタプルを持つseqを生成
} |> Seq.iter (fun (title, url) -> printfn $"""<a href="{url}">{title}</a>""")
// 結果
// <a href="https://learn.microsoft.com/ja-jp/dotnet/fsharp">F# ドキュメント</a>
// <a href="https://www.mongodb.com">MongoDB</a>
// <a href="https://zenn.dev/pgwalker">pg_walker</a>
複数の要素をもつbookmark.tags
に一致するものがあるかをexists
で検索することもできます。このときはquery { ... }
がネストします。
query {
for bookmark in bookmarks do
where (query {
for tag in bookmark.tags do
exists (tag = "Programming")
})
select bookmark.title
} |> Seq.iter (fun s -> printfn "%s" s)
// 結果
// F# ドキュメント
// pg_walker
2つのコレクションをJOIN
次は2つのコレクションのJOINを実行してみます。データはF#でMongoDBクライアント操作実験 - CSVファイルを読み込んでコレクションに保存と同じもの(出典[1][2][3])を使用します。
MongoDBのコレクションにドキュメントを追加
open System
open System.IO
open System.Text.Json
open MongoDB.Driver
open MongoDB.Bson
open MongoDB.Bson.Serialization.Attributes
type MtPref = {
[<BsonId>]
id: ObjectId
lg_code: string
pref: string
pref_kana: string
pref_roma: string
efct_date: DateTime // MongoDB内ではUTC (ToLocalTime可)
}
type MtPrefPos = {
[<BsonId>]
id: ObjectId
lg_code: string
rep_lon: decimal
rep_lat: decimal
}
let getLineItems file_name =
// ファイルのパス
let file_path = $"/path/to/{file_name}.csv"
// ファイルを1行ずつ読み込み、文字列をカンマ区切りで分割したリストを生成する関数(ファイルを読み終えるまで実行)
let rec parseLineItems (reader: StreamReader) =
match reader.ReadLine() with
| null -> []
| "" -> []
| s -> (Array.toList (s.Split(','))) :: parseLineItems reader
// ファイルの読み込み開始
let file = new StreamReader(file_path)
// ヘッダ行を読み飛ばす
let _ = file.ReadLine()
// リストの生成
let line_items = parseLineItems file
// ファイルを閉じる
file.Close()
// 関数の実行結果
line_items
let db_name = "fsmongo" // データベース名
// MongoDBのクライアント起動
let client = new MongoClient()
// データベースの取得
let db = client.GetDatabase(db_name)
// コレクションの取得(都道府県マスター)
let mt_pref_all = db.GetCollection<MtPref>("mt_pref_all")
// コレクションの取得(都道府県マスター位置情報拡張)
let mt_pref_pos_all = db.GetCollection<MtPrefPos>("mt_pref_pos_all")
// 都道府県マスターのドキュメントをコレクションに追加
let task1 = "mt_pref_all"
|> getLineItems
|> List.map (fun items ->
{
id = ObjectId.GenerateNewId() // idを生成
lg_code = items[0] // 文字列リストをレコードの各項目に設定
pref = items[1]
pref_kana = items[2]
pref_roma = items[3]
efct_date = DateTime.Parse items[4] // 日付を文字列からDateTimeに変換
})
|> mt_pref_all.InsertManyAsync
task1.Wait()
// 都道府県マスター位置情報拡張のドキュメントをコレクションに追加
let task2 = "mt_pref_pos_all"
|> getLineItems
|> List.map (fun items ->
{
id = ObjectId.GenerateNewId() // idを生成
lg_code = items[0] // 文字列リストをレコードの各項目に設定
rep_lon = decimal items[1] // 文字列からdecimal型に変換
rep_lat = decimal items[2]
})
|> mt_pref_pos_all.InsertManyAsync
task2.Wait()
printfn $"mt_pref_allに{mt_pref_all.EstimatedDocumentCountAsync().Result}件のドキュメントが保存されました"
printfn $"mt_pref_pos_allに{mt_pref_pos_all.EstimatedDocumentCountAsync().Result}件のドキュメントが保存されました"
mt_pref_allに47件のドキュメントが保存されました
mt_pref_pos_allに47件のドキュメントが保存されました
client.DropDatabase(db_name)
JOINする2つのコレクションはどちらもlg_code
を持っていますので、これが一致するドキュメントどうしを結合していきます。2つのコレクションそれぞれにAsQueryable
メソッドを実行してクエリ式に対応させれば準備完了です。
let mpa_docs = mt_pref_all.AsQueryable()
let mppa_docs = mt_pref_pos_all.AsQueryable()
ドキュメントの結合はクエリ式のjoin ドキュメント in コレクション on (条件式)
で行います。改行する場合はon
にインデントをつけます。条件式は2つのコレクションを結合する条件を定義します。結合後の形式はselect
で定義します。とりあえず結合対象となるドキュメントをタプルにまとめて出力してみます。
// 2つのドキュメントに共通するlg_codeを持つドキュメントどうしを結合
query {
for mpa in mpa_docs do // 結合するコレクション1
join mppa in mppa_docs // 結合するコレクション2
on (mpa.lg_code = mppa.lg_code) // 結合の条件
select (mpa, mppa) // 結果の形式(タプル)
} |> Seq.iter (fun s -> printfn "%A" s)
({ id = 6721dcaf484e6d24bbcde97b
lg_code = "010006"
pref = "北海道"
pref_kana = "ホッカイドウ"
pref_roma = "Hokkaido"
efct_date = 1947/04/16 15:00:00 }, { id = 6721dcb8484e6d24bbcde9aa
lg_code = "010006"
rep_lon = 141.347906782M
rep_lat = 43.0639406375M })
..... (省略) .....
({ id = 6721dcaf484e6d24bbcde9a9
lg_code = "470007"
pref = "沖縄県"
pref_kana = "オキナワケン"
pref_roma = "Okinawa"
efct_date = 1947/04/16 15:00:00 }, { id = 6721dcb8484e6d24bbcde9d8
lg_code = "470007"
rep_lon = 127.680975M
rep_lat = 26.212365M })
結合後は項目数を絞った匿名レコード型にできます。もちろん事前にtype
で定義したレコード型も可能です。
query {
for mpa in mpa_docs do
join mppa in mppa_docs
on (mpa.lg_code = mppa.lg_code)
select {|
lg_code = mpa.lg_code
pref = mpa.pref
rep_lat = mppa.rep_lat
rep_lon = mppa.rep_lon
|}
} |> Seq.iter (fun s -> printfn "%A" s)
{ lg_code = "010006"
pref = "北海道"
rep_lat = 43.0639406375M
rep_lon = 141.347906782M }
..... (省略) .....
{ lg_code = "470007"
pref = "沖縄県"
rep_lat = 26.212365M
rep_lon = 127.680975M }
まとめ
- MongoDBのコレクションに対しクエリ式でのアクセスが可能
- クエリ式は
query { ... }
で囲まれ、結果はseq
になる - クエリ式で用いるコレクションには
AsQueryable
メソッドを実行しておく - クエリ式の結果の形式は
select
で設定できる - クエリ式の
exists
ではクエリ式がネストする - 2つのコレクションを結合するときはクエリ式の
join ... on ...
を設定する(改行時はon
をインデント) - クエリ式の結果は匿名レコード型も可
-
日本 都道府県マスター データセット(
mt_pref_all.csv.zip
を展開したmt_pref_all.csv
) ↩︎ -
日本 都道府県マスター位置参照拡張 データセット(
mt_pref_pos_all.csv.zip
を展開したmt_pref_pos_all.csv
) ↩︎ -
アドレス・ベース・レジストリ (フォーマット仕様や利用規約など) ↩︎
Discussion