在安卓应用开发中,数据持久化是不可或缺的一环,无论是用户信息、设置偏好还是缓存内容,都需要一个可靠的存储方案,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 注入的风险。