Xamarin.Forms 复制本地SQLite数据库
2022年元旦的三天假期,闲暇无事。
想到一个多月以前浏览博客园收藏的两则新闻。一是2021年11月9日 Microsoft 发布了迄今为止最快的 .NET 版本---- .NET6 ,二是号称宇宙最强IDE的Visual Studio 2022 正式版也于同日正式发布。一时技痒,就想用Xamarin.Forms来做一个移动端应用程序。
引子:关于年过半百的老李头
何谓一时技痒呢?本人是一名年过半百(1971年出生)的编程爱好者,92年左右开始接触电脑,当时的电脑叫286或386,内存大多是32K或者64K。存储介质是一种叫软盘的东西,现在的计算机为什么从C盘开始,好像就是因为当年的A盘和B盘都是软盘驱动器,两个软盘驱动器一大一小,小的三寸盘1.44M,大的五寸盘容量512K。当年最高光的时刻就是用BASIC语言,使用一种叫DBASIC III的工具编写了一套可打印的单位工资表。
90年代中期开始接触编程,使用Pascal语言、Delphi3和SQL Server6.5数据库来开发Windows桌面应用程序,曾经获得过省部级的科技成果三等奖。到了20年代初期,ASP等脚本语言逐渐流行起来,开发方式也分为C/S方式和B/S方式。谁也想不到当年毫不起眼的JavaScript和mysql等今天能如此流行。Borland公司也在和Microsoft公司的大战中逐渐失利,最主要的是由于一些个人原因就没有再跟进学习。一晃二十年过去了。当年读过一本书叫《Borland传奇》记录了好多当年的技术大牛传奇故事,不知今天的开发者又有多少人读过。以史为鉴,谁会是今天的Borland?谁又会是明天的Microsoft?三十年后,谁在河东?谁在河西?这,也许是程序员的一种悲哀。
为什么要做移动端应用?由于从来没有做过网页开发,不会任何前端开发语言,一直听说微软有一个宏大的理想,要用.NET一统江湖,对我而言,就是能用C#写网页,也就一直比较关注。不久前,微软推出一个新的 UI 框架Blazor ,知乎上说:“blazor是基于C#的,主要的开发工具是C#(无需使用Javascript), 这对于.net 程序员来说非常友好,可以无需为了前端学习各种不同的技术,同时基于C#的代码也可以很好的和原有的Javascript代码进行沟通。”实验了一下,纷繁复杂的各种前端技术还是给我整蒙了。想到也许今天的网站开发也许就是二十年前的WinForm,于是就想直接挑战一下跨平台移动端应用开发。
从零开始,Hello World。
问题:如何复制本地数据库到目标环境
如果您向我一样,是一个零基础移动端的开发者,那么按照Xamarin.Forms 快速入门(链接地址)的示例指引,那么应该在30分钟左右,就会生成第一个Xamarin的应用,您也可以直接下载示例(链接地址)。链接数据库和创建数据表的方法如下,挺简单吧?
public NoteDatabase(string dbPath) { database = new SQLiteAsyncConnection(dbPath); database.CreateTableAsync<Note>().Wait(); }
但我希望App可以提供查询功能,查询我已经准备好的数据库中的数据。于是对于我这样零基础初学者的坑来了:
--------尝试将本地数据库直接复制到虚拟机中,把查询的功能做完。按照传统的WinForm思维,要直接复制数据库到目标环境中中,需要先知道虚拟机中创建数据库的位置。于是安装了Android的SDK,但没有在android目录中找到DDMS工具。各种ADB shell 在控制台中敲了若干命令行,还是没有在/data/user/0/com.companyname.notes/files/目录下看到App创建的Notes.db3数据库。最终明白了所谓应用沙盒,应用程序可以使用的专有区域。在默认情况下,除操作系统本身外,其他任何应用程序都无法访问此区域。于是放弃。
--------尝试将数据直接INSERT到已经创建好的数据库中。写一个数据库初始化类,直接将初始化的数据Insert进去。少量数据这也许是一个折中(偷懒)的方法,但对于近万条的初始数据,即使能Insert程序启动时也不能等这么久啊。于是放弃。
以上两个尝试消耗了三天假期中的两天。
继续查找微软的Xamarin文档,在“Xamarin.Forms 本地数据库”(链接地址)页面中写道:在多种情况下,可能需要复制 SQLite 数据库:数据库随应用程序一起提供,但必须复制或移动到移动设备上的可写存储。说的挺明白,但没找到后续操作的明确指引。
方法:通过文件流的方式来完成复制操作
在阅读了大量博客园里的文章之后(其中大部分是在第一次尝试直接复制数据库到Android虚拟机中时阅读的),看到denniswang 博主在2011年10月18日《Android中asset文件夹和raw文件夹区别》(链接地址)文章中提到的思路。
虽然感觉这篇十年前的文章应该是采用DSK的原生Android开发,但有两点明确的启示:一是Android中Resource/Raw下的文件在打包后会原封不动的保存在apk包中,不会被编译成二进制。二是读取Resource/Raw下的文件资源,通过获取输入流来的方式来进行写操作。曙光初现,假期还有一天,继续挑战,最终具体步骤如下:
一、准备本地SQLite 数据库文件。首先在Notes.Android项目的Resources目录下新建文件夹raw,然后分别将本机包含初始数据的Notes.db3在VS中复制到Notes.Android项目下的raw和Notes.iOS项目的Resources目录下。然后重新生成解决方案,让复制到项目中的资源可编译。
二、新建ISQLite 接口并分别实现。1、在Notes项目下新建类ISQLite.cs。
using SQLite; namespace Notes { public interface ISQLite { SQLiteAsyncConnection GetConnection(); } }
2、在Notes.Android项目下打开MainActivity.cs。添加属性如下:internal static MainActivity Instance { get; private set; }
3、在Notes.Android项目下新建类SQLite_Android.cs
using System; using Notes.Droid; using Xamarin.Forms; using System.IO; using SQLite; [assembly: Dependency(typeof(SQLite_Android))] namespace Notes.Droid { public class SQLite_Android : ISQLite { public SQLiteAsyncConnection GetConnection() { var sqliteFilename = "Notes.db3"; string documentsPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal); var path = Path.Combine(documentsPath, sqliteFilename); Console.WriteLine(path); if (!File.Exists(path)) { var readStream = MainActivity.Instance.Resources.OpenRawResource(Resource.Raw.Notes); FileStream writeStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write); int Length = 256; Byte[] buffer = new Byte[Length]; int bytesRead = readStream.Read(buffer, 0, Length); while (bytesRead > 0) { writeStream.Write(buffer, 0, bytesRead); bytesRead = readStream.Read(buffer, 0, Length); } readStream.Close(); writeStream.Close(); } var conn = new SQLiteAsyncConnection(path); return conn; } } }
4、在Notes.iOS项目下新建类SQLite_iOS.cs
using System; using Notes.iOS; using Xamarin.Forms; using System.IO; using SQLite; [assembly: Dependency(typeof(SQLite_iOS))] namespace Notes.iOS { public class SQLite_iOS : ISQLite { public SQLite_iOS() { } public SQLiteAsyncConnection GetConnection() { var sqliteFilename = "Notes.db3"; string documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); string libraryPath = Path.Combine(documentsPath, "..", "Library"); var path = Path.Combine(libraryPath, sqliteFilename); // This is where we copy in the prepopulated database Console.WriteLine(path); if (!File.Exists(path)) { File.Copy(sqliteFilename, path); } var conn = new SQLiteAsyncConnection(path); return conn; } } }
三、修改 Notes项目下NoteDatabase.cs类的构造函数public NoteDatabase(string dbPath)。
readonly SQLiteAsyncConnection database; //public NoteDatabase(string dbPath) //{ // database = new SQLiteAsyncConnection(dbPath); // database.CreateTableAsync<Note>().Wait(); //} public NoteDatabase() { database = Xamarin.Forms.DependencyService.Get<ISQLite>().GetConnection(); database.CreateTableAsync<Note>().Wait(); }
四、修改 Notes项目下的App.xaml.cs
public static NoteDatabase Database { get { if (database == null) { //database = new NoteDatabase(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Notes.db3")); database = new NoteDatabase(); } return database; } }
假期三天,就做了这么件事。无所谓好坏和成败。只是感悟,记录于此,希望能给有相同困扰的开发者一点参考。
-------------------图片来源于网页截图,如有侵权,告知即删除-------------------