实现SQLite跨全平台使用

初看这个标题你可能会不解,SQLite 本身就是一个跨平台的数据库,在这里再说跨平台有什么意义呢?

其实不然,目前我就遇到了一个项目需要使用 SQLite 数据库,而且我甚至完全不想花多套代码在不同的平台上,毕竟每个平台的包含的相关 SDK 并不一致。举个简单的例子,在 Android 上操作 SQLite,需要用到 SQLiteDatabase 这个类,用 Java 来操作;而在 iOS 上,除了需要引入 libsqlite3.tbd 外,还需要引入 sqlite3.h 这个头文件,使用 Objective-C 来操作,到了 PC 上,虽然都是以使用 sqlite3.h 为主,但是依然会有不一致的地方,比如说种类繁多的编程语言,大多都有不同的封装,API 不一致这足以让人头疼。

因此,在不同的平台上操作 SQLite,必定会使用不同的代码。当然了,除了 SQLite 之外,实现相同的功能,在不同平台上使用不同的代码也许已经是惯例,大家也习以为常。


Roll your eggs 的习以为常!作为一个懒人,当这样一个锅需要自己背的时候,自然是去找更简单的解决方案了。目标是一套代码走天下!


那么也不多废话了,直接上手写代码,这里有很多种技术可以选择,比如说 C++sqlite3.h 还是很好用的。不过我依然是折腾自己喜欢的 CodeTyphon,因为它有更让人觉得方便的封装。

很幸运的是,CodeTyphon 已经自带了 sqlite3conn 单元,直接引用之即可。关于如何查找可引用的库,可以看 CTCTyphon-IDE PkgsFPC Pkgs 这两页,你会找到你要的。

CTC

首先先制作一个简单的数据库吧,用于测试代码能否正常工作:

$ sqlite3 demo.db
> create table user(id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(32) NOT NULL);
> insert into user(name) value ('ABC');
> insert into user(name) value ('XYZ');

然后根据数据库结构声明一个结构体,后面会用于数据传递:

type
  TDemoRec = record
    AId: Integer;
    AName: PChar;
  end; 

与这个结构等价的 C++ 的结构体是这样的:

struct DemoRec {
    int AId;
    char* AName;
};

这一瞬间我们会发现原来操作 SQLite 是如此的简单,在此我定义了一个类,用来保存一些数据:

TSQLite = class
  private
    FDatabase: TSQLite3Connection;
    FQuery: TSQLQuery;
    FTransaction: TSQLTransaction;
  published
    property Database: TSQLite3Connection read FDatabase write FDatabase;
    property Transaction: TSQLTransaction read FTransaction write FTransaction;
    property Query: TSQLQuery read FQuery write FQuery;
  end;

有了这些东西后,就可以方便的玩起来了,比如说执行一个 SQL 语句:

function TSQLite.ExecuteSQL(ASQL: string): Boolean;
begin
  FQuery.Close;
  FQuery.SQL.Text:= ASQL;
  try
    FQuery.ExecSQL;
    Exit(True);
  except
    Exit(False);
  end;
end;

这段代码似乎太简单了,也许我们更加希望在出错时能够给出一个原因,那么可以改一下:

function TSQLite.ExecuteSQL(ASQL: string; var AError: string): Boolean;
begin
  FQuery.Close;
  FQuery.SQL.Text:= ASQL;
  try
    FQuery.ExecSQL;
    Exit(True);
  except
    on E: Exception do begin
      AError:= e.Message;
      Exit(False);
    end;
  end;
end;

好了,现在调用这个方法时,只需要额外传入一个字符串参数,就可以获取出错时的信息。

在这个体系下,要进行查询也很简单,需要额外封装两个方法:

// 根据 SQL 语句查询
function TSQLite.Select(ASQL: string; var AError: string): Boolean;
begin
  FQuery.Close;
  FQuery.SQL.Text:= ASQL;
  try
    FQuery.Open;
    Exit(True);
  Except
    on E: Exception do begin
      AError:= e.Message;
      Exit(False);
    end;
  end;
end; 

// 获取查询结果的行数
function dbGetSelectResultCount(APath: PChar): Integer;
var
  database: TSQLite;
begin
  Result := -1;
  if (DatabaseExists(string(APath))) then begin
    database := GetDatabase(string(APath));
    Result := database.Query.RecordCount;
  end;
end;

// 获取指定行号的一条记录
function dbGetSelectResult(APath: PChar; AIndex: Integer): TDemoRec;
var
  database: TSQLite;
  tmp: string;
begin
  Inc(AIndex);
  if (DatabaseExists(string(APath))) then begin
    database := GetDatabase(string(APath));
    if (database.Query.RecordCount >= AIndex) then begin
      database.Query.RecNo:= AIndex;
      Result.AId:= database.Query.FieldByName('id').AsInteger;
      tmp := database.Query.FieldByName('name').AsString;
      Result.AName:= StrAlloc(tmp.Length);
      strcopy(Result.AName, PChar(tmp));
    end;
  end;
end;

接下来就是导出函数了,作为一个跨平台的库,它需要被其他程序调用,那么必定有导出函数,而不同的平台下,所需要的函数形态是不一样的,特别是由于 Android 使用 JNI 来调用动态库,导出函数必须符合 JNI 的规范。

下面的例子很好的说明了导出函数的方法:

// iOS, PC
function dbGetSelectResultCount(APath: PChar): Integer; cdecl;
function dbGetSelectResult(APath: PChar; AIndex: Integer): TDemoRec; cdecl;

