💎

F#でMongoDBクライアント操作実験 - クライアントの起動からドキュメント操作まで

2024/10/15に公開

ここではF#MongoDBのクライアントを起動し、MongoDBのデータベースに接続して基本的なドキュメント操作(挿入、検索、更新、削除)を実験的にやってみます。MongoDBの公式ドライバはC#向けとなっていますので、F#はサポート外でしょうが、同じ.NET環境でのプログラミングが可能なF#であえて実験してみようというわけです。

F#について

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

MongoDBについて

MongoDBはリレーショナルデータベースとは異なる形式のデータを扱うNoSQLデータベースのひとつです。データベースにはコレクションと呼ばれるデータの保存場所が作成され、そこにJSONのような階層化された構造を持つデータであるドキュメントが保存されます。そして保存されたデータに対しては検索などの操作ができます。

ここではPCにインストール可能なMongoDB Community Editionを利用しますが、クラウドサービスのMongoDB Atlasも提供されています。

実行環境について

以下がインストール済みで、F#のプログラムを実行する前にMongoDBサーバのインストールmongodコマンドによるサーバの起動は済んでいるものとします。

そしてF#の実行環境は以下の2つがあります。MongoDBサーバへのアクセスはどちらでも可能です。

  • 対話型実行環境(REPL)
  • dotnetコマンドで生成するビルド環境

F#の対話型実行環境(REPL)

dotnet fsiコマンドを実行すると対話型実行環境(REPL)が起動されます。ここでソースコードを入力し、末尾に(コメント//の場合は改行してから);;をつけてEnterキーを押すと記述した分のソースコードが実行されます。複数行をまとめて入力してから実行することもできます。

> dotnet fsi

Microsoft (R) F# インタラクティブ バージョン F# 9.0 のための 12.9.100.0
Copyright (C) Microsoft Corporation. All rights reserved.

ヘルプを表示するには次を入力してください: #help;;

> printfn "Hello, F#!";;    (ソースコードを入力してEnter)
Hello, F#!                  (出力結果)
val it: unit = ()           (実行したプログラムからの戻り値)

>

また、ソースコードをファイル名.fsxに保存してdotnet fsi ファイル名.fsxのように実行することもできます。

dotnetコマンドで生成するビルド環境

dotnetコマンドによりビルド環境を生成し、そこでプログラムのコンパイルと実行ができます。

ビルド環境の構築とプログラムの実行
> dotnet new console -lang f# -o fsmongo    (ビルド環境をfsmongoディレクトリ内に生成)
> cd fsmongo                                (ビルド環境に移動)
> dotnet add package MongoDB.Driver         (ビルド環境にMongoDBドライバを追加)

..... Program.fs(ソースコードファイル)を編集 .....

> dotnet run                                (プログラム実行)

クライアントの起動と接続

まずはMongoDBクライアントの起動と接続をしてみます。そのためにはMongoDBClientのインスタンスを生成します。MongoDBの接続先はデフォルトとします。ただし、これから以下すべてにおいて非同期処理は扱わないものとします。

MongoDBクライアントの起動と接続
#if INTERACTIVE
#r "nuget: MongoDB.Driver"      // 対話型実行環境で実行される部分(MongoDBドライバの追加)
#endif
open MongoDB.Driver

let client = MongoClient()

データベース名一覧の取得

現時点で作成済みであるデータベース名の一覧を取得してみます。

client.ListDatabaseNames().ToList() |> printfn "%A"
// seq ["admin"; "config"; "local"]         (これらはインストール直後に作成済み)

データベースの取得

ここで使用するデータベースを取得します。この時点でデータベースが存在していればすぐアクセス可能ですが、まだ存在していない場合はコレクションとドキュメントを追加するときにデータベースも作成されます。

let db = client.GetDatabase("fsmongo")

コレクションの取得

コレクションを取得するときはクラスやレコード型でドキュメントの形式を指定します。この時点でコレクションが作成済みであればすぐアクセス可能ですが、そうでなければドキュメントがコレクションに追加されるときに作成されます。

open MongoDB.Bson
open MongoDB.Bson.Serialization.Attributes

// ドキュメントの形式(ここではレコード型)
type Bookmark = {
    [<BsonId>] id: ObjectId
    title: string
    url: string
    tags: string seq
}

// コレクションの取得
let col = db.GetCollection<Bookmark>("bookmark")

ドキュメントの追加

取得したコレクションにドキュメントを保存してみます。保存は複数一括か、あるいは1件ずつ実行できます。コレクションに保存されたデータの件数はEstimatedDocumentCountメソッドで実行できます。

ドキュメントの追加
// コレクションに追加するドキュメント
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"}
    }
]

// 複数のドキュメントを一括で追加
col.InsertMany(bm)

// ドキュメントを1件ずつ追加するとき
// col.InsertOne(bm[0])
// col.InsertOne(bm[1])
// col.InsertOne(bm[2])

// コレクションに保存されているドキュメントの数
col.EstimatedDocumentCount() |> printfn "EstimatedDocumentCount: %A"

idを自動生成するには

ドキュメントのidを自動的に生成する場合は、その項目にIdGeneratorを設定します。その種類はMongoDB.Bson.Serialization.IdGeneratorsに定義されています。このときドキュメントの各項目をmutableとすることがポイントです。

idを自動生成するとき
open MongoDB.Bson.Serialization.IdGenerators
type Bookmark = {
    [<BsonId(IdGenerator = typeof<StringObjectIdGenerator>)>]
    mutable id: string
    mutable title: string
    mutable url: string
    mutable tags: string seq
}

