在UniApp开发中,SQLite作为轻量级本地数据库被广泛使用。但直接操作SQLite会遇到几个典型问题:需要手动拼接SQL语句容易出错、数据类型转换繁琐、多表关联查询代码冗长。我在实际项目中就遇到过SQL拼接错误导致数据丢失的惨痛教训。
ORM(对象关系映射)的核心思想就是把数据库表映射为JavaScript对象。比如用户表users可以直接用user.name访问,而不是写SELECT name FROM users WHERE id=1。这种操作方式更符合前端开发者的思维习惯。
举个例子,假设我们有个商品表products,传统SQLite操作是这样的:
javascript复制const sql = `INSERT INTO products (name, price) VALUES ('手机', 3999)`;
plus.sqlite.executeSql({sql});
而ORM封装后可以这样写:
javascript复制const product = {name: '手机', price: 3999};
db.products.insert(product);
首先需要封装基础的数据库连接功能。我通常会创建一个SqlHelper类,包含以下核心方法:
javascript复制class SqlHelper {
constructor() {
this.dbName = 'app.db';
this.dbPath = '_doc/app.db';
}
// 打开或创建数据库
open() {
return new Promise((resolve, reject) => {
plus.sqlite.openDatabase({
name: this.dbName,
path: this.dbPath,
success: resolve,
fail: reject
});
});
}
// 关闭数据库
close() {
return new Promise((resolve, reject) => {
plus.sqlite.closeDatabase({
name: this.dbName,
success: resolve,
fail: reject
});
});
}
}
这里有几个实用技巧:
_doc或_downloads目录接下来封装最基础的增删改查方法。以插入数据为例:
javascript复制// 原始SQL方式
async insert(table, data) {
const fields = Object.keys(data).join(',');
const values = Object.values(data)
.map(v => typeof v === 'string' ? `'${v}'` : v)
.join(',');
const sql = `INSERT INTO ${table} (${fields}) VALUES (${values})`;
return this.executeSql(sql);
}
// 使用示例
await db.insert('products', {
name: '笔记本电脑',
price: 5999,
stock: 100
});
这里要注意SQL注入防护,实际项目中我会用参数化查询替代字符串拼接。
SQLite只有5种基本数据类型,但JavaScript类型更丰富。我们需要处理以下转换:
| JavaScript类型 | SQLite类型 | 处理方式 |
|---|---|---|
| Number | INTEGER/REAL | 根据是否整数判断 |
| String | TEXT | 自动添加引号 |
| Boolean | INTEGER | true→1, false→0 |
| Date | TEXT | 格式化为ISO字符串 |
实现代码示例:
javascript复制function jsToSqlType(value) {
switch(typeof value) {
case 'number':
return Number.isInteger(value) ? 'INTEGER' : 'REAL';
case 'string':
return 'TEXT';
case 'boolean':
return 'INTEGER';
default:
if(value instanceof Date) return 'TEXT';
throw new Error(`Unsupported type: ${typeof value}`);
}
}
开发过程中经常需要修改表结构,我总结了两种方案:
方案1:删除重建
javascript复制async function recreateTable(table, newSchema) {
await this.executeSql(`DROP TABLE IF EXISTS ${table}`);
await this.createTable(table, newSchema);
}
方案2:增量更新(推荐)
javascript复制async function addColumn(table, column, type) {
try {
await this.executeSql(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
} catch(e) {
if(!e.message.includes('duplicate column')) {
throw e;
}
}
}
实际项目中我会在App启动时检查所有表结构,自动添加新增字段。
假设有用户表和用户详情表:
javascript复制// 传统SQL写法
const sql = `
SELECT u.*, ud.*
FROM users u
JOIN user_details ud ON u.id = ud.user_id
WHERE u.id = 1
`;
// ORM封装后
const user = await db.users.findOne(1, {
include: [{ model: db.userDetails }]
});
比如文章和评论:
javascript复制// 查询文章及其所有评论
const article = await db.articles.findOne(1, {
include: [{
model: db.comments,
where: { status: 'approved' } // 可以加查询条件
}]
});
// 生成的SQL
// SELECT a.*, c.*
// FROM articles a
// LEFT JOIN comments c ON a.id = c.article_id AND c.status = 'approved'
// WHERE a.id = 1
对于复杂关联,可以使用懒加载避免一次性查询过多数据:
javascript复制const order = await db.orders.findOne(1);
// 后续需要时再加载关联数据
const products = await order.getProducts();
以一个简易电商App为例,典型表结构如下:
products表
javascript复制{
id: 1,
name: '智能手机',
price: 2999,
stock: 100,
createdAt: '2023-01-01'
}
users表
javascript复制{
id: 1,
username: 'testuser',
password: '加密字符串',
avatar: 'path/to/avatar.jpg'
}
orders表
javascript复制{
id: 1,
userId: 1,
totalAmount: 5998,
status: 'paid'
}
对应的ORM模型定义:
javascript复制// 产品模型
db.defineModel('products', {
name: { type: 'TEXT', notNull: true },
price: { type: 'REAL', defaultValue: 0 },
stock: { type: 'INTEGER' }
});
// 订单关联
db.orders.belongsTo(db.users, { foreignKey: 'userId' });
db.orders.hasMany(db.orderItems, { foreignKey: 'orderId' });
查询最近3个订单及其商品:
javascript复制const orders = await db.orders.findAll({
where: { userId: 1 },
order: [['createdAt', 'DESC']],
limit: 3,
include: [{
model: db.orderItems,
include: [db.products]
}]
});
单条插入改为批量插入可提升10倍以上性能:
javascript复制// 低效写法
for(const product of products) {
await db.products.insert(product);
}
// 高效写法
await db.products.bulkCreate(products);
关键操作要使用事务保证数据一致性:
javascript复制await db.transaction(async (t) => {
await db.orders.create({...}, { transaction: t });
await db.orderItems.bulkCreate([...], { transaction: t });
await db.inventory.decrement('stock', {...}, { transaction: t });
});
为常用查询字段添加索引:
javascript复制// 创建索引
await db.executeSql('CREATE INDEX idx_products_category ON products(category_id)');
// 查询时使用索引
await db.products.findAll({
where: { categoryId: 5 },
order: [['price', 'ASC']]
});
坑1:SQLite不支持ALTER TABLE删除列
解决方案是先创建新表,然后迁移数据:
javascript复制async function dropColumn(table, column) {
const tempTable = `${table}_temp`;
const columns = await this.getColumns(table);
// 过滤要删除的列
const newColumns = columns.filter(c => c.name !== column);
// 创建新表
await this.createTable(tempTable, newColumns);
// 迁移数据
await this.executeSql(`
INSERT INTO ${tempTable}
SELECT ${newColumns.map(c => c.name).join(',')}
FROM ${table}
`);
// 替换表
await this.dropTable(table);
await this.executeSql(`ALTER TABLE ${tempTable} RENAME TO ${table}`);
}
坑2:多线程并发问题
解决方案是使用单例模式管理数据库连接:
javascript复制let instance = null;
class Database {
static getInstance() {
if(!instance) {
instance = new Database();
await instance.open();
}
return instance;
}
}
最后分享我目前在用的封装架构:
code复制src/
├── database/
│ ├── index.js # 数据库入口
│ ├── SqlHelper.js # 基础SQL操作
│ ├── Model.js # ORM基类
│ └── models/ # 各业务模型
│ ├── User.js
│ ├── Product.js
│ └── ...
└── utils/
└── dbUtils.js # 数据库工具函数
关键代码结构:
javascript复制// Model.js
class Model {
constructor(tableName) {
this.table = tableName;
}
async find(where) {
const sql = `SELECT * FROM ${this.table} WHERE ${this.buildWhere(where)}`;
return SqlHelper.select(sql);
}
buildWhere(conditions) {
// 构造WHERE条件
}
}
// Product.js
class Product extends Model {
constructor() {
super('products');
}
// 自定义业务方法
async search(keyword) {
return this.find({ name: { like: `%${keyword}%` } });
}
}
这种架构下,业务代码可以这样使用:
javascript复制const product = new Product();
const results = await product.search('手机');
随着App迭代,数据库结构可能需要变更。我采用的迁移方案是:
version表获取当前版本javascript复制// migration/v1.0.0__init_db.js
exports.up = async (db) => {
await db.executeSql(`CREATE TABLE users (...)`);
await db.executeSql(`CREATE TABLE products (...)`);
};
// migration/v1.1.0__add_product_category.js
exports.up = async (db) => {
await db.addColumn('products', 'categoryId', 'INTEGER');
};
在App启动时执行迁移:
javascript复制const migrations = [
require('./v1.0.0__init_db'),
require('./v1.1.0__add_product_category')
];
async function migrate() {
const currentVersion = await getCurrentVersion();
for(const migration of migrations) {
if(migration.version > currentVersion) {
await migration.up(db);
await updateVersion(migration.version);
}
}
}
经过多个项目实践,我总结出以下经验:
典型错误示例:
javascript复制// 错误:嵌套回调+事务未处理
db.transaction(() => {
db.insert('table1', data1, () => {
db.update('table2', data2, () => {
// 如果这里出错,事务不会回滚
});
});
});
正确写法:
javascript复制// 正确:async/await+try-catch
try {
await db.transaction(async () => {
await db.insert('table1', data1);
await db.update('table2', data2);
});
} catch(e) {
console.error('操作失败', e);
}