Xamarin.Froms+.NET Standardでローカルな SQLite データベースを使う

Xamarin.FormsでSQLiteを扱う記事はいくつかあるのだけど、.NET Standard 2.0を扱ったものは、以下しかなかったのでメモ代わりに記録しておく。

Xamarin.Forms, NET Standard 2.0 et Entity Framework Core avec SQLite ? Christophe Gigax
http://www.christophe.gigax.fr/2017/11/23/xamarin-forms-net-standard-2-0-et-entity-framework-core-avec-sqlite/

しかも記事がフランス語だという…コードを読めれば十分なんだけど。

ローカルなSQLiteファイルを用意する

大抵の記事がアプリの実行時に新規にSQLiteファイルを作っていて、じゃあ既存のSQLiteファイルを使いたい場合はどうするのか?というのが無かった。Androidの記事で似たものがあったので、これを活用していく。

Android SDK assetsの内容を全てローカルにコピーする – 自堕落なぺぇじ
http://d.hatena.ne.jp/corrupt/20110203/1296695472

Entitiy Model クラスは MySQL の redmine から借りて来る。ひとまず、projects と issues の構造を用意しておく。

public class projects
{
    public int id { get; set; }
    public string name { get; set; }
    public string description { get; set; }
    public string homepage { get; set; }
    public int is_public { get; set; }
    public int? parent_id { get; set; }
    public DateTime? created_on { get; set; }
    public DateTime? updated_on { get; set; }
    public string identifier { get; set; }
    public int status { get; set; }
    public int? lft { get; set; }
    public int? rgt { get; set; }
    public int inherit_members { get; set; }
    public int? default_version_id { get; set; }
}

public class issues
{
    public int id { get; set; }
    public int tracker_id { get; set; }
    public int project_id { get; set; }
    public string subject { get; set; }
    public string description { get; set; }
    public DateTime? due_date { get; set; }
    public int? category_id { get; set; }
    public int status_id { get; set; }
    public int? assigned_to_id { get; set; }
    public int priority_id { get; set; }
    public int? fixed_version_id { get; set; }
    public int author_id { get; set; }
    public int lock_version { get; set; }
    public DateTime? created_on { get; set; }
    public DateTime? updated_on { get; set; }
    public DateTime? start_date { get; set; }
    public int done_ratio { get; set; }
    public double? estimated_hours { get; set; }
    public int? parent_id { get; set; }
    public int? root_id { get; set; }
    public int? lft { get; set; }
    public int? rgt { get; set; }
    public int is_private { get; set; }
    public DateTime? closed_on { get; set; }
}

SQLite であらかじめテーブルを作成しておく。

create table issues (
	id int PRIMARY KEY AUTOINCREMENT,
	tracker_id int,
	project_id int,
	subject text,
	description	text,
	due_date text,
	category_id int,
	status_id int,
	assigned_to_id int,
	priority_id int,
	fixed_version_id int,
	author_id int,
	lock_version int,
	created_on text,
	updated_on text,
	start_date text,
	done_ratio int,
	estimated_hours real,
	parent_id int,
	root_id int,
	lft int,
	rgt int,
	is_private int,
	closed_on text
);
create table projects (
	id integer PRIMARY KEY AUTOINCREMENT,
	name text,
	description text,
	homepage text,
	is_public int,
	parent_id int,
	created_on text,
	updated_on text,
	identifier text,
	status int,
	lft int,
	rgt int,
	inherit_members int,
	default_version_id int,
	default_assigned_to_id int
);

型の変換ルールは、

  • MySQLのvarcharをSQLiteでtextへ
  • MySQLのdatetimeをSQLiteでtextへ

ことになる。SQLiteには日付型がないので文字列(text型)として扱うのだが、Microsoft.EntityFrameworkCore.Sqlite がうまいこと日付と文字列を相互変換してくれるので、Entitiy のほうは DateTime のままで良い。

MySQLからSQLiteへコピーする

ちょっと乱暴だが、.NET Core で Console アプリを使って、NuGet で MySql.Data.EntityFrameworkCore と Microsoft.EntityFrameworkCore.Sqlite を追加することで EF を使って MySQL から SQLite へコピーするツールが作れる。

MySQLのEF

public partial class RedmineEntities : DbContext
{
    public RedmineEntities()
    {

    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);

        var fname = "redmine.sqlite3";
        var docs = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
        var path = System.IO.Path.Combine(docs, fname);

        optionsBuilder.UseSqlite($"Data Source={path}");
    }

    public DbSet<projects> projects { get; set; }
    public DbSet<issues> issues { get; set; }
}

SQLiteのEF

public partial class RedmineEntitiesMySql : DbContext
{
    public RedmineEntitiesMySql()
    {

    }
        
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
        // SSH フォワーディングしておく
        // ssh -L 19000:localhost:3306 pi@raspi3.local
        optionsBuilder.UseMySQL(@"server=localhost;user id=redmine;password=redmine;database=redmine;port=19000;sslmode=None");
    }

    public DbSet<projects> projects { get; set; }
    public DbSet<issues> issues { get; set; }
}