// コレクションに追加するドキュメント
let bm = [
    {
        id = ""     // コレクション追加時に値が設定される
        title = "MongoDB"
        url = "https://www.mongodb.com"
        tags = seq {"MongoDB"; "Database"}
    }
    {
        id = ""
        title = "F# ドキュメント"
        url = "https://learn.microsoft.com/ja-jp/dotnet/fsharp"
        tags = seq {"Fsharp"; "Programming"}
    }
    {
        id = ""
        title = "pg_walker"
        url = "https://zenn.dev/pg_walker"
        tags = seq {"Fsharp"; "Zig"; "Programming"}
    }
]

データベースとコレクションの一覧

ドキュメントを追加した後に取得したデータベースやコレクションが作成されたかどうかを確認しておきます。以下の結果がtrueであれば作成済みです。

// データベース'fsmongo'が作成されたか
client.ListDatabaseNames().ToEnumerable()
|> Seq.contains "fsmongo"
|> printfn "Database 'fsmongo': %A"     // true

// コレクション'bookmark'が作成されたか
db.ListCollectionNames().ToEnumerable()
|> Seq.contains "bookmark"
|> printfn "Collection 'bookmark': %A"  // true

ドキュメントの検索

コレクションにドキュメントを追加できたら、それらを条件[1]を変えながら検索してみます。

全件検索

全件検索の場合は条件なしのEmptyで行います。このときToEnumerable()によって検索結果をシーケンス(seq)で扱えるようにしてみます。

ドキュメントの検索
// 全件検索
let filter = Builders<Bookmark>.Filter
col.Find(filter.Empty).ToEnumerable()
|> Seq.iter (fun d -> printfn "%A" d)
結果(idの値は環境により異なる)
{ id = 670b771ca26fb883f1ee1546
  title = "MongoDB"
  url = "https://www.mongodb.com"
  tags = seq ["MongoDB"; "Database"] }
{ id = 670b771ca26fb883f1ee1547
  title = "F# ドキュメント"
  url = "https://learn.microsoft.com/ja-jp/dotnet/fsharp"
  tags = seq ["Fsharp"; "Programming"] }
{ id = 670b771ca26fb883f1ee1548
  title = "pg_walker"
  url = "https://zenn.dev/pgwalker"
  tags = seq ["Fsharp"; "Zig"; "Programming"] }

設定した条件により検索されるデータの件数はCountDocumentsメソッドで確認できます。

コレクションに保存されているドキュメントの件数
col.CountDocuments(filter.Empty) |> printfn "CountDocuments: %A"

項目指定

検索する項目を指定するときはラベル(もしくはプロパティ)と検索条件をfilter.Eq("title", "MongoDB")のように設定します。項目が配列の場合も同じです。また、文字列の項目であれば正規表現による比較もできます。

ドキュメントの検索
// 項目をレコードのラベルで指定するとき(項目をラベルや文字列で指定)
col.Find(filter.Eq("title", "MongoDB")).First() |> printfn "title = MongoDB\n%A"    // 項目が文字列の配列のとき、その配列の要素で指定
col.Find(filter.Eq("tags", "Programming")).ToEnumerable() |> printfn "Pragramming tag\n%A"

// 項目が文字列など単体の場合はプロパティでの比較も可
col.Find(filter.Eq(_.title, "MongoDB")).First() |> printfn "_.title = MongoDB\n%A"

// 正規表現による検索
open System.Text.RegularExpressions
col.Find(filter.Regex("url", Regex "dotnet")).First() |> printfn "url ~= dotnet\n%A"

ドキュメントの更新

コレクションに追加されたドキュメントのうち、特定のidを持つものについて一部の項目を更新してみます。

特定のidほ持つドキュメントを更新
// 更新するドキュメントを検索する条件
// col.Find(filter.Eq("id", bm[0].id)).First()

// 更新するデータ(レコード型の一部を更新)
let bm_new = {
    bm[0] with
        title = "MongoDB Developer"
        url = "https://www.mongodb.com/developer/"
}

// ドキュメントの更新(ModifiedCountは更新した件数)
col.ReplaceOne(filter.Eq("id", bm[0].id), bm_new).ModifiedCount |> printfn "ModifiedCount: %A"

// 更新されたドキュメントの検索
col.Find(filter.Regex("title", Regex "Developer")).First() |> printfn "title ~= Developer\n%A"

ドキュメントの削除

検索条件に適合するドキュメントの削除をしてみます。この後も必要なドキュメントを削除してしまわないように検索条件の設定は慎重に確認してください。

ドキュメントの削除
// 特定のドキュメントを削除(DeletedCountは削除した件数)
col.DeleteOne(filter.Eq("title", "pg_walker")).DeletedCount |> printfn "DeletedCount: %A"

// コレクション内のすべてのドキュメントを削除
col.DeleteMany(filter.Empty).DeletedCount |> printfn "DeletedCount: %A"

// コレクション内にあるドキュメントの件数
col.EstimatedDocumentCount() |> printfn "EstimatedDocumentCount: %A"

コレクションの削除

コレクションの削除はデフォルトで実行する限り戻り値がなく(void)、存在しないコレクションの名称を指定してもエラーになりません。

db.DropCollection("bookmark")

データベースの削除

データベースの削除もコレクションと同様、デフォルトで実行する限り戻り値がなく(void)、存在しないデータベースの名称を指定してもエラーになりません。

client.DropDatabase("fsmongo")

関連Webサイト

脚注
  1. FilterDefinitionBuilder<TDocument> ↩︎

Discussion