F#でMongoDBクライアント操作実験 - CSVファイルを読み込んでコレクションに保存
ここではF#でMongoDBのクライアントを起動し、2種類のCSVファイルを読み込んで、それぞれの内容に合わせた型でドキュメントを生成しMongoDBのコレクションに保存する処理を実験的にやってみます。MongoDBの公式ドライバはC#向けとなっていますので、F#はサポート外でしょうが、同じ.NET環境でのプログラミングが可能なF#であえて実験してみようというわけです。
F#について
MongoDBについて
MongoDBはリレーショナルデータベースとは異なる形式のデータを扱うNoSQLデータベースのひとつです。データベースにはコレクションと呼ばれるデータの保存場所が作成され、そこにJSONのような階層化された構造を持つデータであるドキュメントが保存されます。そして保存されたデータに対しては検索などの操作ができます。
ここではPCにインストール可能なMongoDB Community Editionを利用しますが、クラウドサービスのMongoDB Atlasも提供されています。
実行環境について
F#とMongoDBの実行環境についてはF#でMongoDBクライアント操作実験 - クライアントの起動からドキュメント操作までをご覧ください。クライアントの起動やドキュメントの保存についてもここで紹介しています。ただしこちらではドライバAPIの非同期メソッドを使用することにします。
使用するCSVファイルは以下の2つのWebサイトからダウンロードしたZipファイルを展開したものです。
-
日本 都道府県マスター データセット(
mt_pref_all.csv.zip
を展開したmt_pref_all.csv
) -
日本 都道府県マスター位置参照拡張 データセット(
mt_pref_pos_all.csv.zip
を展開したmt_pref_pos_all.csv
)
これらのフォーマット仕様や利用規約はアドレス・ベース・レジストリから閲覧できます。
CSVデータと生成するドキュメントの確認
まず、これから読み込むCSVデータと生成するドキュメントを確認しておきます。
前項1.の都道府県マスターの先頭5行は以下の通りです。
$ head -n5 mt_pref_all.csv
lg_code,pref,pref_kana,pref_roma,efct_date,ablt_date,remarks
010006,北海道,ホッカイドウ,Hokkaido,1947-04-17,,
020001,青森県,アオモリケン,Aomori,1947-04-17,,
030007,岩手県,イワテケン,Iwate,1947-04-17,,
040002,宮城県,ミヤギケン,Miyagi,1947-04-17,,
最初にヘッダ行がありますので、これをドキュメントの項目名とします。ただしドキュメントとして保存するのは左から5項目までとします。そのため、ドキュメントの型は以下のようにします。
type MtPref = {
[<BsonId>]
id: ObjectId
lg_code: string
pref: string
pref_kana: string
pref_roma: string
efct_date: DateTime // MongoDB内ではUTC (ToLocalTime可)
}
なおefct_date
はCSVファイルでは文字列ですが、ドキュメントを生成する際にSystem.DateTime
に変換することにします。ただし、これをMongoDBに保存するときはUTCとなるため、注意してください。
そして前項2.の都道府県マスター位置参照拡張の先頭5行は以下の通りです。
$ head -n5 mt_pref_pos_all.csv
lg_code,rep_lon,rep_lat,rep_srid,rep_scale,plygn_fname,plygn_kcode,plygn_fmt,plygn_srid,plygn_scale
010006,141.347906782,43.0639406375,EPSG:6668,,,,,,
020001,140.740087,40.824338,EPSG:6668,,,,,,
030007,141.152592,39.703647,EPSG:6668,,,,,,
040002,140.871846,38.268803,EPSG:6668,,,,,,
こちらも最初のヘッダ行をドキュメントの項目名とし、ドキュメントとして保存するのは左からの3項目とします。そのためドキュメントの型を以下のように定義します。
type MtPrefPos = {
[<BsonId>]
id: ObjectId
lg_code: string
rep_lon: decimal
rep_lat: decimal
}
なお、rep_lon
とrep_lat
は小数点以下の値を正確に数値に反映させるためdecimal
型にしました。
CSVファイルを読み込んでリストを生成
CSVファイルの内容はシンプルなカンマ区切りなので、ファイルから1行ずつ読み込んて、それをカンマで区切った文字列のリストを生成します。生成後のデータは以下のようなイメージです。
[
["010006"; "北海道"; "ホッカイドウ"; "Hokkaido"; "1947-04-17"; ""; ""];
["020001"; "青森県"; "アオモリケン"; "Aomori"; "1947-04-17"; ""; ""];
["030007"; "岩手県"; "イワテケン"; "Iwate"; "1947-04-17"; ""; ""];
["040002"; "宮城県"; "ミヤギケン"; "Miyagi"; "1947-04-17"; ""; ""];
["050008"; "秋田県"; "アキタケン"; "Akita"; "1947-04-17"; ""; ""];
..........
]
この処理は以下の関数で行うことにします。ポイントは関数内部で定義しているparseLineItems
でStreamReader.ReadLine
メソッドの実行結果に基づいて処理を振り分けているところです。これはrec
により再帰関数として定義されていてファイルを読み込み終えるまで繰り返し実行されます。
// getLineItems: file_name: 'a -> string list list
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 line_items = getLineItems "mt_pref_all"
ドキュメントの生成
文字列のリストを用意したら、それをもとにドキュメントを生成します。それぞれのCSVファイルごとにドキュメントの型が違いますので、それぞれ型に合わせて生成処理を行います。
// 都道府県マスターのドキュメント生成
"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_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]
})
// 生成したドキュメントをコレクションに保存
ドキュメントをコレクションに保存
ドキュメントを生成したらそれをMongoDBのコレクションに保存します。コレクションはCSVファイルごとに用意することにします。ドキュメントを保存するときは非同期処理を行うためInsertManyAsync
メソッドを実行します。これによりTask
オブジェクト[1]を得られるため、Wait
メソッドで終了を待ちます。
#if INTERACTIVE
#r "nuget: MongoDB.Driver" // 対話型実行環境(dotnet fsi)のとき実行
#endif
open System
open System.IO
open System.Text.Json
open MongoDB.Driver
open MongoDB.Bson
open MongoDB.Bson.Serialization.Attributes
// ドキュメントの型 - 都道府県マスター
type MtPref = {
// ..........
}
// ドキュメントの型 - 都道府県マスター位置情報拡張
type MtPrefPos = {
// ..........
}
// CSVファイルから文字列のリストを生成
let getLineItems file_name =
// ..........
}
// MongoDBクライアントを起動
let client = new MongoClient()
// データベースを取得
let db_name = "fsmongo"
let db = client.GetDatabase(db_name)
// CSVファイル(ドキュメント)ごとにコレクションを取得
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
// ..........
|> mt_pref_all.InsertManyAsync
task1.Wait()
// 都道府県マスター位置情報拡張を読み込んでコレクションに保存
let task2 = "mt_pref_pos_all"
|> getLineItems
// ..........
|> 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件のドキュメントが保存されました
ドキュメントの検索とJSON文字列化
ドキュメントが保存できたかを確認するため、まず都道府県マスターのコレクションから1件ドキュメントを検索します。
let tokyo = mt_pref_all.FindAsync<MtPref>(
Builders<MtPref>.Filter.Eq("pref", "東京都")
).Result.FirstAsync<MtPref>().Result
そして検索されたドキュメントをJSON文字列化してみます。その方法は.NET APIのJsonSerializer
[2]とMongoDBドライバのToJson
メソッド[3]がありますが、結果が異なりますので注意が必要です。
tokyo |> JsonSerializer.Serialize |> printfn "%s"
{
"id":{
"Timestamp":1729764391,
"CreationTime":"2024-10-24T10:06:31Z"
},
"lg_code":"130001",
"pref":"\u6771\u4EAC\u90FD",
"pref_kana":"\u30C8\u30A6\u30AD\u30E7\u30A6\u30C8",
"pref_roma":"Tokyo",
"efct_date":"1947-04-16T15:00:00Z"
}
tokyo.ToJson() |> printfn "%s"
{
"_id" : { "$oid" : "671a00075738daa541aff736" },
"lg_code" : "130001",
"pref" : "東京都",
"pref_kana" : "トウキョウト",
"pref_roma" : "Tokyo",
"efct_date" : { "$date" : { "$numberLong" : "-716720400000" } }
}
都道府県マスター位置情報拡張のほうも同様に検索とJSON文字列化を実行すると以下のようになります。
// ドキュメントの検索(都道府県マスター位置情報拡張)
let tokyo_pos = mt_pref_pos_all.FindAsync<MtPrefPos>(
Builders<MtPrefPos>.Filter.Eq("lg_code", "130001")
).Result.FirstAsync<MtPrefPos>().Result
// JSON文字列化(.NET API)
tokyo_pos |> JsonSerializer.Serialize |> printfn "%s"
// JSON文字列化(MongooDBドライバ)
tokyo_pos.ToJson() |> printfn "%s"
// JSON文字列化(.NET API)
{
"id":{
"Timestamp":1729765156,
"CreationTime":"2024-10-24T10:19:16Z"
},
"lg_code":"130001",
"rep_lon":139.691717,
"rep_lat":35.689568
}
// JSON文字列化(MongooDBドライバ)
{
"_id" : { "$oid" : "671a03049d71be6811f403d5" },
"lg_code" : "130001",
"rep_lon" : { "$numberDecimal" : "139.691717" },
"rep_lat" : { "$numberDecimal" : "35.689568" }
}
まとめ
- CSVファイルの読み込みは再帰関数(
rec
)を使用した - 文字列のリストをもとに
List.map
でドキュメントを生成した - ドキュメントの保存は非同期処理(
InsertManyAsync
)で行った - ドキュメントの検索も非同期処理(
FindAsync
,FirstAsync
)で行った - ドキュメントのJSON文字列化には.NET API(
JsonSerializer
)とMongoDBドライバ(ToJson
メソッド)が実行できるが、両者は結果が異なるため注意が必要
Discussion