# ffsky-app-database **Repository Path**: hljdrl/ffsky-app-database ## Basic Information - **Project Name**: ffsky-app-database - **Description**: android sqlite table数据库,sqlite加密数据库,kv数据存储 - **Primary Language**: Android - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-01-07 - **Last Updated**: 2026-02-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ffsky-app-database ## 项目简介 ffsky-app-database 是一个轻量级 Android 本地数据库组件,基于 **SQLCipher** 加密引擎,提供开箱即用的键值存储和多用户设置管理能力。 ### 核心功能 - **数据库加密** — 基于 SQLCipher,数据库文件全量加密存储,防止数据被反编译或 root 后直接读取 - **KV 键值存储** — 内置 `KVTable`,一行代码完成 key-value 的存取删,支持 String / int / long / float / double / boolean 类型读取 - **多用户设置隔离** — 内置 `SettingTable`,按账户自动隔离用户配置数据,切换账户即切换数据 - **自动初始化** — 通过 `ContentProvider` 实现 App 启动时自动完成数据库初始化,零代码接入 - **自定义表扩展** — 继承 `AbstractTable` 或 `AbstractAccountTable` 即可快速定义业务数据表 - **字段增量升级** — 提供 `checkTableColumn` + `addTableColumn` 机制,无需手写版本迁移即可新增字段 - **WAL 并发优化** — 默认启用 Write-Ahead Logging 模式,读写操作互不阻塞 - **线程安全** — 数据库读写操作 `synchronized` 保护,单例采用静态内部类 / volatile + synchronized 模式 ### 架构总览 ``` ┌─────────────────────────────────────────────────┐ │ 调用层(App) │ │ KVTable.getInstance().saveItem("k","v") │ │ SettingTable.getInstance().readItem("k","") │ └──────────────────┬──────────────────────────────┘ │ ┌──────────────────▼──────────────────────────────┐ │ DataBaseManager │ │ install() → onLoad() → checkTables() │ │ getWritableDatabase() / getReadableDatabase() │ └──────────────────┬──────────────────────────────┘ │ ┌──────────────────▼──────────────────────────────┐ │ SQLCipher (加密层) │ │ SQLiteOpenHelper → 加密读写 → WAL 模式 │ └──────────────────┬──────────────────────────────┘ │ ┌──────────────────▼──────────────────────────────┐ │ SQLite 数据库文件 (.db) │ └─────────────────────────────────────────────────┘ ``` --- ## 设计理念 ### 极简 API 调用层只需面对 `getInstance()` + `saveItem()` / `readItem()` 等少量方法,不需要关心数据库连接、事务、游标关闭等底层细节。一行代码完成数据存取,降低业务开发者的心智负担。 ### 开箱即用 内置 `KVTable`(全局键值)和 `SettingTable`(多用户配置)两张常用表,覆盖 90% 以上的本地轻量存储场景。通过 `ContentProvider` 自动初始化,接入即可用,无需在 Application 中手写初始化代码。 ### 安全优先 采用 SQLCipher 全量加密数据库文件,即使设备被 root 或数据库文件被提取,也无法直接读取明文数据。加密密码支持外部注入,推荐结合 Android Keystore 动态获取。 ### 多用户天然支持 `AbstractAccountTable` 基类内置 `account` 字段,配合 `AccountCache` 全局缓存当前用户标识,所有读写操作自动按用户隔离——切换账户只需一行 `setCacheAccount()`,无需修改任何业务查询逻辑。 ### 渐进式扩展 - **不够用?** 继承 `AbstractTable` 定义自己的业务表,`addTable()` + `checkTable()` 两步注册 - **要加字段?** 覆写 `checkTableColumn()`,配合 `addTableColumn()` 实现无损增量升级 - **要升版本?** 覆写 `migrate()` 自定义迁移策略,默认 DROP + 重建保持简单 ### 轻量无侵入 整个库仅 8 个 Java 类,无第三方依赖(除 SQLCipher),不引入注解处理器、代码生成或反射,编译零开销,包体积增量极小。作为公共组件可被任意 Android 项目直接引用。 --- ## API 类与方法索引 ### DatabaseTable(接口) 数据库表接口,定义表的生命周期方法。 | 方法 | 说明 | |------|------| | `create(SQLiteDatabase db)` | 创建数据库表 | | `migrate(SQLiteDatabase db, int toVersion)` | 数据库版本升级迁移 | | `clear()` | 清空表中所有数据 | | `checkTableColumn(SQLiteDatabase db, String tableName)` | 检查并补全表字段 | ### AbstractTable(抽象类,implements DatabaseTable) 数据表抽象基类,提供通用 CRUD 辅助方法。 | 方法 | 说明 | |------|------| | `getTableName()` | 获取表名(子类实现) | | `getProjection()` | 获取列名数组(子类实现) | | `getListOrder()` | 获取默认排序方式 | | `migrate(SQLiteDatabase db, int toVersion)` | 默认迁移(DROP TABLE) | | `getStringValue(Cursor cursor, String columnName)` | 从 Cursor 获取字符串值 | | `getIntValue(Cursor cursor, String columnName)` | 从 Cursor 获取整数值 | | `list()` | 查询所有数据 | | `list(String select, String[] selectionArgs)` | 按条件查询 | | `list(String select, String[] selectionArgs, String groupBy)` | 按条件查询(带分组) | | `list(String select, String[] selectionArgs, String groupBy, String order)` | 按条件查询(带分组和排序) | | `getCount()` | 获取记录总数 | | `clear()` | 清空表数据 | | `addTableColumn(SQLiteDatabase db, String tableName, String columnName, String sql)` | 添加新列(字段升级) | | `checkColumnExist(SQLiteDatabase db, String tableName, String columnName)` | 检查列是否存在 | | `checkColumnExistsAsSqliteMaster(SQLiteDatabase db, String tableName, String columnName)` | 检查列是否存在(通过 sqlite_master) | | `checkTableColumn(SQLiteDatabase db, String tableName)` | 检查表字段(子类覆写) | | `readItem(String name)` | 读取原始值(子类覆写) | | `readItemAsString(String name, String defaultValue)` | 读取字符串值 | | `readItemAsInt(String name, int defaultValue)` | 读取 int 值 | | `readItemAsLong(String name, long defaultValue)` | 读取 long 值 | | `readItemAsFloat(String name, float defaultValue)` | 读取 float 值 | | `readItemAsDouble(String name, double defaultValue)` | 读取 double 值 | | `readItemAsBoolean(String name, boolean defaultValue)` | 读取 boolean 值 | | `hasData(Cursor c)` | 判断 Cursor 是否有数据 | | `closeCursor(Cursor c)` | 安全关闭 Cursor | | `string(String... str)` | 拼接字符串 | | `string(Object... str)` | 拼接对象字符串 | ### AbstractAccountTable(抽象类,extends AbstractTable) 多用户数据表抽象基类,增加 account 字段进行数据隔离。 | 方法 | 说明 | |------|------| | `removeAccount(String account)` | 删除指定账户的所有数据 | | `getAccount(Cursor cursor)` | 从 Cursor 读取账户标识 | ### DataBaseManager(extends SQLiteOpenHelper) 数据库管理器,负责创建、升级、表注册与生命周期管理。 | 方法 | 说明 | |------|------| | `install(Context ctx, String dbName, int dbVersion, String dbPassword)` | 初始化数据库(静态,线程安全) | | `getInstance()` | 获取单例实例 | | `unInstance()` | 销毁单例 | | `reInit(Context ctx, String name)` | 重新初始化 | | `setPassword(String password)` | 设置加密密码 | | `getWritableDatabase()` | 获取可写数据库(线程安全) | | `getReadableDatabase()` | 获取只读数据库(线程安全) | | `addTable(DatabaseTable table)` | 注册自定义表 | | `onLoad()` | 加载数据库(启用 WAL、建表、检查字段) | | `onClear()` | 清空所有表数据 | | `checkTables()` | 检查并补全所有已注册表 | | `checkTable(AbstractTable table)` | 检查并补全单个表 | | `tableIsExist(SQLiteDatabase db, String tableName)` | 判断表是否存在 | | `tablesInDB(SQLiteDatabase db)` | 获取数据库中所有表名 | | `removeAccount(String account)` | 删除指定账户在所有多用户表中的数据 | | `execSQL(SQLiteDatabase db, String sql)` | 执行原始 SQL | | `dropTable(SQLiteDatabase db, String table)` | 删除表 | | `renameTable(SQLiteDatabase db, String table, String newTable)` | 重命名表 | ### KVTable(extends AbstractTable) 键值(KV)数据表,用于存储简单的 key-value 数据。 | 方法 | 说明 | |------|------| | `getInstance()` | 获取单例实例 | | `create(SQLiteDatabase db)` | 创建 KV 表 | | `saveItem(String key, String body)` | 保存数据(upsert) | | `deleteItem(String key)` | 删除指定 key | | `deleteLikeItem(String key)` | 模糊删除 | | `hasItem(String key)` | 查找 key 的记录 ID | | `hasKeyValue(String key)` | 判断 key 是否存在 | | `readMap()` | 读取所有数据为 Map | | `readItem(String key, String defaultValue)` | 读取指定 key 的值 | | `readItemInt(String key, int defaultValue)` | 读取 int 值(已废弃) | | `readItemAsString(String name, String defaultValue)` | 读取字符串值(继承) | | `readItemAsInt(String name, int defaultValue)` | 读取 int 值(继承) | | `readItemAsLong(String name, long defaultValue)` | 读取 long 值(继承) | | `readItemAsFloat(String name, float defaultValue)` | 读取 float 值(继承) | | `readItemAsDouble(String name, double defaultValue)` | 读取 double 值(继承) | | `readItemAsBoolean(String name, boolean defaultValue)` | 读取 boolean 值(继承) | ### SettingTable(extends AbstractAccountTable) 用户设置数据表,支持多用户数据隔离。 | 方法 | 说明 | |------|------| | `getInstance()` | 获取单例实例 | | `create(SQLiteDatabase db)` | 创建设置表 | | `saveItem(String key, String body)` | 保存设置(upsert) | | `deleteItem(String key)` | 删除当前账户指定 key | | `hasItem(String key)` | 查找当前账户 key 的记录 ID | | `hasKeyValue(String key)` | 判断当前账户 key 是否存在 | | `readItem(String name, String defaultValue)` | 读取当前账户指定 key 的值 | | `removeAccount()` | 删除当前账户的所有数据 | | `readItemAsString(String name, String defaultValue)` | 读取字符串值(继承) | | `readItemAsInt(String name, int defaultValue)` | 读取 int 值(继承) | | `readItemAsLong(String name, long defaultValue)` | 读取 long 值(继承) | | `readItemAsFloat(String name, float defaultValue)` | 读取 float 值(继承) | | `readItemAsDouble(String name, double defaultValue)` | 读取 double 值(继承) | | `readItemAsBoolean(String name, boolean defaultValue)` | 读取 boolean 值(继承) | ### AccountCache 当前账户缓存,用于多用户场景下标识当前登录用户。 | 方法 | 说明 | |------|------| | `setCacheAccount(String account)` | 设置当前账户标识 | | `getCacheAccount()` | 获取当前账户标识 | ### DatabaseInitProvider(extends ContentProvider) 数据库自动初始化 Provider,App 启动时自动完成数据库初始化。 | 方法 | 说明 | |------|------| | `onCreate()` | 系统自动调用,读取 meta-data 配置并初始化数据库 | --- #### 使用说明 引入 ```gradle // 2.0.0 重大更新: // AGP 升级至 8.x,compileSdk/targetSdk 升至 35,Java 17 // 新增 DatabaseInitProvider 自动初始化,App 启动零代码接入 // 新增 WAL 并发模式,读写互不阻塞 // saveItem 改用 INSERT OR REPLACE 原子 upsert,建表增加 UNIQUE 约束 // 线程安全加固:volatile 单例、synchronized 读写、Holder 单例模式 // 资源泄漏修复:全部 Cursor 操作 try-finally 保护 // readItemAs* 系列方法提升至 AbstractTable 基类,消除代码重复 // StringBuffer → StringBuilder,DatabaseTable 接口改为 public // 完善全部类和方法的中文 Javadoc api "com.gitee.hljdrl:database:2.0.0" //新增SettingTable,用户配置表格,继承AbstractAccountTable,用户多用户环境 //KVTable增加:readItemAsString、readItemAsInt、readItemAsLong、readItemAsFloat、readItemAsDouble、readItemAsBoolean api "com.gitee.hljdrl:database:1.0.1" api "com.gitee.hljdrl:database:1.0.0" ``` 初始化数据库 ```java DataBaseManager.install(activity.getApplication(), "database.db", 1, null); ``` 数据库表初始化【隐私协议之后,具体根据APP业务而定】 ```java DataBaseManager.getInstance().onLoad(); ``` 数据表使用 ```java //数据保存 KVTable.getInstance().saveItem("app_set",System.currentTimeMillis()+""); //数据读取 String readItem = KVTable.getInstance().readItem("app_set", null); //数据删除 KVTable.getInstance().clear(); ``` ##自定义数据表,继承AbstractTable、AbstractAccountTable ```java package com.ffsky.template.demo.table; import android.content.ContentValues; import android.provider.BaseColumns; import com.gitee.hljdrl.database.AbstractTable; import com.gitee.hljdrl.database.DataBaseManager; import net.sqlcipher.Cursor; import net.sqlcipher.database.SQLiteDatabase; public class CacheTable extends AbstractTable { public static final class Fields implements BaseColumns { public static final String TAB_NAME = "_CacheTable" ; // public static final String DATA_KEY = "_DATA_KEY" ; public static final String DATA_BODY = "_DATA_JSON" ; public static final String COLUMNS[] = {_ID,DATA_KEY,DATA_BODY}; } private static CacheTable instance; public static CacheTable getInstance() { if(instance==null){ instance = new CacheTable(); } return instance; } private CacheTable() { } @Override public void create(SQLiteDatabase db) { String sql; sql = "CREATE TABLE " + CacheTable.Fields.TAB_NAME + " (" + CacheTable.Fields._ID+ " INTEGER PRIMARY KEY," + CacheTable.Fields.DATA_KEY + " TEXT," + CacheTable.Fields.DATA_BODY + " TEXT);"; DataBaseManager.execSQL(db, sql); } public int saveItem(String key, String body){ ContentValues cv = new ContentValues(); cv.put(CacheTable.Fields.DATA_KEY, key); cv.put(CacheTable.Fields.DATA_BODY, body); SQLiteDatabase db = DataBaseManager.getInstance().getWritableDatabase(); int $id = hasItem(key); if($id>-1){ int $rid = db.update(getTableName(), cv, CacheTable.Fields._ID+"=?", new String[]{String.valueOf($id)}); return $rid; }else{ long id = db.insert(getTableName(), null, cv); return (int) id; } } public int deleteItem(String key){ try { SQLiteDatabase db = DataBaseManager.getInstance().getWritableDatabase(); int _id = db.delete(getTableName(), CacheTable.Fields.DATA_KEY + "=?", new String[]{key}); return _id; }catch (Exception ex){ } return 0; } public int hasItem(String key){ SQLiteDatabase db = DataBaseManager.getInstance().getReadableDatabase(); Cursor c = db.query(getTableName(), getProjection(), CacheTable.Fields.DATA_KEY+"=?", new String[]{key}, null, null, null); if (hasData(c)) { c.moveToFirst(); int _id = c.getInt(c.getColumnIndex(CacheTable.Fields._ID)); closeCursor(c); return _id; } else { closeCursor(c); return -1; } } public String readItem(String _key,String _default) { String _result = _default; Cursor c = list(CacheTable.Fields.DATA_KEY+"=?",new String[]{_key}); try{ if(c!=null && c.getCount()>0){ c.moveToFirst(); String $body = getStringValue(c, CacheTable.Fields.DATA_BODY); _result = $body; } }finally { closeCursor(c); } return _result; } @Override protected String getTableName() { return CacheTable.Fields.TAB_NAME; } @Override protected String[] getProjection() { return CacheTable.Fields.COLUMNS; } } ``` 自定义表初始化 ```java DataBaseManager.getInstance().addTable(CacheTable.getInstance()); DataBaseManager.getInstance().checkTable(CacheTable.getInstance()); CacheTable.getInstance().saveItem("json", "save-json"); String var = CacheTable.getInstance().readItem("json", null); Toast.makeText(activity, " read: " + var, Toast.LENGTH_SHORT).show(); ```