在安卓应用开发中,数据持久化是不可或缺的一环,无论是用户信息、设置偏好还是缓存内容,都需要一个可靠的存储方案,SQLite 作为安卓内置的轻量级关系型数据库,是处理结构化数据的首选,直接操作原生 SQLite API 相对繁琐且容易出错,掌握如何高效、安全地编写数据库查询代码至关重要,本文将详细介绍两种主流的实现方式:Google 推荐的 Room 持久化库和传统的 SQLiteOpenHelper 方法,并提供清晰的代码示例与最佳实践。
使用Room持久化库(现代化推荐方案)
Room 是一个在 SQLite 上提供的抽象层,它极大地简化了数据库操作,并利用注解在编译时验证 SQL 语句,从而减少了运行时错误,Room 主要包含三个核心组件:
- Entity:代表数据库中的表。
- DAO (Data Access Object):包含访问数据库的方法。
- Database:持有数据库并作为应用持久化数据底层连接的主要访问点。
查询逻辑主要在 DAO 接口中定义,通过使用 @Query
注解,我们可以直接编写 SQL 语句,Room 会自动处理其余工作。
定义实体 (Entity)
我们需要一个数据模型类来映射数据库表。
@Entity(tableName = "users") public class User { @PrimaryKey(autoGenerate = true) private int id; @ColumnInfo(name = "user_name") private String name; private int age; // Getters and Setters... }
定义数据访问对象 (DAO)
这是编写查询代码的核心,我们创建一个接口,并用 @Dao
注解。
@Dao public interface UserDao { // 查询所有用户,返回一个列表 @Query("SELECT * FROM users") List<User> getAllUsers(); // 根据ID查询特定用户,返回单个对象 @Query("SELECT * FROM users WHERE id = :userId") User findUserById(int userId); // 根据年龄范围查询用户,使用多个参数 @Query("SELECT * FROM users WHERE age BETWEEN :minAge AND :maxAge") List<User> findUsersByAge(int minAge, int maxAge); // 查询所有用户并按名字排序 @Query("SELECT * FROM users ORDER BY user_name ASC") List<User> getAllUsersSortedByName(); // 模糊查询,查找名字包含特定字符串的用户 @Query("SELECT * FROM users WHERE user_name LIKE '%' || :keyword || '%'") List<User> searchUsersByName(String keyword); // 使用LiveData进行响应式查询,当数据变化时UI会自动更新 @Query("SELECT * FROM users") LiveData<List<User>> observeAllUsers(); }
在 @Query
注解中,userId
、minAge
等是命名参数,它们会自动映射到方法签名中同名的参数上,这种方式不仅代码可读性高,Room 会在编译时检查这些 SQL 语句的正确性,避免了运行时才发现 SQLSyntaxErrorException
的尴尬。
直接使用SQLite API(传统方案)
在 Room 出现之前,开发者通常通过继承 SQLiteOpenHelper
类来管理数据库的创建和版本更新,查询操作则通过 SQLiteDatabase
对象的 query()
或 rawQuery()
方法完成。
创建 SQLiteOpenHelper 子类
public class MyDbHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "my_app.db"; private static final int DATABASE_VERSION = 1; public MyDbHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { // 创建表的SQL语句 String CREATE_USERS_TABLE = "CREATE TABLE users (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "user_name TEXT," + "age INTEGER)"; db.execSQL(CREATE_USERS_TABLE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 升级数据库的逻辑 db.execSQL("DROP TABLE IF EXISTS users"); onCreate(db); } }
执行查询操作
查询后,你需要手动处理一个 Cursor
对象来遍历结果集。
public List<User> getUsersOldWay(Context context) { List<User> userList = new ArrayList<>(); MyDbHelper dbHelper = new MyDbHelper(context); SQLiteDatabase db = dbHelper.getReadableDatabase(); // 定义要查询的列 String[] columns = {"id", "user_name", "age"}; // 定义查询条件 String selection = "age > ?"; String[] selectionArgs = {"18"}; // 查询年龄大于18的用户 // 定义排序方式 String sortOrder = "user_name DESC"; Cursor cursor = db.query( "users", // 表名 columns, // 要查询的列 selection, // WHERE子句 selectionArgs, // WHERE子句的参数 null, // GROUP BY null, // HAVING sortOrder // ORDER BY ); // 遍历Cursor if (cursor.moveToFirst()) { do { int id = cursor.getInt(cursor.getColumnIndexOrThrow("id")); String name = cursor.getString(cursor.getColumnIndexOrThrow("user_name")); int age = cursor.getInt(cursor.getColumnIndexOrThrow("age")); userList.add(new User(id, name, age)); } while (cursor.moveToNext()); } cursor.close(); db.close(); return userList; }
使用 rawQuery()
可以直接执行完整的 SQL 语句,但同样需要手动处理 Cursor
。
Cursor cursor = db.rawQuery("SELECT * FROM users WHERE user_name LIKE ?", new String[]{"%张%"}); // ... 处理Cursor的逻辑同上 ...
方案对比与最佳实践
为了更直观地理解两种方案的差异,下表进行了详细对比:
特性 | Room 持久化库 | 原生 SQLite API |
---|---|---|
易用性 | 高,通过注解极大简化代码 | 低,需要编写大量模板代码 |
编译时检查 | 支持,SQL语句在编译时被验证 | 不支持,运行时才能发现SQL错误 |
类型安全 | 强,返回值直接映射到Java/Kotlin对象 | 弱,需手动从Cursor中获取数据,易出错 |
线程安全 | 自动处理,支持主线程安全查询 | 需开发者手动管理,容易在主线程操作 |
响应式编程 | 原生支持LiveData和Flow | 不支持,需手动实现观察者模式 |
代码维护性 | 高,逻辑清晰,结构化 | 低,SQL和Java代码混杂,难以维护 |
最佳实践小编总结:
- 优先使用 Room:对于所有新项目,强烈推荐使用 Room,它提供的抽象层和编译时检查能显著提升开发效率和应用的稳定性。
- 参数化查询:无论使用哪种方式,都应使用参数化查询(如
userId
或 占位符)来传递变量,这是防止 SQL 注入攻击的关键。 - 善用响应式组件:结合 Room 的
LiveData
或Flow
,可以轻松构建数据驱动的UI,当数据库数据变化时,界面会自动刷新,无需手动控制。
相关问答 (FAQs)
为什么说 Room 比直接使用 SQLite 更安全?
解答: Room 的安全性主要体现在两个方面,首先是 编译时验证,Room 会在应用编译时检查 @Query
注解中的 SQL 语句是否存在语法错误、表名或列名是否匹配,这能将大量运行时才暴露的数据库错误提前到开发阶段解决,其次是 类型安全,Room 会自动将查询结果(Cursor)映射到你定义的 Java/Kotlin 对象(Entity)上,避免了手动从 Cursor 获取数据时可能出现的列名拼写错误或类型不匹配问题,从而减少了 IllegalArgumentException
和 IllegalStateException
的风险。
在使用原生 SQLite 的 rawQuery()
时,如何有效防止 SQL 注入?
解答: 防止 SQL 注入的核心原则是 永远不要直接将用户输入或不可信的字符串拼接到 SQL 语句中,正确的做法是使用参数化查询,在 rawQuery()
方法中,使用 作为占位符来代替需要插入变量的位置,然后将这些变量作为 selectionArgs
字符串数组传入。db.rawQuery("SELECT * FROM users WHERE name = ?", new String[]{userName})
,这样,SQLite 数据库驱动会负责将参数安全地绑定到 SQL 语句中,即使用户输入 userName
包含恶意的 SQL 代码,它也只会被当作一个普通的字符串来处理,而不会被当作 SQL 命令执行,从而杜绝了 SQL 注入的风险。
【版权声明】:本站所有内容均来自网络,若无意侵犯到您的权利,请及时与我们联系将尽快删除相关内容!
发表回复