Android SQLite开发总结与注意事项
SQLite 简介
SQLite 是一款轻型的数据库,是遵守 ACID 的关系型数据库管理系统,它包含在一个相对小的 C 库中。它是 D.Richard Hipp 为了简化嵌入式设备存储数据的方式而开发的。SQLite 不需要一个单独的服务器进程或操作的系统(无服务器的),SQLite 数据库完全包含在一个单一的磁盘文件中,并且其数据库文件可以在不同字节顺序的机器间自由共享。这种特性使得 SQLite 非常适合在 Android 应用中作为本地数据存储方案。
Android 中使用 SQLite 的优势
- 轻量级:在 Android 设备资源有限的情况下,SQLite 的轻量级特性不会过多占用系统资源,不会对应用的性能造成较大负担。
- 本地存储:数据存储在设备本地,不需要额外的网络连接来读写数据,这对于一些需要离线使用或者对数据实时性要求较高的应用非常友好,比如一些记录类、游戏类应用。
- 易于集成:Android 系统内置了对 SQLite 的支持,开发人员可以很方便地在应用中使用 SQLite 数据库,通过 Android 提供的 SQLiteOpenHelper 等类可以快速实现数据库的创建、升级等操作。
SQLiteOpenHelper 类解析
- 作用:SQLiteOpenHelper 类是 Android 提供的一个用于管理数据库创建和版本管理的帮助类。它简化了数据库创建和升级的过程,开发人员只需要继承这个类并实现其中的一些抽象方法即可。
- 关键方法
- onCreate(SQLiteDatabase db):当数据库第一次被创建时,系统会调用这个方法。在这个方法中,开发人员通常会编写创建数据库表、初始化数据等操作的 SQL 语句。例如:
@Override
public void onCreate(SQLiteDatabase db) {
String createTableSql = "CREATE TABLE IF NOT EXISTS users (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT," +
"name TEXT," +
"age INTEGER)";
db.execSQL(createTableSql);
}
在上述代码中,我们创建了一个名为 users
的表,该表有 id
(自增长的主键)、name
(文本类型)和 age
(整型)三个字段。
- onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion):当数据库版本发生变化时,系统会调用这个方法。通常在应用更新,数据库结构需要改变时会用到。例如,我们需要给 users
表添加一个 email
字段:
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < 2) {
String addColumnSql = "ALTER TABLE users ADD COLUMN email TEXT";
db.execSQL(addColumnSql);
}
}
这里假设数据库从版本 1 升级到版本 2,我们通过 ALTER TABLE
语句给 users
表添加了 email
字段。
- getReadableDatabase():获取一个可读取的数据库实例。如果数据库不存在,会先调用 onCreate
方法创建数据库;如果数据库版本需要升级,会先调用 onUpgrade
方法进行升级。
- getWritableDatabase():获取一个可读写的数据库实例,同样会处理数据库的创建和升级操作。与 getReadableDatabase()
的区别在于,当磁盘空间已满等情况导致数据库只能以只读方式打开时,getReadableDatabase()
仍然可以返回一个只读的数据库实例,而 getWritableDatabase()
会返回 null
。
数据库操作之增删改查
- 插入数据(INSERT)
- 使用 execSQL 方法:
SQLiteDatabase db = sqliteOpenHelper.getWritableDatabase();
String insertSql = "INSERT INTO users (name, age) VALUES ('John', 25)";
db.execSQL(insertSql);
db.close();
这种方式直接执行 SQL 语句进行插入操作,比较直观,但对于数据中的特殊字符等情况需要手动处理转义,否则可能导致 SQL 注入问题。 - 使用 ContentValues 类:
SQLiteDatabase db = sqliteOpenHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("name", "Jane");
values.put("age", 23);
long newRowId = db.insert("users", null, values);
db.close();
ContentValues
类就像一个键值对的容器,使用它来插入数据更安全,Android 系统会自动处理数据的转义和 SQL 注入问题。insert
方法返回新插入行的行 ID,如果插入失败返回 -1。
2. 查询数据(SELECT)
- 简单查询:
SQLiteDatabase db = sqliteOpenHelper.getReadableDatabase();
String[] projection = {"id", "name", "age"};
Cursor cursor = db.query("users", projection, null, null, null, null, null);
try {
while (cursor.moveToNext()) {
int id = cursor.getInt(cursor.getColumnIndexOrThrow("id"));
String name = cursor.getString(cursor.getColumnIndexOrThrow("name"));
int age = cursor.getInt(cursor.getColumnIndexOrThrow("age"));
Log.d("QueryResult", "Id: " + id + ", Name: " + name + ", Age: " + age);
}
} finally {
cursor.close();
db.close();
}
在上述代码中,query
方法的参数依次为表名、要查询的列(投影)、筛选条件(WHERE
子句)、筛选条件的参数、分组条件(GROUP BY
子句)、分组筛选条件(HAVING
子句)和排序条件(ORDER BY
子句)。这里我们进行了一个简单的全表查询,并遍历 Cursor
获取每一行的数据。
- 带条件查询:
SQLiteDatabase db = sqliteOpenHelper.getReadableDatabase();
String[] projection = {"id", "name", "age"};
String selection = "age >?";
String[] selectionArgs = {"20"};
Cursor cursor = db.query("users", projection, selection, selectionArgs, null, null, null);
// 后续处理 cursor 同简单查询
这里我们使用了筛选条件,只查询年龄大于 20 的用户数据。selectionArgs
用于替换 selection
中的占位符 ?
,这样可以有效防止 SQL 注入。
3. 更新数据(UPDATE)
- 使用 execSQL 方法:
SQLiteDatabase db = sqliteOpenHelper.getWritableDatabase();
String updateSql = "UPDATE users SET age = age + 1 WHERE name = 'John'";
db.execSQL(updateSql);
db.close();
同样,这种方式需要注意 SQL 注入问题。 - 使用 ContentValues 类:
SQLiteDatabase db = sqliteOpenHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("age", 26);
String whereClause = "name =?";
String[] whereArgs = {"John"};
int rowsAffected = db.update("users", values, whereClause, whereArgs);
db.close();
update
方法返回受影响的行数,通过 ContentValues
设置要更新的列和值,通过 whereClause
和 whereArgs
设置更新的条件,这种方式更安全可靠。
4. 删除数据(DELETE)
- 使用 execSQL 方法:
SQLiteDatabase db = sqliteOpenHelper.getWritableDatabase();
String deleteSql = "DELETE FROM users WHERE name = 'John'";
db.execSQL(deleteSql);
db.close();
- **使用 delete 方法**:
SQLiteDatabase db = sqliteOpenHelper.getWritableDatabase();
String whereClause = "name =?";
String[] whereArgs = {"John"};
int rowsDeleted = db.delete("users", whereClause, whereArgs);
db.close();
delete
方法返回删除的行数,通过 whereClause
和 whereArgs
设置删除条件,避免误删数据。
事务处理
- 事务的概念:事务是数据库操作的一个逻辑单元,由一系列的数据库操作组成,这些操作要么全部成功执行,要么全部失败回滚,以保证数据的一致性和完整性。在 SQLite 中,Android 提供了对事务的支持。
- 事务操作示例:
SQLiteDatabase db = sqliteOpenHelper.getWritableDatabase();
try {
db.beginTransaction();
// 插入操作 1
ContentValues values1 = new ContentValues();
values1.put("name", "Alice");
values1.put("age", 22);
db.insert("users", null, values1);
// 插入操作 2
ContentValues values2 = new ContentValues();
values2.put("name", "Bob");
values2.put("age", 24);
db.insert("users", null, values2);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
db.close();
}
在上述代码中,我们开始一个事务,执行两个插入操作,只有当 setTransactionSuccessful()
被调用后,事务中的操作才会真正提交到数据库。如果在事务执行过程中出现异常,没有调用 setTransactionSuccessful()
,事务会自动回滚,之前的插入操作不会对数据库产生影响。
SQLite 的数据类型
- 存储类:SQLite 使用动态类型系统,列没有固定的数据类型,存储在 SQLite 数据库中的值的数据类型取决于值本身,而不是它所存储的列。SQLite 有五种存储类:
- NULL:表示空值。
- INTEGER:带符号的整数,根据值的大小存储为 1、2、3、4、6 或 8 字节。
- REAL:浮点值,存储为 8 字节的 IEEE 浮点数。
- TEXT:文本字符串,使用数据库编码(UTF - 8、UTF - 16BE 或 UTF - 16LE)存储。
- BLOB:二进制大对象,完全根据输入存储数据。
- 亲和类型:虽然 SQLite 是动态类型,但每个列仍然有一个亲和类型,它决定了在插入数据时如何将值转换为合适的数据类型。例如,声明为
INTEGER
亲和类型的列,在插入数据时,如果值是文本且可以转换为整数,会自动转换为INTEGER
类型存储。常见的亲和类型有TEXT
、NUMERIC
、INTEGER
、REAL
和NONE
。例如,创建表时定义age INTEGER
,这表明age
列具有INTEGER
亲和类型。
索引的使用
- 索引的作用:索引是一种特殊的数据结构,它可以加快数据库查询的速度。就像书籍的目录一样,通过索引可以快速定位到需要的数据,而不必全表扫描。在 SQLite 中,合理使用索引可以显著提升查询性能。
- 创建索引:
- 创建普通索引:
SQLiteDatabase db = sqliteOpenHelper.getWritableDatabase();
String createIndexSql = "CREATE INDEX idx_name ON users (name)";
db.execSQL(createIndexSql);
db.close();
上述代码在 users
表的 name
列上创建了一个名为 idx_name
的普通索引。当对 name
列进行查询时,数据库可以利用这个索引快速定位数据,从而提高查询效率。
- 创建唯一索引:
SQLiteDatabase db = sqliteOpenHelper.getWritableDatabase();
String createUniqueIndexSql = "CREATE UNIQUE INDEX idx_unique_email ON users (email)";
db.execSQL(createUniqueIndexSql);
db.close();
这里在 users
表的 email
列上创建了一个唯一索引 idx_unique_email
,这不仅可以提高查询效率,还能保证 email
列的值在表中是唯一的。
3. 注意事项:虽然索引可以提高查询性能,但过多的索引会增加数据库的存储空间,并且在插入、更新和删除数据时会因为需要维护索引而降低操作速度。所以在创建索引时,需要根据实际的查询需求谨慎考虑。
数据库性能优化
- 避免频繁打开和关闭数据库:每次打开和关闭数据库都有一定的性能开销,特别是在频繁进行数据库操作的场景下。可以将数据库实例保持在一个单例类中,在应用需要使用数据库时直接获取实例,而不是每次都创建和销毁。例如:
public class DatabaseManager {
private static DatabaseManager instance;
private SQLiteDatabase db;
private DatabaseManager(Context context) {
SQLiteOpenHelper sqliteOpenHelper = new MyDatabaseHelper(context);
db = sqliteOpenHelper.getWritableDatabase();
}
public static DatabaseManager getInstance(Context context) {
if (instance == null) {
instance = new DatabaseManager(context);
}
return instance;
}
public SQLiteDatabase getDatabase() {
return db;
}
}
在应用中通过 DatabaseManager.getInstance(context).getDatabase()
获取数据库实例,这样可以减少数据库打开和关闭的次数。
2. 批量操作:尽量将多个插入、更新或删除操作合并为一个事务进行处理,而不是逐个执行。例如,在插入大量数据时,如果逐个插入,每次插入都会有一定的性能开销,而将这些插入操作放在一个事务中,可以大大提高插入效率。
3. 合理使用查询语句:避免使用 SELECT *
进行全表查询,只查询需要的列可以减少数据传输和处理的开销。同时,尽量使用索引来优化查询,通过分析查询语句,确定哪些列需要创建索引。
4. 定期清理数据库:对于一些不再需要的数据,及时进行删除操作,避免数据库文件过大,影响性能。可以根据应用的业务逻辑,设置定期清理的机制,例如在应用启动时或者特定的时间间隔检查并清理过期数据。
多线程与 SQLite
- 线程安全问题:SQLite 本身是线程安全的,但在 Android 应用中使用时,需要注意一些线程相关的问题。默认情况下,SQLiteDatabase 实例不是线程安全的,不能在多个线程中同时使用同一个 SQLiteDatabase 实例进行读写操作,否则可能会导致数据损坏或程序崩溃。
- 解决方案:
- 使用 SQLiteOpenHelper 的单例模式:如前面提到的
DatabaseManager
类,在单例类中管理 SQLiteDatabase 实例,确保在同一个线程中使用该实例。如果需要在不同线程中操作数据库,可以为每个线程创建独立的 SQLiteOpenHelper 实例和对应的 SQLiteDatabase 实例。 - 使用 SQLiteDatabase 的同步方法:SQLiteDatabase 提供了一些同步方法,如
beginTransaction()
、setTransactionSuccessful()
和endTransaction()
,可以在多线程环境下保证数据的一致性。例如,在多个线程同时进行插入操作时,可以在每个线程的插入操作前后使用事务相关方法,确保每个线程的操作是原子性的,不会相互干扰。
- 使用 SQLiteOpenHelper 的单例模式:如前面提到的
常见错误及解决方法
- SQLiteException: no such table:这个错误通常表示在执行 SQL 语句时,指定的表不存在。可能的原因是数据库还没有创建成功,或者在数据库升级过程中表结构发生了变化但没有正确处理。解决方法是检查
SQLiteOpenHelper
的onCreate
和onUpgrade
方法,确保表的创建和升级逻辑正确。同时,可以在应用启动时添加一些日志输出,检查数据库是否正常创建。 - SQLiteException: near "(": syntax error:这种错误提示表明 SQL 语句存在语法错误,通常是括号不匹配、关键字拼写错误等原因导致。仔细检查 SQL 语句的语法,特别是在使用复杂的查询、插入或更新语句时,注意括号的位置、逗号的使用以及关键字的正确性。可以将 SQL 语句在 SQLite 命令行工具中进行测试,以快速定位语法错误。
- SQLiteConstraintException: UNIQUE constraint failed:当插入或更新数据时违反了唯一约束条件,就会抛出这个异常。例如,在创建了唯一索引的列上插入重复值。解决方法是在插入或更新数据之前,先进行查询操作,检查是否存在冲突的数据,或者在捕获到这个异常时,提示用户修改数据以满足唯一约束条件。
与其他数据存储方式的比较
- SharedPreferences:SharedPreferences 主要用于存储简单的键值对数据,如应用的配置信息、用户设置等。与 SQLite 相比,它的优点是使用简单,适合存储少量的简单数据。但它不适合存储大量复杂的数据,并且不支持数据查询等操作。例如,存储用户的登录状态、主题设置等可以使用 SharedPreferences,而存储用户的详细信息列表则适合使用 SQLite。
- 文件存储:文件存储可以用于存储各种类型的数据,如文本文件、二进制文件等。它适用于存储一些非结构化的数据,如图片、音频、视频等。与 SQLite 相比,文件存储在处理结构化数据和数据查询方面比较薄弱,而 SQLite 更擅长处理结构化数据,提供了丰富的查询功能和数据一致性保证。例如,存储用户的日志文件可以使用文件存储,而存储用户的订单信息等结构化数据则应使用 SQLite。
在 Android 开发中,根据应用的需求合理选择数据存储方式,可以提高应用的性能和用户体验。如果需要处理大量结构化数据,并进行复杂的查询和数据操作,SQLite 是一个非常好的选择。但在一些简单场景下,SharedPreferences 或文件存储可能更合适。开发人员需要根据实际情况权衡利弊,选择最适合的存储方案。
通过以上对 Android SQLite 开发的各个方面的总结和注意事项的阐述,希望能帮助开发人员在使用 SQLite 进行 Android 应用开发时更加得心应手,编写出高效、稳定的数据存储和管理模块。在实际开发中,还需要不断积累经验,根据具体的业务需求和应用场景,进一步优化数据库设计和操作,以提升应用的整体性能。