// Android
function Java_com_sqlite_sample_NativeAPI_dbGetSelectResultCount(env: PJNIEnv; obj: jobject; APath: jstring): jint; stdcall;
function Java_com_sqlite_sample_NativeAPI_dbGetSelectResult(env: PJNIEnv; obj: jobject; APath: jstring; AIndex: jint): jobject; stdcall;

唯一需要注意的是调用协定,用于 JNI 的必须设为 stdcall,而其他的均设为 cdecl

那么再下一步就是编译,直接使用 FPC 跨平台编译器即可,编译方法很简单:

$ fpc64 -Fisqlite -Fusqlite sample.lpr 

此时即可以在 Mac 端生成 libsample.dylib 以及在 Linux 端生成 libsample.so

要跨平台编译的话,稍微麻烦一点,但是也比想象中简单很多:

$ export ANDROID_LIB=/usr/local/codetyphon/binLibraries/android-5.0-api21-arm/
$ export FPC=/usr/local/codetyphon/fpc/fpc64/bin/x86_64-linux/fpc
$ ${FPC} -Tandroid -Parm -Fl${ANDROID_LIB} -Fiqslite -Fusqlite sample.lpr

此时即可生成一个供 Android 系统使用的,arm 架构的 libsample.so,通过更换 -P 后面的参数,也可以编译 x86,mips 等架构的 so。

完成后再看一下 iOS 的库要怎么编译。由于 iOS 已不再允许动态加载 dylib,我们必须把代码编译为静态库,也就是 .a 文件,并且静态链接到 iOS 项目内。

$ export FPC_ROOT=/usr/local/lib/fpc/3.1.1
$ export FPC=${FPC_ROOT}/ppcrossa64
$ ${FPC} -Tdarwin -dIPHONEALL -Cn -Fisqlite -Fusqlite sample.lpr
$ ar -q libsample.a `grep "\.o$" link.res`
$ ranlib libsample.a

此时可以得到一个用于 64 位真机的 libsample.a 文件,若是要在 32 位的 iOS 和模拟器上完成兼容,还必须再另外编译两个 .a

32 位真机:替换编译器为 ppcrossarm
模拟器:替换编译器为 ppcx64,并替换 -T 参数为 iphonesim

当我们得到了 3 个不同架构的 .a 后,有些时候需要将它们合并,使用如下命令来合并之:

lipo -create libsample_A64.a libsample_ARM.a libsample_EMU.a -output libsample.a

这样就得到了一个融合了的 .a,它可以用于各种场合。


现在一切都准备好了,看看如何使用我们做好的库吧,以上述的 dbGetSelectResultCountdbGetSelectResult 为例,分别讲述在各平台的使用方法。

Android:

package com.sqlite.sample;

public class NativeAPI {
    static {  System.loadLibrary("sample"); }
    public static native int dbGetSelectResultCount(String APath);
    public static native DemoRec dbGetSelectResult(String APath, int AIndex);
}

iOS:

extern int dbGetSelectResultCount(const char* APath);
extern struct DemoRec dbGetSelectResult(const char* APath, int AIndex);

PC(以 C++ 为例):

typedef int (*dbSelectResultCount)(const char* APath);
typedef struct DemoRec (*dbSelectResult)(const char* APath, int AIndex);

void* handle = dlopen("./libsample.so", RTLD_LAZY);
dbSelectResultCount mSelectResultCount = (dbSelectResultCount) dlsym(handle, "dbGetSelectResultCount");
dbSelectResult mSelectResult = (dbSelectResult) dlsym(handle, "dbGetSelectResult");

可以看到,不论在哪个平台上,最终得到的 API 都是一致的,这样就统一了调用方式。在此基础上,要做二次封装也是非常方便。另外,由于代码耦合几乎没有,也能够很方便的对 SQLite 的底层库的逻辑进行修改,只要 API 不变,就不会影响上层的调用。


以下是一个完整的调用代码,以 iOS 端为例,其他各端均一致:

// 复制数据库文件
NSString * originPath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"db"];
NSString * destPath = [ViewController getDocumentPath];
NSString * dbFile = [destPath stringByAppendingPathComponent:@"demo.db"];
[ViewController copyFile:originPath destFile:dbFile];

// 打开数据库    
int b = dbOpen([dbFile UTF8String]);
printf("Open Database => %d\n", b);
// 执行查询
b = dbSelect([dbFile UTF8String], "select * from user");
printf("Select => %d\n", b);
// 获取查询结果的行数
int count = dbGetSelectResultCount([dbFile UTF8String]);
printf("Select Rows => %d\n", count);
// 取出查到的每一条数据
for (int i = 0; i < count; i++) {
    struct DemoRec r = dbGetSelectResult([dbFile UTF8String], i);
    printf("Data %d => {id => %d, name => %s}\n", i, r.AId, r.AName);
}
// 关闭数据库
b = dbClose([dbFile UTF8String]);
printf("Close Database => %d\n", b);

这段代码的输出为:

可以看到,调用成功,并且正确的传递了数据。在其他平台上的效果也是完全一样的。


这个用于演示的项目已经开源,请访问我的 github 获取,地址:

章鱼猫/rarnu/cross_sqlite
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 使用的过程根据使用的函数大致分为如下几个过程: sqlite3_open() sqlite3_prepare() ...
    随风飘荡的小逗逼阅读 5,997评论 0 3
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,568评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,613评论 4 59
  • 最傻的事情就是相信他说的话……我做出退让,全局就输了.....哪怕玩笑也会成真……不会对你好的,除非他撞南墙......
    诺米洋阅读 149评论 0 0
  • 想要 Activity 在启动时就不显示(不会出现闪一下或黑屏的情形),只要在 AndroidManifest.x...
    飛飛萨阅读 6,441评论 1 4