💎

F#でMongoDBクライアント操作実験 - クエリ式でのアクセスとコレクションのJOIN

2024/10/30に公開

F#でMongoDBクライアント操作実験 - クエリ式でのアクセスとコレクションのJOIN

ここではF#MongoDBのクライアントを起動し、クエリ式でアクセスし、さらに2つのコレクションのJOINを実験的にやってみます。MongoDBの公式ドライバはC#向けとなっていますので、F#はサポート外でしょうが、同じ.NET環境でのプログラミングが可能なF#であえて実験してみようというわけです。

F#について

F#はC#と同じくMicrosoftの.NET環境で利用できるプログラミング言語です。C#と同じオブジェクト指向プログラミングとOCamlから派生した関数型プログラミングそれぞれの特徴を持っています。

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のコレクションにドキュメントを追加
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メソッドを実行しておくのがポイントです。

MongoDBにクエリ式でアクセス
// クエリ式でアクセスするコレクション
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 { ... }がネストします。

bookmark.tags(seq)の要素が一致しているドキュメントを検索
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のコレクションにドキュメントを追加
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メソッドを実行してクエリ式に対応させれば準備完了です。

2つのコレクションをクエリ式で結合
let mpa_docs = mt_pref_all.AsQueryable()
let mppa_docs = mt_pref_pos_all.AsQueryable()

ドキュメントの結合はクエリ式のjoin ドキュメント in コレクション on (条件式)で行います。改行する場合はonにインデントをつけます。条件式は2つのコレクションを結合する条件を定義します。結合後の形式はselectで定義します。とりあえず結合対象となるドキュメントをタプルにまとめて出力してみます。

2つのコレクションをクエリ式で結合
// 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をインデント)
  • クエリ式の結果は匿名レコード型も可
脚注
  1. 日本 都道府県マスター データセット(mt_pref_all.csv.zipを展開したmt_pref_all.csv) ↩︎

  2. 日本 都道府県マスター位置参照拡張 データセット(mt_pref_pos_all.csv.zipを展開したmt_pref_pos_all.csv) ↩︎

  3. アドレス・ベース・レジストリ (フォーマット仕様や利用規約など) ↩︎

Discussion