国外做二手服装网站有哪些深圳家装互联网网站
国外做二手服装网站有哪些,深圳家装互联网网站,网站建设与维护基础知识,owl WordPress鸿蒙应用数据持久化实战#xff1a;从RDB基础到高级查询优化
如果你正在开发一个鸿蒙应用#xff0c;无论是需要保存用户的个人设置、缓存网络请求的列表数据#xff0c;还是管理一个离线可用的笔记库#xff0c;本地数据存储都是绕不开的核心环节。在众多方案中#xff0…鸿蒙应用数据持久化实战从RDB基础到高级查询优化如果你正在开发一个鸿蒙应用无论是需要保存用户的个人设置、缓存网络请求的列表数据还是管理一个离线可用的笔记库本地数据存储都是绕不开的核心环节。在众多方案中关系型数据库RDB因其强大的结构化查询能力和数据一致性保障成为了处理复杂业务逻辑时的首选。很多开发者虽然知道RDB但在实际项目中面对数据库设计、性能优化和复杂查询时依然会感到无从下手。这篇文章我将从一个真实的“个人笔记”应用场景出发带你一步步构建一个健壮、高效的数据层。我们不仅会覆盖基础的增删改查更会深入探讨如何设计表结构、利用谓词进行灵活查询以及那些官方文档里不会明说但在实际开发中能极大提升效率的实践技巧。无论你是刚刚接触鸿蒙RDB还是希望优化现有项目的数据库模块这里都有你需要的干货。1. 项目初始化与数据库架构设计在动手写第一行代码之前花点时间思考数据模型是至关重要的。一个糟糕的表设计会在后期带来无尽的麻烦。假设我们要开发一个笔记应用核心实体是“笔记”它至少包含标题、内容、创建时间和分类。一个常见的初学者错误是试图把所有信息塞进一张表里比如把分类名称直接存在笔记表里。这会导致数据冗余和更新异常。更优雅的设计是采用关系模型。让我们先定义两个核心的TypeScript接口这能帮助我们在编码阶段就获得良好的类型提示和代码补全。// 定义分类接口 interface Category { id?: number; // 主键自增 name: string; // 分类名称如“工作”、“生活” color: string; // 分类颜色用于UI展示存储为HEX字符串如‘#FF6B6B’ } // 定义笔记接口 interface Note { id?: number; // 主键自增 title: string; content: string; categoryId: number; // 外键关联到Category表的id createdAt: number; // 使用时间戳存储创建时间 updatedAt: number; // 更新时间戳用于同步或排序 isPinned: boolean; // 是否置顶 }接下来是创建数据库和表的SQL语句。我强烈建议将SQL语句单独管理而不是硬编码在业务逻辑里。这里我们创建两张表并建立外键关联。// 数据库SQL语句定义 const SQL_CREATE_CATEGORY_TABLE CREATE TABLE IF NOT EXISTS category ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, -- 分类名唯一 color TEXT NOT NULL DEFAULT #CCCCCC ); const SQL_CREATE_NOTE_TABLE CREATE TABLE IF NOT EXISTS note ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, category_id INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, is_pinned INTEGER DEFAULT 0, -- 使用0/1表示布尔值 FOREIGN KEY (category_id) REFERENCES category (id) ON DELETE RESTRICT ); // 为常用的查询字段创建索引可以大幅提升查询速度 const SQL_CREATE_NOTE_INDEX CREATE INDEX IF NOT EXISTS idx_note_category ON note (category_id); const SQL_CREATE_NOTE_TIME_INDEX CREATE INDEX IF NOT EXISTS idx_note_updated ON note (updated_at DESC);注意在鸿蒙RDB中虽然SQLite支持外键约束但默认可能是关闭的。为了确保数据完整性我们可以在初始化数据库后执行PRAGMA foreign_keys ON;语句。ON DELETE RESTRICT表示如果分类下还有笔记则禁止删除该分类这符合业务逻辑。有了清晰的数据模型和SQL定义我们就可以初始化RdbStore了。这里我封装了一个数据库管理器类采用单例模式避免在应用内重复创建连接。import { relationalStore } from kit.ArkData; class RDBManager { private static instance: RDBManager; private rdbStore: relationalStore.RdbStore | null null; private readonly DB_NAME note_app.db; private readonly DB_SECURITY_LEVEL relationalStore.SecurityLevel.S1; private constructor() {} public static getInstance(): RDBManager { if (!RDBManager.instance) { RDBManager.instance new RDBManager(); } return RDBManager.instance; } // 初始化数据库创建表和索引 public async initialize(context: Context): PromiserelationalStore.RdbStore { if (this.rdbStore) { return this.rdbStore; } try { // 1. 获取或创建RdbStore this.rdbStore await relationalStore.getRdbStore(context, { name: this.DB_NAME, securityLevel: this.DB_SECURITY_LEVEL, }); // 2. 执行建表SQL await this.rdbStore.executeSql(SQL_CREATE_CATEGORY_TABLE); await this.rdbStore.executeSql(SQL_CREATE_NOTE_TABLE); // 3. 创建索引 await this.rdbStore.executeSql(SQL_CREATE_NOTE_INDEX); await this.rdbStore.executeSql(SQL_CREATE_NOTE_TIME_INDEX); // 4. 开启外键约束可选但推荐 await this.rdbStore.executeSql(PRAGMA foreign_keys ON;); console.info(Database initialized successfully.); return this.rdbStore; } catch (error) { console.error(Failed to initialize database:, JSON.stringify(error)); throw error; } } public getStore(): relationalStore.RdbStore { if (!this.rdbStore) { throw new Error(RDBStore not initialized. Call initialize() first.); } return this.rdbStore; } }这个管理器类将数据库的创建、升级逻辑集中在一处后续业务模块只需通过RDBManager.getInstance().getStore()来获取操作句柄代码更加清晰和可维护。2. 核心数据操作增删改查的工程化实践掌握了数据库初始化我们进入最常使用的CRUD操作。我将展示如何将这些操作封装成易于理解和使用的函数并处理一些边界情况。2.1 插入数据处理关联与默认值插入操作看似简单但需要注意外键约束和默认值的设置。例如插入一条笔记前需要确保其categoryId指向一个已存在的分类。// 在RDBManager类中添加方法 public async insertNote(note: OmitNote, id): Promisenumber { const store this.getStore(); // 构建ValuesBucket注意字段名与数据库列名匹配 const valueBucket: relationalStore.ValuesBucket { title: note.title, content: note.content || , // 处理可能为undefined的内容 category_id: note.categoryId, created_at: note.createdAt || Date.now(), updated_at: note.updatedAt || Date.now(), is_pinned: note.isPinned ? 1 : 0, // 布尔值转换为整数 }; try { // insert方法返回新插入行的rowid通常就是自增主键id const newRowId await store.insert(note, valueBucket); return newRowId; } catch (error) { // 特别处理外键约束失败的情况 if (error.code 206) { // SQLite约束错误码可能不同需根据实际情况调整 console.error(Insert failed: Invalid category_id. The category might not exist.); throw new Error(所选分类不存在); } console.error(Insert note failed:, JSON.stringify(error)); throw error; } } // 批量插入分类的实用方法 public async insertDefaultCategories(): Promisevoid { const defaultCats: OmitCategory, id[] [ { name: 工作, color: #4A90E2 }, { name: 生活, color: #50E3C2 }, { name: 学习, color: #BD10E0 }, { name: 收藏, color: #F5A623 }, ]; const store this.getStore(); for (const cat of defaultCats) { // 使用insertWithConflictStrategy处理重复插入如果name有UNIQUE约束 try { await store.insert(category, { name: cat.name, color: cat.color }); } catch (e) { // 忽略重复错误继续插入其他 if (!e.message?.includes(UNIQUE constraint failed)) { throw e; } } } }2.2 查询数据灵活运用RdbPredicates查询是数据库操作中最灵活的部分。鸿蒙的RdbPredicates提供了一套链式API来构建查询条件比拼接SQL字符串更安全避免SQL注入且更符合ArkTS的编程风格。假设我们需要实现笔记列表页面它需要支持多种查询模式查看所有笔记按更新时间倒序。查看某个特定分类下的笔记。搜索包含特定关键词的笔记。仅显示置顶的笔记。下面是一个综合的查询方法public async queryNotes(options: { categoryId?: number; searchText?: string; onlyPinned?: boolean; offset?: number; limit?: number; }): PromiseNote[] { const store this.getStore(); const predicates new relationalStore.RdbPredicates(note); // 1. 按分类筛选 if (options.categoryId ! undefined) { predicates.equalTo(category_id, options.categoryId); } // 2. 文本搜索在标题和内容中模糊匹配 if (options.searchText options.searchText.trim().length 0) { // 使用OR条件组合两个字段的模糊匹配 predicates.or() .like(title, %${options.searchText}%) .like(content, %${options.searchText}%); } // 3. 置顶筛选 if (options.onlyPinned) { predicates.equalTo(is_pinned, 1); } // 4. 排序先按置顶状态降序置顶在前再按更新时间降序 predicates.orderByDesc(is_pinned).orderByDesc(updated_at); // 5. 分页支持 if (options.offset ! undefined options.limit ! undefined) { // 注意RdbPredicates本身可能不直接支持offset/limit需要结合query接口 // 一种做法是使用query的columns, predicates, params, 但更直接的方式是使用SQL // 这里展示使用predicates后通过结果集游标手动模拟分页适用于数据量不大时 } const resultSet: relationalStore.ResultSet await store.query(predicates, [id, title, content, category_id, created_at, updated_at, is_pinned]); const notes: Note[] []; // 遍历结果集 while (resultSet.goToNextRow()) { notes.push({ id: resultSet.getLong(resultSet.getColumnIndex(id)), title: resultSet.getString(resultSet.getColumnIndex(title)), content: resultSet.getString(resultSet.getColumnIndex(content)), categoryId: resultSet.getLong(resultSet.getColumnIndex(category_id)), createdAt: resultSet.getLong(resultSet.getColumnIndex(created_at)), updatedAt: resultSet.getLong(resultSet.getColumnIndex(updated_at)), isPinned: resultSet.getLong(resultSet.getColumnIndex(is_pinned)) 1, }); } resultSet.close(); // 重要使用完毕后关闭结果集 return notes; }对于更复杂的分页如果数据量很大使用LIMIT和OFFSET是更高效的做法。这时可以直接使用executeSql执行原生SQL查询。public async queryNotesWithPaging(page: number, pageSize: number, categoryId?: number): Promise{ data: Note[]; total: number } { const store this.getStore(); const offset (page - 1) * pageSize; let whereClause ; let params: (string | number)[] []; if (categoryId ! undefined) { whereClause WHERE category_id ?; params [categoryId]; } // 查询分页数据 const sql SELECT * FROM note ${whereClause} ORDER BY updated_at DESC LIMIT ? OFFSET ?; const resultSet await store.querySql(sql, [...params, pageSize, offset]); // 查询总数 const countSql SELECT COUNT(*) as total FROM note ${whereClause}; const countResultSet await store.querySql(countSql, params); await countResultSet.goToFirstRow(); const total countResultSet.getLong(countResultSet.getColumnIndex(total)); countResultSet.close(); // ... 将resultSet转换为Note数组 ... const data this.convertResultSetToNotes(resultSet); resultSet.close(); return { data, total }; }2.3 更新与删除原子操作与事务更新和删除操作必须谨慎务必使用谓词明确指定范围否则可能误操作全部数据。// 更新单条笔记 public async updateNote(id: number, updates: PartialOmitNote, id): Promiseboolean { const store this.getStore(); const predicates new relationalStore.RdbPredicates(note); predicates.equalTo(id, id); const valueBucket: relationalStore.ValuesBucket {}; if (updates.title ! undefined) valueBucket.title updates.title; if (updates.content ! undefined) valueBucket.content updates.content; if (updates.categoryId ! undefined) valueBucket.category_id updates.categoryId; if (updates.isPinned ! undefined) valueBucket.is_pinned updates.isPinned ? 1 : 0; // 每次更新都自动刷新updated_at时间戳 valueBucket.updated_at Date.now(); if (Object.keys(valueBucket).length 0) { return false; // 没有要更新的字段 } const affectedRows await store.update(valueBucket, predicates); return affectedRows 0; } // 删除笔记同时检查外键约束如果分类被笔记引用则删除失败 public async deleteCategory(id: number): Promiseboolean { const store this.getStore(); // 由于设置了ON DELETE RESTRICT如果有关联笔记此操作会抛出异常 try { const predicates new relationalStore.RdbPredicates(category); predicates.equalTo(id, id); const affectedRows await store.delete(predicates); return affectedRows 0; } catch (error) { if (error.code 206) { throw new Error(无法删除该分类因为仍有笔记属于此分类。请先移动或删除相关笔记。); } throw error; } }对于需要原子性的一组操作例如将A分类下的所有笔记转移到B分类然后删除A分类必须使用事务来保证数据一致性。public async mergeCategories(fromCategoryId: number, toCategoryId: number): Promisevoid { const store this.getStore(); // 开始事务 await store.beginTransaction(); try { // 1. 更新所有原分类下的笔记到新分类 const updatePredicates new relationalStore.RdbPredicates(note); updatePredicates.equalTo(category_id, fromCategoryId); const updateValues: relationalStore.ValuesBucket { category_id: toCategoryId, updated_at: Date.now() }; await store.update(updateValues, updatePredicates); // 2. 删除原分类 const deletePredicates new relationalStore.RdbPredicates(category); deletePredicates.equalTo(id, fromCategoryId); await store.delete(deletePredicates); // 提交事务 await store.commit(); console.info(Categories merged successfully.); } catch (error) { // 回滚事务 await store.rollback(); console.error(Failed to merge categories, transaction rolled back:, error); throw error; // 将错误抛给上层处理 } }提示事务是确保数据完整性的关键工具。任何涉及多步数据修改的操作如果其中一步失败会导致数据不一致就应该放在事务中执行。beginTransaction(),commit(),rollback()这三个方法必须成对使用。3. 性能优化与高级查询技巧当数据量增长到数千甚至上万条时基础操作的性能问题就会显现。这一节我们深入探讨如何让RDB跑得更快。3.1 索引为查询插上翅膀索引是提升查询速度最有效的手段但它是以增加写操作开销和存储空间为代价的。我们需要明智地创建索引。单列索引适用于WHERE,ORDER BY,JOIN子句中频繁使用的列。例如我们经常按category_id和updated_at查询所以为它们创建了索引。复合索引如果查询条件经常是多个列的组合复合索引可能更高效。例如如果经常同时按category_id和is_pinned查询可以创建(category_id, is_pinned)的复合索引。-- 创建复合索引的SQL CREATE INDEX IF NOT EXISTS idx_note_category_pinned ON note (category_id, is_pinned);但是索引不是越多越好。下面这个表格帮你决策何时创建索引场景推荐操作理由主键列 (id)自动创建索引主键默认有唯一索引用于快速定位。外键列 (category_id)强烈建议创建加速表连接(JOIN)和基于外键的筛选。频繁用于WHERE条件的列建议创建如is_pinned,created_at(范围查询)。频繁用于ORDER BY的列建议创建如updated_at DESC避免文件排序。频繁用于GROUP BY的列建议创建加速分组聚合操作。低基数列值重复率高谨慎创建如gender(仅‘男‘/‘女‘)索引收益可能不大。小表1000行通常不需要全表扫描可能更快索引带来的管理开销不划算。写多读少的表减少索引每次INSERT/UPDATE/DELETE都需要更新索引影响写入性能。3.2 查询优化避免性能陷阱即使有索引低效的查询语句也会拖慢应用。以下是一些常见的陷阱和优化方法避免使用SELECT *只查询需要的列。网络传输和内存解析更少的数据总是更快的。// 不推荐 const resultSet await store.query(predicates); // 推荐明确指定列 const resultSet await store.query(predicates, [id, title, updated_at]);谨慎使用LIKE %keyword%前导通配符%会导致索引失效。如果业务允许尽量使用后缀匹配LIKE keyword%或者考虑引入更专业的全文搜索方案。利用连接查询代替多次查询例如在获取笔记列表时如果还需要显示分类名称和颜色使用JOIN比先查笔记再循环查分类要高效得多。public async queryNotesWithCategory(): Promise(Note { categoryName: string; categoryColor: string })[] { const store this.getStore(); // 使用JOIN一次性获取笔记及其分类信息 const sql SELECT n.id, n.title, n.content, n.category_id, n.created_at, n.updated_at, n.is_pinned, c.name as category_name, c.color as category_color FROM note n LEFT JOIN category c ON n.category_id c.id ORDER BY n.updated_at DESC ; const resultSet await store.querySql(sql); const list []; while (resultSet.goToNextRow()) { list.push({ id: resultSet.getLong(resultSet.getColumnIndex(id)), title: resultSet.getString(resultSet.getColumnIndex(title)), // ... 其他笔记字段 ... categoryName: resultSet.getString(resultSet.getColumnIndex(category_name)), categoryColor: resultSet.getString(resultSet.getColumnIndex(category_color)), }); } resultSet.close(); return list; }3.3 大数据量处理分页与懒加载当列表数据很多时一次性加载所有数据到内存是不可取的。我们需要实现分页。上面已经展示了使用LIMIT/OFFSET的基础分页。但对于深度分页例如OFFSET 10000OFFSET效率会越来越低因为它需要先扫描并跳过前N条记录。一种更高效的方案是“游标分页”或“键集分页”。假设我们按updated_at倒序排列我们可以记住上一页最后一条记录的updated_at时间戳和id然后查询比这个时间戳更早的记录。public async queryNotesByCursor(lastUpdatedAt?: number, lastId?: number, limit: number 20): PromiseNote[] { const store this.getStore(); let sql SELECT * FROM note; const params: any[] []; if (lastUpdatedAt lastId) { // 关键查询比上一页最后一条记录“更旧”的记录 // (updated_at ?) OR (updated_at ? AND id ?) 确保排序稳定 sql WHERE (updated_at ?) OR (updated_at ? AND id ?); params.push(lastUpdatedAt, lastUpdatedAt, lastId); } sql ORDER BY updated_at DESC, id DESC LIMIT ?; params.push(limit); const resultSet await store.querySql(sql, params); // ... 转换结果集 ... return notes; }这种方法的优点是无论翻到第几页查询速度都只和LIMIT的值有关性能稳定。4. 架构进阶数据访问层与类型安全在大型项目中直接将数据库操作散落在各个UI页面或业务逻辑中会导致代码难以维护和测试。我们需要一个更清晰的架构。通常我会引入一个Repository仓库模式的数据访问层。4.1 定义Repository接口首先为笔记和分类定义抽象的仓库接口这有助于后续替换实现例如未来换用其他数据库或接入网络API。// 定义通用的数据操作接口 interface IRepositoryT { create(item: OmitT, id): Promisenumber; findById(id: number): PromiseT | undefined; findAll(options?: any): PromiseT[]; update(id: number, item: PartialOmitT, id): Promiseboolean; delete(id: number): Promiseboolean; } // 笔记仓库的特定接口 interface INoteRepository extends IRepositoryNote { findByCategory(categoryId: number): PromiseNote[]; search(keyword: string): PromiseNote[]; togglePin(id: number): Promiseboolean; } // 分类仓库的特定接口 interface ICategoryRepository extends IRepositoryCategory { findByName(name: string): PromiseCategory | undefined; }4.2 实现基于RDB的Repository然后基于我们之前封装的RDBManager来实现这些接口。class NoteRepositoryImpl implements INoteRepository { private rdbManager RDBManager.getInstance(); async create(note: OmitNote, id): Promisenumber { return this.rdbManager.insertNote(note); } async findById(id: number): PromiseNote | undefined { const store this.rdbManager.getStore(); const predicates new relationalStore.RdbPredicates(note); predicates.equalTo(id, id); const resultSet await store.query(predicates); if (await resultSet.goToFirstRow()) { // ... 转换逻辑 ... const note this.convertRowToNote(resultSet); resultSet.close(); return note; } resultSet.close(); return undefined; } async findAll(options?: { categoryId?: number }): PromiseNote[] { // 调用之前封装好的高级查询方法 return this.rdbManager.queryNotes({ categoryId: options?.categoryId }); } async findByCategory(categoryId: number): PromiseNote[] { return this.rdbManager.queryNotes({ categoryId }); } async search(keyword: string): PromiseNote[] { return this.rdbManager.queryNotes({ searchText: keyword }); } async togglePin(id: number): Promiseboolean { // 先查询当前状态 const note await this.findById(id); if (!note) return false; return this.rdbManager.updateNote(id, { isPinned: !note.isPinned }); } async update(id: number, updates: PartialOmitNote, id): Promiseboolean { return this.rdbManager.updateNote(id, updates); } async delete(id: number): Promiseboolean { const store this.rdbManager.getStore(); const predicates new relationalStore.RdbPredicates(note); predicates.equalTo(id, id); const affectedRows await store.delete(predicates); return affectedRows 0; } private convertRowToNote(resultSet: relationalStore.ResultSet): Note { // ... 转换逻辑 ... } }4.3 依赖注入与全局状态管理最后在应用顶层例如EntryAbility或一个全局的AppContext中初始化数据库和仓库并通过依赖注入或一个简单的服务定位器提供给UI组件使用。// 一个简单的服务容器 class AppService { private static _noteRepository: INoteRepository; private static _categoryRepository: ICategoryRepository; public static async initialize(context: Context): Promisevoid { // 1. 初始化数据库 await RDBManager.getInstance().initialize(context); // 2. 初始化默认数据如默认分类 await RDBManager.getInstance().insertDefaultCategories(); // 3. 创建仓库实例 this._noteRepository new NoteRepositoryImpl(); this._categoryRepository new CategoryRepositoryImpl(); } public static get noteRepository(): INoteRepository { return this._noteRepository; } public static get categoryRepository(): ICategoryRepository { return this._categoryRepository; } } // 在Ability的onCreate中初始化 export default class EntryAbility extends Ability { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { AppService.initialize(this.context).then(() { console.info(App services initialized.); }).catch((err) { console.error(Failed to initialize app services:, err); }); } } // 在UI组件中使用 Entry Component struct NoteListPage { State notes: Note[] []; async aboutToAppear() { // 直接通过服务类获取仓库实例进行操作 this.notes await AppService.noteRepository.findAll(); } build() { // ... UI构建 ... } }采用这种架构后你的数据访问逻辑变得集中、可测试且易于替换。UI组件只关心调用repository的方法而不需要知道底层是RDB、对象存储还是网络API。当业务复杂度增加需要增加数据缓存、日志记录或性能监控时只需要在Repository层进行装饰或拦截即可对上层业务透明。