main 関数

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("copy MySQL to SQLite");

        var mysql = new RedmineEntitiesMySql();
        var sqlite = new RedmineEntities();

        // projectsとisseusの中身を消す
        sqlite.Database.ExecuteSqlCommand("delete from projects");
        sqlite.Database.ExecuteSqlCommand("delete from issues");
        // データをコピーする
        sqlite.projects.AddRange(mysql.projects.ToListAsync().Result);
        sqlite.issues.AddRange(mysql.issues.ToListAsync().Result);
        sqlite.SaveChanges();
        Console.WriteLine("コピーしました");
    }
}

DB Browser for SQLite で中身を確認しておく。ファイル名は「redmine.sqlite3」としている。

Xamarin.Fromsにredmine.sqlite3を追加する

さて、Android限定になるのだが(iOSはまだ調べていない)、SQLiteファイルを指定するためには、どこか読み込めるところにファイルをコピーしておかないといけない。これには2つの方法があって、

  • 起動時に1回だけネットワークを通じて取ってくる。
  • 起動時に1回だけリソースからコピーする。

ことになる。ゲームなどの画像ファイルや更新データなどは、ネットワークから取ってきて Android 内に置くパターンが多いのだが、今回はリソースからコピーしてみる。

Android SDK assetsの内容を全てローカルにコピーする – 自堕落なぺぇじ
http://d.hatena.ne.jp/corrupt/20110203/1296695472

ここの記事を参考にして、Assets/redmine.sqlite3 ファイルを、/data/user/0/…/files/redmine.sqlite3 へコピーする。これは、Xamarin.FormsのAndroid プロジェクトのほうに作っておく。

private string copyLocal( string fname )
{
    var st = new System.IO.BinaryReader( this.Resources.Assets.Open(fname));
    var docs = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
    var ofname = System.IO.Path.Combine(docs, fname);
    var ofs = System.IO.File.OpenWrite(ofname);
    int DEFAULT_BUFFER_SIZE = 1024 * 4;
    byte[] buf  = new byte[DEFAULT_BUFFER_SIZE];
    int n = 0;
    int nn = 0;
    while ((n = st.Read(buf, 0, buf.Length)) > 0)
    {
        ofs.Write(buf, 0, n);
        System.Diagnostics.Debug.WriteLine(string.Format("cnt: {0}", nn));
        nn++;
    }
    ofs.Close();
    st.Close();
    return ofname;
}

Xamarin.Formsに「Microsoft.EntityFrameworkCore.Sqlite」を追加する

Xamarin.Formsの.NET Standardプロジェクトのほうに、Nuget で Microsoft.EntityFrameworkCore.Sqlite を追加する。
EF の接続文字列は、SQLiteファイルへのパスになるので、コピー先のディレクトリを指定させる。

ちなみに「Microsoft.EntityFrameworkCore.Sqlite.Core」というのもあるが、この .Core 付きとないのとどう違うのかわからない。Xamarin.Forms の場合は、.Core 付きでは動かなかったので、.NET Core 専用なのか?依存関係が

  • Microsoft.EntityFrameworkCore.Sqlite は、SQLitePCLRaw.bundle_green
  • Microsoft.EntityFrameworkCore.Sqlite.Coreは、Microsoft.Data.Sqlite.Core

となっている。

public partial class RedmineEntities : DbContext
{
    public RedmineEntities()
    {

    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);

        var fname = "redmine.sqlite3";
        var docs = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
        var path = System.IO.Path.Combine(docs, fname);

        optionsBuilder.UseSqlite($"Data Source={path}");
    }

    public DbSet<projects> projects { get; set; }
    public DbSet<issues> issues { get; set; }
}

試しに、MainPageクラスのコンストラクタで、SQLiteに接続し、ListView に設定する。

public partial class MainPage : ContentPage
{
	public MainPage()
	{
		InitializeComponent();
        // redmine.sqlite3 をロードする
        _ent = new RedmineModel.RedmineEntities();
        this.lv.ItemsSource = _ent.issues.ToList();
	}

    RedmineModel.RedmineEntities _ent;
	...
}

ちなみに、XAML はこんな感じ。

<ContentPage.Content>
    <StackLayout>
        <ListView x:Name="lv" VerticalOptions="FillAndExpand" HasUnevenRows="true" ItemSelected="OnItemSelected">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <StackLayout Padding="10">
                            <Label Text="{Binding id}" LineBreakMode="NoWrap" Style="{DynamicResource ListItemTextStyle}" FontSize="16"/>
                            <Label Text="{Binding subject}" LineBreakMode="NoWrap" Style="{DynamicResource ListItemDetailTextStyle}" FontSize="13"/>
                        </StackLayout>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ContentPage.Content>

実行すると、こんな風になる。

ローカルなSQLiteファイルを何に使うか?

スマホでデータベースに繋げるのだから、AzureかWeb APIでいいような気もするのだが、いくつか内部でローカルなデータベースとして持っておいたほうが良いパターンが考えられる。

  • スマホで外部ネットワークに繋がらない、あるいは極端に遅い場合
  • スマホを社内WiFiのみに限る場合
  • タブレットをマニュアルっぽく使う

今回 SQLite に持ってきたのは redmine なので、あまり意味はないのだが、wordpress のデータをスマホの SQLite に持ってきて、単体でブラウジングできるようにすると電子書籍っぽく使えるのではないか?と思ったり。
まあ、ひとまず、非クラウドなスタンドアローン環境でスマホを使う技になる。

サンプルコード

github sample-sqlite-ef

カテゴリー: 開発, SQLite, Xamarin パーマリンク