You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
git/java/net/micode/notes/data/NotesDatabaseHelper.java

509 lines
22 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.data;
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.DataConstants;
import net.micode.notes.data.Notes.NoteColumns;
/**
* 笔记应用的SQLite数据库帮助类继承自Android的SQLiteOpenHelper
* 核心功能:
* 1. 采用单例模式管理数据库实例,避免多实例导致的数据库锁问题;
* 2. 创建笔记应用的核心数据表note表、data表及索引
* 3. 定义数据库触发器,实现数据联动逻辑(如文件夹笔记数量自动更新、笔记内容同步等);
* 4. 初始化系统预设文件夹(根目录、通话记录、临时、回收站);
* 5. 处理数据库版本升级,保证数据迁移的兼容性。
*
* @author MiCode Open Source Community
* @date 2010-2011
*/
public class NotesDatabaseHelper extends SQLiteOpenHelper {
/**
* 数据库文件名,存储在应用的私有数据目录下
*/
private static final String DB_NAME = "note.db";
/**
* 数据库版本号用于版本升级控制当前为6增加了标签功能
*/
private static final int DB_VERSION = 6;
/**
* 数据表名称接口定义note表、data表、标签表和便签-标签关联表的名称常量
*/
public interface TABLE {
// 笔记/文件夹表名称
public static final String NOTE = "note";
// 笔记明细数据表名称(存储文本、通话记录等具体内容)
public static final String DATA = "data";
}
/**
* 日志标签用于Logcat输出时标识当前类方便调试定位问题
*/
private static final String TAG = "NotesDatabaseHelper";
/**
* 单例实例全局唯一的NotesDatabaseHelper对象
* 采用静态变量存储,避免多次创建数据库帮助类实例
*/
private static NotesDatabaseHelper mInstance;
// ====================== 数据表创建SQL语句 ======================
/**
* 创建note表的SQL语句
* note表存储笔记和文件夹的基础信息如ID、父级ID、创建时间、类型等
* 所有列均设置默认值保证数据完整性主键为ID自增整型
*/
private static final String CREATE_NOTE_TABLE_SQL =
"CREATE TABLE " + TABLE.NOTE + "(" +
NoteColumns.ID + " INTEGER PRIMARY KEY," + // 主键ID
NoteColumns.PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + // 父级ID关联文件夹
NoteColumns.ALERTED_DATE + " INTEGER NOT NULL DEFAULT 0," + // 提醒时间
NoteColumns.BG_COLOR_ID + " INTEGER NOT NULL DEFAULT 0," + // 背景颜色ID
// 创建时间:默认值为当前时间戳(秒转毫秒)
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.HAS_ATTACHMENT + " INTEGER NOT NULL DEFAULT 0," + // 是否有附件
// 修改时间:默认值为当前时间戳
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.NOTES_COUNT + " INTEGER NOT NULL DEFAULT 0," + // 文件夹下的笔记数量
NoteColumns.SNIPPET + " TEXT NOT NULL DEFAULT ''," + // 文件夹名称/笔记摘要
NoteColumns.TYPE + " INTEGER NOT NULL DEFAULT 0," + // 类型(笔记/文件夹/系统)
NoteColumns.PINNED + " INTEGER NOT NULL DEFAULT 0," + // 是否置顶0不置顶1置顶
NoteColumns.WIDGET_ID + " INTEGER NOT NULL DEFAULT 0," + // 关联的Widget ID
NoteColumns.WIDGET_TYPE + " INTEGER NOT NULL DEFAULT -1," + // 关联的Widget类型
NoteColumns.SYNC_ID + " INTEGER NOT NULL DEFAULT 0," + // 同步IDGTask
NoteColumns.LOCAL_MODIFIED + " INTEGER NOT NULL DEFAULT 0," + // 本地修改标记
NoteColumns.ORIGIN_PARENT_ID + " INTEGER NOT NULL DEFAULT 0," + // 原始父级ID
NoteColumns.GTASK_ID + " TEXT NOT NULL DEFAULT ''," + // GTask ID
NoteColumns.VERSION + " INTEGER NOT NULL DEFAULT 0" + // 版本号
")";
/**
* 创建data表的SQL语句
* data表存储笔记的具体内容文本、通话记录等与note表通过NOTE_ID关联
*/
private static final String CREATE_DATA_TABLE_SQL =
"CREATE TABLE " + TABLE.DATA + "(" +
DataColumns.ID + " INTEGER PRIMARY KEY," + // 主键ID
DataColumns.MIME_TYPE + " TEXT NOT NULL," + // 数据类型(文本/通话记录)
DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," + // 关联的note表ID
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 创建时间
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," + // 修改时间
DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," + // 数据内容
DataColumns.DATA1 + " INTEGER," + // 通用整型列1
DataColumns.DATA2 + " INTEGER," + // 通用整型列2
DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," + // 通用文本列3
DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," + // 通用文本列4
DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" + // 通用文本列5
")";
/**
* 创建data表的NOTE_ID索引的SQL语句
* 索引用于提升根据NOTE_ID查询data表的性能频繁关联查询场景
*/
private static final String CREATE_DATA_NOTE_ID_INDEX_SQL =
"CREATE INDEX IF NOT EXISTS note_id_index ON " +
TABLE.DATA + "(" + DataColumns.NOTE_ID + ");";
// ====================== 数据库触发器SQL语句note表 ======================
/**
* 触发器更新笔记的父级ID时增加新文件夹的笔记数量
* 触发时机note表的PARENT_ID列更新后
* 逻辑将新父级文件夹的NOTES_COUNT加1
*/
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER increase_folder_count_on_update "+
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
" WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
" END";
/**
* 触发器更新笔记的父级ID时减少旧文件夹的笔记数量
* 触发时机note表的PARENT_ID列更新后
* 逻辑将旧父级文件夹的NOTES_COUNT减1需保证数量大于0
*/
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER decrease_folder_count_on_update " +
" AFTER UPDATE OF " + NoteColumns.PARENT_ID + " ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
" WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
" AND " + NoteColumns.NOTES_COUNT + ">0" + ";" +
" END";
/**
* 触发器:插入新笔记时,增加对应文件夹的笔记数量
* 触发时机note表插入数据后
* 逻辑将新笔记的父级文件夹的NOTES_COUNT加1
*/
private static final String NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER =
"CREATE TRIGGER increase_folder_count_on_insert " +
" AFTER INSERT ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + " + 1" +
" WHERE " + NoteColumns.ID + "=new." + NoteColumns.PARENT_ID + ";" +
" END";
/**
* 触发器:删除笔记时,减少对应文件夹的笔记数量
* 触发时机note表删除数据后
* 逻辑将被删除笔记的父级文件夹的NOTES_COUNT减1需保证数量大于0
*/
private static final String NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER =
"CREATE TRIGGER decrease_folder_count_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN " +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.NOTES_COUNT + "=" + NoteColumns.NOTES_COUNT + "-1" +
" WHERE " + NoteColumns.ID + "=old." + NoteColumns.PARENT_ID +
" AND " + NoteColumns.NOTES_COUNT + ">0;" +
" END";
// ====================== 数据库触发器SQL语句data表 ======================
/**
* 触发器插入文本笔记数据时同步更新note表的摘要内容
* 触发时机data表插入数据后且数据类型为文本笔记
* 逻辑将note表的SNIPPET列设为data表的CONTENT内容
*/
private static final String DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER =
"CREATE TRIGGER update_note_content_on_insert " +
" AFTER INSERT ON " + TABLE.DATA +
" WHEN new." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
" WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
" END";
/**
* 触发器更新文本笔记数据时同步更新note表的摘要内容
* 触发时机data表更新数据后且数据类型为文本笔记
*/
private static final String DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER =
"CREATE TRIGGER update_note_content_on_update " +
" AFTER UPDATE ON " + TABLE.DATA +
" WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=new." + DataColumns.CONTENT +
" WHERE " + NoteColumns.ID + "=new." + DataColumns.NOTE_ID + ";" +
" END";
/**
* 触发器删除文本笔记数据时清空note表的摘要内容
* 触发时机data表删除数据后且数据类型为文本笔记
*/
private static final String DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER =
"CREATE TRIGGER update_note_content_on_delete " +
" AFTER delete ON " + TABLE.DATA +
" WHEN old." + DataColumns.MIME_TYPE + "='" + DataConstants.NOTE + "'" +
" BEGIN" +
" UPDATE " + TABLE.NOTE +
" SET " + NoteColumns.SNIPPET + "=''" +
" WHERE " + NoteColumns.ID + "=old." + DataColumns.NOTE_ID + ";" +
" END";
/**
* 触发器删除笔记时级联删除对应的data表数据
* 触发时机note表删除数据后
* 逻辑删除data表中NOTE_ID等于被删除笔记ID的所有数据避免数据冗余
*/
private static final String NOTE_DELETE_DATA_ON_DELETE_TRIGGER =
"CREATE TRIGGER delete_data_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN" +
" DELETE FROM " + TABLE.DATA +
" WHERE " + DataColumns.NOTE_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
* 触发器:删除文件夹时,级联删除该文件夹下的所有笔记
* 触发时机note表删除数据后删除的是文件夹
* 逻辑删除note表中PARENT_ID等于被删除文件夹ID的所有笔记
*/
private static final String FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER =
"CREATE TRIGGER folder_delete_notes_on_delete " +
" AFTER DELETE ON " + TABLE.NOTE +
" BEGIN" +
" DELETE FROM " + TABLE.NOTE +
" WHERE " + NoteColumns.PARENT_ID + "=old." + NoteColumns.ID + ";" +
" END";
/**
* 构造方法调用父类SQLiteOpenHelper的构造方法初始化数据库
*
* @param context 上下文对象,用于访问应用的资源和私有目录
*/
public NotesDatabaseHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
/**
* 创建note表并初始化相关触发器和系统文件夹
*
* @param db SQLiteDatabase对象用于执行SQL语句
*/
public void createNoteTable(SQLiteDatabase db) {
// 执行创建note表的SQL
db.execSQL(CREATE_NOTE_TABLE_SQL);
// 重新创建note表的触发器
reCreateNoteTableTriggers(db);
// 初始化系统文件夹
createSystemFolder(db);
Log.d(TAG, "note table has been created");
}
/**
* 重新创建note表的所有触发器先删除旧触发器再创建新触发器
* 用于数据库升级或触发器重建场景,避免触发器重复创建
*
* @param db SQLiteDatabase对象
*/
private void reCreateNoteTableTriggers(SQLiteDatabase db) {
// 删除旧触发器(如果存在)
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_update");
db.execSQL("DROP TRIGGER IF EXISTS decrease_folder_count_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS delete_data_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS increase_folder_count_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS folder_delete_notes_on_delete");
// 创建新触发器
db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_UPDATE_TRIGGER);
db.execSQL(NOTE_DECREASE_FOLDER_COUNT_ON_DELETE_TRIGGER);
db.execSQL(NOTE_DELETE_DATA_ON_DELETE_TRIGGER);
db.execSQL(NOTE_INCREASE_FOLDER_COUNT_ON_INSERT_TRIGGER);
db.execSQL(FOLDER_DELETE_NOTES_ON_DELETE_TRIGGER);
}
/**
* 初始化系统预设文件夹(通话记录、根目录、临时、回收站)
* 向note表插入系统文件夹数据类型为TYPE_SYSTEM
*
* @param db SQLiteDatabase对象
*/
private void createSystemFolder(SQLiteDatabase db) {
ContentValues values = new ContentValues();
/**
* 1. 通话记录文件夹:存储通话记录笔记
*/
values.put(NoteColumns.ID, Notes.ID_CALL_RECORD_FOLDER); // 设置固定ID
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); // 类型为系统文件夹
db.insert(TABLE.NOTE, null, values);
/**
* 2. 根文件夹:默认文件夹,存储无归属的笔记
*/
values.clear(); // 清空ContentValues
values.put(NoteColumns.ID, Notes.ID_ROOT_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* 3. 临时文件夹:用于移动笔记时的临时存储
*/
values.clear();
values.put(NoteColumns.ID, Notes.ID_TEMPARAY_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
/**
* 创建data表初始化相关触发器和索引
*
* @param db SQLiteDatabase对象
*/
public void createDataTable(SQLiteDatabase db) {
// 执行创建data表的SQL
db.execSQL(CREATE_DATA_TABLE_SQL);
// 重新创建data表的触发器
reCreateDataTableTriggers(db);
// 创建note_id索引提升查询性能
db.execSQL(CREATE_DATA_NOTE_ID_INDEX_SQL);
Log.d(TAG, "data table has been created");
}
/**
* 重新创建data表的所有触发器先删除旧触发器再创建新触发器
*
* @param db SQLiteDatabase对象
*/
private void reCreateDataTableTriggers(SQLiteDatabase db) {
// 删除旧触发器
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_delete");
// 创建新触发器
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_INSERT_TRIGGER);
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_UPDATE_TRIGGER);
db.execSQL(DATA_UPDATE_NOTE_CONTENT_ON_DELETE_TRIGGER);
}
/**
* 获取NotesDatabaseHelper的单例实例线程安全
* 采用synchronized关键字保证多线程环境下实例的唯一性避免数据库锁问题
*
* @param context 上下文对象建议使用Application Context避免内存泄漏
* @return 全局唯一的NotesDatabaseHelper实例
*/
static synchronized NotesDatabaseHelper getInstance(Context context) {
// 懒汉式加载实例为null时才创建
if (mInstance == null) {
mInstance = new NotesDatabaseHelper(context);
}
return mInstance;
}
/**
* 数据库首次创建时调用的方法
* 执行note表和data表的创建逻辑
*
* @param db SQLiteDatabase对象
*/
@Override
public void onCreate(SQLiteDatabase db) {
createNoteTable(db);
createDataTable(db);
}
/**
* 数据库版本升级时调用的方法
* 处理从旧版本1/2/3升级到新版本4的逻辑包括表结构修改、数据迁移、触发器重建等
*
* @param db SQLiteDatabase对象
* @param oldVersion 旧数据库版本号
* @param newVersion 新数据库版本号
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 是否需要重建触发器的标记
boolean reCreateTriggers = false;
// 是否跳过版本2升级的标记版本1升级包含版本2的逻辑
boolean skipV2 = false;
// 从版本1升级到版本2
if (oldVersion == 1) {
upgradeToV2(db);
skipV2 = true; // 版本1的升级已包含版本2到3的逻辑
oldVersion++;
}
// 从版本2升级到版本3未跳过的情况
if (oldVersion == 2 && !skipV2) {
upgradeToV3(db);
reCreateTriggers = true; // 需要重建触发器
oldVersion++;
}
// 从版本3升级到版本4
if (oldVersion == 3) {
upgradeToV4(db);
oldVersion++;
}
// 从版本4升级到版本5
if (oldVersion == 4) {
// 为note表添加pinned字段用于置顶功能
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.PINNED
+ " INTEGER NOT NULL DEFAULT 0");
oldVersion++;
}
// 如果需要,重建触发器
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
}
// 升级失败时抛出异常,提示版本升级错误
if (oldVersion != newVersion) {
throw new IllegalStateException("Upgrade notes database to version " + newVersion
+ "fails");
}
}
/**
* 从版本1升级到版本2的逻辑
* 删除旧表,重新创建新表(全量重建,会丢失数据,适用于早期版本)
*
* @param db SQLiteDatabase对象
*/
private void upgradeToV2(SQLiteDatabase db) {
// 删除旧的note表和data表
db.execSQL("DROP TABLE IF EXISTS " + TABLE.NOTE);
db.execSQL("DROP TABLE IF EXISTS " + TABLE.DATA);
// 重新创建表和初始化数据
createNoteTable(db);
createDataTable(db);
}
/**
* 从版本2升级到版本3的逻辑
* 1. 删除无用的触发器;
* 2. 为note表添加GTASK_ID列。
*
* @param db SQLiteDatabase对象
*/
private void upgradeToV3(SQLiteDatabase db) {
// 删除无用的触发器(更新笔记修改时间的触发器)
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_delete");
db.execSQL("DROP TRIGGER IF EXISTS update_note_modified_date_on_update");
// 为note表添加GTASK_ID列用于GTask同步
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_ID
+ " TEXT NOT NULL DEFAULT ''");
}
/**
* 从版本3升级到版本4的逻辑
* 为note表添加VERSION列用于数据同步的版本控制
*
* @param db SQLiteDatabase对象
*/
private void upgradeToV4(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.VERSION
+ " INTEGER NOT NULL DEFAULT 0");
}
}