Open4

[.NET MAUI] SQLiteデータベース利用

GomitaGomita

レンタカー予約画面の都道府県入力用リストの都道府県データを、SQLiteのデータベースから読み込んで生成するようにしたい。

SQLiteデータベースの作成

今回は都道府県のデータが入っているRegion.db3というSQLiteのデータベースをあらかじめ作成し、アプリにバンドルする形式とする。
まずは準備として、あとでテーブルにインポートするためのCSVを準備しておく。

/Resources/Raw/Region.csv
1,北海道
2,青森県
...
47,沖縄県

次に、コマンドラインでRegion.db3を作成する。macならSQLiteのコマンドラインツールが標準搭載されている。下記の通りRegionという名前のテーブルを作成し、Region.csvからインポートする。

% cd ~/Projects/MyProto/Resources/Raw/
% sqlite3 Region.db3
sqlite> create table Region ( Code integer, Name text );
sqlite> .separator ','
sqlite> .import region.csv Region
sqlite> select * from Region;
...
sqlite> .exit
GomitaGomita

SQLiteでのデータベース接続

.NET MAUIアプリケーションでSQLiteを使用するためには「ツール > NuGetパッケージの管理」から下記2つのパッケージをインストールする必要がある。

  • sqlite-net-pcl
  • SQLitePCLRaw.bundle-green

後者が入っていないとMacCatalystまたはiOSでDBアクセス時にSystem.TypeInitializationException例外スローされ、ここでだいぶハマった。公式サイトでもちゃんと書いてあるので、よく読めということ。

都道府県リストの生成処理は、最小限のコードで書くならこんな感じ。
Region.db3データベースと接続後、RegionテーブルからToListAsyncメソッドで全項目を取得し、RegionクラスのListに型変換する。

MainPage.xaml.cs
+using SQLite;
...
        // 都道府県リストの生成
        filePath = NSBundle.MainBundle.ResourcePath;
        filePath = Path.Combine(filePath, "Raw");
-        filePath = Path.Combine(filePath, "region.json");
-        json = File.ReadAllText(filePath);
-        var rlst = JsonSerializer.Deserialize<List<Region>>(json);
+        filePath = Path.Combine(filePath, "Region.db3");
+        SQLiteAsyncConnection dbConn = new SQLiteAsyncConnection(filePath);
+        var rlst = await dbConn.Table<Region>().ToListAsync();
+        dbConn.GetConnection().Close();
+        dbConn.GetConnection().Dispose();
        foreach (Region r in rlst)
        {
            RegionListFlexLayout.Children.Add(
                new Button { Text = r.Name }
            );
        }

デバッグ実行してみると、めでたくSQLiteのDBから読み込んで都道府県入力用リストを生成できた!

GomitaGomita

テーブル名

さきほどの最小限のコードでは、データベース接続後にテーブル名を指定せずとも普通にToListAsyncRegion型のListを取ってこれたあたりが不思議である。

テーブルの構造はこちらpublic class Regionの部分で定義したクラスの構造と一致しているが、テーブル名までは決めた記憶はないぞ?
どうやらテーブル名は、テーブルの構造を定義したクラス名(今回の場合はRegion)と一致している必要があるようだ。ためしにRegionクラスの先頭に[Table]属性をつけて明示的にテーブル名を変えるとSQLite.SQLiteException: no such table: RegionTable例外スローされた。

MainPage.xaml.cs
+[Table("RegionTable")]
public class Region
{
    public int Code { get; set; } = 0;
    public string Name { get; set; } = "";
}

なお、テーブルの構造を定義するクラスでは他にも下記のような属性の使い方があるもよう。これでCreateTableAsync<Region>()でテーブルを新規に作ると、列にユニーク制約や最大長指定ができるようになる。

MainPage.xaml.cs
public class Region
{
+    [Unique]
    public int Code { get; set; } = 0;
+    [MaxLength(20)]
    public string Name { get; set; } = "";
}
GomitaGomita

DBアクセス用クラス

今回は最小限のソースでSQLiteデータベース接続を実現してみたが、いくつかのサンプルコードを読むと、データベース接続用クラスを作って呼び出す形式が一般的である。

public class RegionDB
{
    SQLiteAsyncConnection _dbConn;

    public RegionDB()
    {
    }

    private async Task Init()
    {
        if (_dbConn is not null)
            return;
        // DB接続
        string filePath = NSBundle.MainBundle.ResourcePath;
        filePath = Path.Combine(filePath, "Raw");
        filePath = Path.Combine(filePath, "Region.db3");
        _dbConn = new SQLiteAsyncConnection(filePath);
        var result = await _dbConn.CreateTableAsync<Region>();
    }

    // テーブル内の全項目を返す
    public async Task<List<Region>> GetAllRegions()
    {
        await Init();
        return await _dbConn.Table<Region>().ToListAsync();
    }
}

RegionDBクラスのインスタンスを作ってアクセスする処理はこのような感じ。

MainPage.xaml.cs
                var db = new RegionDB();
                var rlst = await db.GetAllRegions();