查看原文
其他

Room数据库使用一些坑

未扬帆的小船 鸿洋
2024-09-26

本文作者


作者:未扬帆的小船

链接:

https://juejin.cn/post/7386844718141374473

本文由作者授权发布。


1问题1:Room怎么查询大量数据的表而不导致OOM?


1. 分页查询(Paging Library)


使用Android Paging Library可以有效地分批加载数据,而不是一次性加载所有数据。
java
// 在Dao中使用PagingSource
@Query("SELECT * FROM your_table")
PagingSource<Integer, YourEntity> getAllData();
在ViewModel中:
java
public LiveData<PagingData<YourEntity>> getPagedData() {
    return new Pager(
        new PagingConfig(
            pageSize = 20, // 每页加载的数据量
            enablePlaceholders = false
        )
    ) {
        @Override
        public PagingSource create() {
            return yourDao.getAllData();
        }
    }.liveData;
}

2. 使用流(Flow)或 LiveData

使用Flow或LiveData,可以逐步加载数据,减少内存占用。
java
@Query("SELECT * FROM your_table")
LiveData<List<YourEntity>> getAllData();

3. 限制查询的数据量

如果你不需要所有数据,可以限制查询的数据量:
java
@Query("SELECT * FROM your_table LIMIT :limit OFFSET :offset")
List<YourEntity> getLimitedData(int limit, int offset);

4. 适当地选择字段

只查询必要的字段,避免加载不需要的数据。
java
@Query("SELECT column1, column2 FROM your_table")
List<YourEntity> getSelectedColumns();

5. 使用SQLite的内存管理

配置SQLite数据库的内存使用:
java
SQLiteDatabase db = roomDatabase.getOpenHelper().getWritableDatabase();
db.execSQL("PRAGMA cache_size=10000"); // 设置缓存大小
db.execSQL("PRAGMA temp_store=MEMORY"); // 使用内存中的临时存储


6. 避免在主线程查询

确保所有数据库操作在子线程中执行,避免阻塞主线程和内存溢出:
java
Executors.newSingleThreadExecutor().execute(() -> {
    List<YourEntity> data = yourDao.getAllData();
    // 处理数据
});
7. 使用Cursor
如果数据量特别大,考虑使用Cursor进行逐行处理,而不是一次性加载所有数据:
java
@Query("SELECT * FROM your_table")
Cursor getAllDataCursor();

在使用Cursor时,要注意及时关闭Cursor以释放资源。

8. 数据库设计优化

检查数据库设计,确保索引、外键等设计合理,以提高查询效率。
通过以上方法,可以有效减少OOM错误的发生,提高应用的稳定性。如果具体问题有更多细节,欢迎提供更多信息以便进一步优化。
使用Cursor进行数据查询时,确保及时释放资源非常重要。以下是使用Cursor时释放资源的最佳实践:

使用Cursor怎么保证会释放资源 怎么释放?

2问题2:使用Cursor进行数据查询时,注意事项

使用Cursor进行数据查询时,确保及时释放资源非常重要。以下是使用Cursor时释放资源的最佳实践:

1. 手动管理Cursor

手动管理Cursor时,确保在使用完Cursor后关闭它。
java
Cursor cursor = null;
try {
    cursor = yourDao.getAllDataCursor();
    if (cursor != null && cursor.moveToFirst()) {
        do {
            // 处理每一行数据
        } while (cursor.moveToNext());
    }
} finally {
    if (cursor != null) {
        cursor.close();
    }
}

2. 使用try-with-resources

从API 19(Android 4.4)开始,try-with-resources语法可以自动管理资源的关闭。
java
try (Cursor cursor = yourDao.getAllDataCursor()) {
    if (cursor != null && cursor.moveToFirst()) {
        do {
            // 处理每一行数据
        } while (cursor.moveToNext());
    }
} // try-with-resources会自动调用cursor.close()

3. 在ViewModel或Repository中管理Cursor

如果在ViewModel或Repository中使用Cursor,确保在ViewModel销毁时释放资源。
java
public class MyViewModel extends ViewModel {
    private Cursor cursor;

    public Cursor getCursor() {
        if (cursor == null) {
            cursor = yourDao.getAllDataCursor();
        }
        return cursor;
    }

    @Override
    protected void onCleared() {
        super.onCleared();
        if (cursor != null && !cursor.isClosed()) {
            cursor.close();
        }
    }
}

4. 在AsyncTask中使用Cursor

如果在AsyncTask中使用Cursor,确保在后台线程中处理数据,并在任务完成时关闭Cursor。
java
private class QueryTask extends AsyncTask<Void, Void, List<YourEntity>> {
    @Override
    protected List<YourEntity> doInBackground(Void... voids) {
        Cursor cursor = null;
        List<YourEntity> result = new ArrayList<>();
        try {
            cursor = yourDao.getAllDataCursor();
            if (cursor != null && cursor.moveToFirst()) {
                do {
                    // 读取数据并添加到结果集
                } while (cursor.moveToNext());
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return result;
    }

    @Override
    protected void onPostExecute(List<YourEntity> result) {
        // 处理结果
    }
}

5. 在ContentProvider中使用Cursor

如果使用ContentProvider,可以在onLoadFinished方法中关闭Cursor。
java
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    if (data != null && data.moveToFirst()) {
        do {
            // 处理每一行数据
        } while (data.moveToNext());
    }
    data.close(); // 确保在使用完Cursor后关闭它
}

6. 使用Room的CursorWrapper

Room 2.2.0及以上版本支持返回CursorWrapper,可以在操作完成后自动关闭Cursor。
java
@Query("SELECT * FROM your_table")
CursorWrapper getAllDataCursor();
使用时,可以像使用普通Cursor一样使用CursorWrapper,但它会在操作完成后自动关闭Cursor。

通过这些方法,可以确保Cursor在使用完后被及时关闭,避免内存泄漏和OOM问题。

3问题3:@Transaction的作用是什么


在Room数据库中,@Transaction注解用于确保多个数据库操作在一个原子操作中执行。这意味着所有操作要么全部成功,要么全部失败。如果在事务中任何一个操作失败,整个事务都会回滚,确保数据库状态的一致性。
以下是一些使用@Transaction的示例和其作用:

1. 确保多个操作的原子性

假设你有一个复杂的操作需要插入多个表中的数据,或者更新和删除某些表中的数据,你可以使用@Transaction来确保这些操作在一个事务中执行。
java
@Dao
public interface YourDao {
    
    @Insert
    void insertUser(User user);
    
    @Insert
    void insertOrder(Order order);
    
    @Update
    void updateUser(User user);

    @Transaction
    default void insertUserAndOrder(User user, Order order) {
        insertUser(user);
        insertOrder(order);
    }

    @Transaction
    default void updateUserAndInsertOrder(User user, Order order) {
        updateUser(user);
        insertOrder(order);
    }
}
在上面的示例中,insertUserAndOrderupdateUserAndInsertOrder方法被@Transaction注解标记,这意味着它们的操作将在一个事务中执行。如果插入用户或订单时发生错误,整个事务将回滚,确保数据库不会进入不一致的状态。

2. 在查询操作中使用@Transaction

你可以在返回复杂对象(包括嵌套对象)的方法上使用@Transaction。这在需要执行多个查询以获取完整数据时非常有用。例如,如果你有一个用户和他们的订单,你想在一次调用中检索这些信息:
java
@Dao
public interface YourDao {
    
    @Query("SELECT * FROM User WHERE userId = :userId")
    User getUserById(int userId);
    
    @Query("SELECT * FROM Order WHERE userId = :userId")
    List<Order> getOrdersByUserId(int userId);

    @Transaction
    @Query("SELECT * FROM User WHERE userId = :userId")
    UserWithOrders getUserWithOrders(int userId);
}

public class UserWithOrders {
    @Embedded
    public User user;

    @Relation(
        parentColumn = "userId",
        entityColumn = "userId"
    )
    public List<Order> orders;
}
在这个示例中,getUserWithOrders方法被@Transaction注解标记,Room会在一个事务中执行getUserByIdgetOrdersByUserId查询,确保数据一致性。

3. 回滚机制

如果在事务中任何一步操作失败,整个事务会回滚。例如:
java
@Dao
public interface YourDao {
    
    @Insert
    void insertUser(User user);
    
    @Insert
    void insertOrder(Order order);
    
    @Transaction
    default void insertUserAndOrder(User user, Order order) {
        insertUser(user);
        if (someConditionFails()) {
            throw new RuntimeException("Transaction failed");
        }
        insertOrder(order);
    }
}
如果someConditionFails()返回true,insertOrder不会被执行,insertUser的插入也会被回滚。

使用@Transaction确保了数据库操作的一致性和完整性,尤其在涉及多个表的复杂操作时非常有用。

4问题4:Room怎么优雅删除表的数据而不会OOM


在使用Room框架进行数据库操作时,确保删除表数据时不会发生OOM(OutOfMemoryError)是很重要的。OOM通常发生在操作大量数据时,特别是在内存管理不当或者没有适当分页处理的情况下。以下是一些优雅删除表数据的方法:

方法1:使用@Query执行DELETE语句

最简单的方法是使用@Query注解在DAO接口中执行DELETE语句,例如:
java
@Dao
public interface UserDao {
    @Query("DELETE FROM users")
    void deleteAllUsers();
}
这里的deleteAllUsers()方法会删除users表中的所有数据。如果你的表非常大,可以考虑使用LIMIT来分批删除:
java
@Dao
public interface UserDao {
    @Query("DELETE FROM users LIMIT :batchSize")
    void deleteUsersInBatch(int batchSize);
}

方法2:使用Room的事务(@Transaction)

如果你需要删除大量数据,并希望确保操作的原子性和性能,可以使用Room的@Transaction注解来执行删除操作。事务可以确保一组数据库操作要么全部完成,要么全部失败回滚,从而避免数据库处于不一致的状态。
java
@Dao
public interface UserDao {
    @Transaction
    @Query("DELETE FROM users")
    void deleteAllUsers();
}

方法3:使用WorkManager进行后台操作

对于需要长时间运行或者大量数据的删除操作,推荐将其放在后台进行,以避免影响主线程和用户体验。你可以使用Android Jetpack的WorkManager来调度后台任务,确保任务在合适的时机执行。

方法4:适当的异常处理和内存管理

在进行任何数据库操作时,都应该注意异常处理,尤其是处理OOM异常的情况。确保在处理大数据量时,使用适当的分页查询和批处理删除,以降低内存使用和提高性能。

示例代码

下面是一个简单的示例,演示如何在Room中执行批量删除操作:
java
@Dao
public interface UserDao {
    @Query("DELETE FROM users")
    void deleteAllUsers();

    @Transaction
    @Query("SELECT * FROM users")
    List<User> getAllUsers();

    @Transaction
    void deleteUsersInBatch(int batchSize) {
        List<User> users = getAllUsers();
        for (int i = 0; i < users.size(); i += batchSize) {
            int endIndex = Math.min(i + batchSize, users.size());
            List<User> batch = users.subList(i, endIndex);
            deleteUsers(batch);
        }
    }

    @Delete
    void deleteUsers(List<User> users);
}
在这个例子中,deleteUsersInBatch方法将先获取所有用户,然后根据指定的batchSize批量删除用户数据。

总结

通过使用适当的查询语句、事务、后台任务和内存管理技术,你可以在使用Room框架时优雅地处理大量数据的删除操作,避免OOM错误的发生,并提高应用程序的性能和稳定性。

5问题5:调整数据库的缓存大小


在Android中,特别是使用Room数据库时,无法直接调整数据库的缓存大小,因为Room并没有提供直接控制缓存大小的API。Room是建立在SQLite之上的抽象层,而SQLite的缓存大小通常由底层SQLite库和系统配置决定。

SQLite 缓存的工作原理:

SQLite在内部使用页面缓存来提高读取和写入数据的性能。这些页面缓存的大小由SQLite库自动管理,并且可以通过SQLite的PRAGMA语句进行配置,但通常情况下,这些参数是不可在应用层进行动态调整的。

Room 中的缓存:

在Room中,缓存通常指的是在内存中保留的已查询数据的副本,这些数据通常由LiveData或者其他数据持久性技术管理。Room提供了诸如LiveData、RxJava等技术来管理数据的生命周期和缓存,但这些缓存的大小和策略是由这些框架和组件自行决定的,开发者无法直接干预。

如何优化数据访问和内存管理:

虽然无法直接调整Room或SQLite的缓存大小,但可以通过以下方式来优化数据访问和内存管理:
  1. 合理使用分页查询:对于大量数据,使用分页技术来限制一次查询返回的数据量,以减少内存消耗。
  2. 优化查询语句:确保查询语句简洁高效,只检索必要的数据列,避免一次性检索过多的数据。
  3. 使用事务:对于大量的插入、更新或删除操作,使用事务来批量处理,减少频繁的数据库操作对性能的影响。
  4. 避免内存泄漏:确保在不需要时及时释放数据库访问对象和结果集,避免不必要的内存持有。
  5. 使用合适的数据库管理策略:根据应用程序的需求和性能需求,选择合适的数据库管理和操作策略,如Room、SQLite直接操作或者其他ORM框架。

调整SQLite缓存的方式:

如果你确定需要调整SQLite的缓存大小,可以通过SQLite的PRAGMA语句来调整。但这通常需要在SQLite库编译时进行配置,并且需要在底层C代码中进行设置,不适用于在Android应用程序中动态调整。

6问题6:SELECT column1, column2 FROM your_table 返回的数据需要注意什么


当你使用SELECT column1, column2 FROM your_table从数据库中查询数据时,需要注意以下几点,以确保数据处理的正确性和效率:

1. 列名的一致性

确保查询中的列名(column1, column2)与数据库表中的实际列名一致。如果列名拼写错误或不存在,将导致异常。

2. 防止SQL注入

如果查询包含用户输入,必须使用参数化查询或预编译语句来防止SQL注入攻击。不要将用户输入直接拼接到SQL查询中。

3. 资源管理

使用Cursor对象读取数据时,要确保在使用完毕后关闭它,以防资源泄漏。

4. 处理空值

确保正确处理可能包含空值的列。使用Cursor的isNull()方法检查列值是否为空。

5. 索引优化

对于经常查询的列,确保它们在数据库中有适当的索引,以提高查询性能。

6. 查询性能

尽量避免在单个查询中返回大量数据。可以使用LIMIT子句限制返回的数据行数,或根据需要进行分页查询。

7. 并发控制

在多线程环境中操作数据库时,确保有适当的并发控制,如使用Room数据库的事务处理,以防止数据竞争问题。

8. 异常处理

确保在查询和处理数据时捕获并处理可能的异常,如SQLException。

示例代码

下面是一个完整的Kotlin示例代码,演示如何进行一个安全、有效的数据库查询,并正确处理返回的数据:
kotlin
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.room.Room

class MainActivity : AppCompatActivity() {
    private lateinit var db: SQLiteDatabase

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 初始化数据库
        db = SQLiteDatabase.openOrCreateDatabase("database-name", null)

        // 安全查询示例
        val cursor: Cursor? = db.rawQuery(
            "SELECT column1, column2 FROM your_table WHERE column1 > ?",
            arrayOf("value")
        )

        cursor?.use {
            if (it.moveToFirst()) {
                val column1Index = it.getColumnIndex("column1")
                val column2Index = it.getColumnIndex("column2")

                do {
                    // 确保列索引有效
                    val column1 = if (column1Index != -1) it.getString(column1Index) else null
                    val column2 = if (column2Index != -1) it.getString(column2Index) else null

                    // 处理数据
                    println("Column1: $column1, Column2: $column2")
                } while (it.moveToNext())
            }
        }
    }
}

关键点解释

1. 参数化查询:
kotlin
val cursor: Cursor? = db.rawQuery(
    "SELECT column1, column2 FROM your_table WHERE column1 > ?",
    arrayOf("value")
)

使用参数化查询来防止SQL注入。

2. 资源管理:
kotlin
cursor?.use {
    if (it.moveToFirst()) {
        val column1Index = it.getColumnIndex("column1")
        val column2Index = it.getColumnIndex("column2")

        do {
            // 确保列索引有效
            val column1 = if (column1Index != -1) it.getString(column1Index) else null
            val column2 = if (column2Index != -1) it.getString(column2Index) else null

            // 处理数据
            println("Column1: $column1, Column2: $column2")
        } while (it.moveToNext())
    }
}

使用cursor?.use自动管理资源,确保在使用完毕后关闭Cursor。

3. 处理空值:
kotlin
val column1 = if (column1Index != -1) it.getString(column1Index) else null
val column2 = if (column2Index != -1) it.getString(column2Index) else null

检查列索引是否有效,并处理可能的空值。

通过以上方法和注意事项,可以确保在进行数据库查询时既能有效获取数据,又能防止常见问题,如SQL注入、资源泄漏和空值处理问题。

7问题7:翻页查询传入offset要考虑数据库越界的问题

传入OFFSET参数时需要注意防止越界问题,以确保查询不会尝试跳过超过数据库中实际存在的记录数。防止越界可以通过以下几种方法来实现:
  1. 获取总记录数:在进行分页查询之前,先查询数据库中的总记录数,根据总记录数来判断OFFSET是否超出范围。
  2. 检查页面索引:在应用逻辑中检查当前页面索引和总页数是否有效,防止用户请求超出范围的页面。

示例代码

以下是一个实现防止OFFSET越界的完整示例:

定义实体和DAO

kotlin
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.Dao
import androidx.room.Query

@Entity(tableName = "users")
data class User(
    @PrimaryKey val id: Int,
    val name: String
)

@Dao
interface UserDao {
    @Query("SELECT * FROM users LIMIT :limit OFFSET :offset")
    suspend fun getUsersWithLimitOffset(limit: Int, offset: Int): List<User>

    @Query("SELECT COUNT(*) FROM users")
    suspend fun getUserCount(): Int
}

使用DAO进行分页查询并检查越界

kotlin
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import androidx.room.Room
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {

    private lateinit var db: AppDatabase

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 初始化数据库
        db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java, "database-name"
        ).build()

        // 查询第一页数据(假设每页20条数据)
        val pageSize = 20
        val pageIndex = 0

        lifecycleScope.launch {
            val userCount = db.userDao().getUserCount()
            val totalPages = (userCount + pageSize - 1) / pageSize  // 计算总页数
            if (pageIndex < totalPages) {
                val offset = pageIndex * pageSize
                val users = db.userDao().getUsersWithLimitOffset(pageSize, offset)
                users.forEach {
                    println("User: ${it.name}")
                }
            } else {
                println("Requested page index $pageIndex is out of range.")
            }
        }

        // 查询第二页数据
        lifecycleScope.launch {
            val userCount = db.userDao().getUserCount()
            val totalPages = (userCount + pageSize - 1) / pageSize  // 计算总页数
            val nextPageIndex = 1
            if (nextPageIndex < totalPages) {
                val offset = nextPageIndex * pageSize
                val users = db.userDao().getUsersWithLimitOffset(pageSize, offset)
                users.forEach {
                    println("User: ${it.name}")
                }
            } else {
                println("Requested page index $nextPageIndex is out of range.")
            }
        }
    }
}

关键点解释

1. 获取总记录数:
kotlin
@Query("SELECT COUNT(*) FROM users")
suspend fun getUserCount(): Int

定义一个查询方法来获取数据库中的总记录数。

2. 计算总页数:
kotlin
val totalPages = (userCount + pageSize - 1) / pageSize

根据总记录数和每页的记录数计算总页数。

3. 检查页面索引:
kotlin
if (pageIndex < totalPages) {
    val offset = pageIndex * pageSize
    val users = db.userDao().getUsersWithLimitOffset(pageSize, offset)
    // 处理查询结果
else {
    println("Requested page index $pageIndex is out of range.")
}

在进行分页查询前检查当前页面索引是否在有效范围内,防止OFFSET越界。

通过这种方式,可以有效防止分页查询中的OFFSET越界问题,确保查询不会尝试跳过超过数据库中实际存在的记录数。

8问题8:删除表中所有数据:clearUsersTable 跟DELETE FROM users的区别


clearUsersTable() 和 DELETE FROM users 在功能上是相同的,都用于删除表中的所有数据,但在实现细节和使用场景上可能会有所不同。


clearUsersTable()

这个是我们在 DAO 中定义的方法,通过 Room 的注解来执行 SQL 命令:
kotlin
@Dao
interface UserDao {
    @Query("DELETE FROM users")
    suspend fun clearUsersTable()
}

DELETE FROM users

这是直接在 SQL 中执行的命令,用于删除 users 表中的所有记录:
sql
DELETE FROM users;

详细解释

  1. 功能
  • 两者都是用来删除表中的所有记录。
  • Room 的好处:
    • 通过 Room 的 DAO 方法(如 clearUsersTable()),可以确保删除操作符合 Room 的生命周期和线程管理,便于在 Kotlin 或 Java 代码中进行调用。
    • 使用 Room,可以更好地利用 Room 的特性,比如事务管理、类型安全和异步操作。
  • 性能
    • 两者在性能上基本没有区别,都是执行相同的 SQL 命令。
    • 如果表非常大且数据量非常多,删除操作可能仍然会导致性能问题,但这不取决于使用哪种方式来执行删除。

    示例代码

    定义 DAO 接口

    kotlin
    import androidx.room.Dao
    import androidx.room.Query

    @Dao
    interface UserDao {
        @Query("DELETE FROM users")
        suspend fun clearUsersTable()
    }

    使用 DAO 清空表数据

    kotlin
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import androidx.lifecycle.lifecycleScope
    import androidx.room.Room
    import kotlinx.coroutines.launch

    class MainActivity : AppCompatActivity() {

        private lateinit var db: AppDatabase

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)

            // 初始化数据库
            db = Room.databaseBuilder(
                applicationContext,
                AppDatabase::class.java, "database-name"
            ).build()

            // 清空表数据
            lifecycleScope.launch {
                db.userDao().clearUsersTable()
                println("Table cleared")
            }
        }
    }

    @Entity(tableName = "users")
    data class User(
        @PrimaryKey val id: Int,
        val name: String
    )

    @Database(entities = [User::class], version = 1)
    abstract class AppDatabase : RoomDatabase() {
        abstract fun userDao(): UserDao
    }


    注意事项

    1. 事务管理:
    • 虽然单个 DELETE FROM users 操作通常是原子的,但在复杂的业务逻辑中,可以将多个相关操作放在一个事务中,以确保数据一致性。
  • 异步操作:
    • 使用协程 (suspend) 确保删除操作在后台线程中进行,不阻塞主线程,适合在 Android 应用中使用。
  • 索引重置:
    • 如果需要重置主键自增序列,可以使用 TRUNCATE TABLE 而不是 DELETE,但 Room 不支持 TRUNCATE 语句。如果有这种需求,可能需要手动执行原生 SQL。

    示例代码(重置主键)

    如果你需要删除数据并重置主键自增序列,可以执行以下原生 SQL:
    kotlin
    import androidx.room.Dao
    import androidx.room.Query
    import androidx.room.RoomDatabase

    @Dao
    interface UserDao {
        @Query("DELETE FROM users")
        suspend fun clearUsersTable()

        @Query("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'users'")
        suspend fun resetPrimaryKey()
    }

    kotlin
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import androidx.lifecycle.lifecycleScope
    import androidx.room.Room
    import kotlinx.coroutines.launch

    class MainActivity : AppCompatActivity() {

        private lateinit var db: AppDatabase

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)

            // 初始化数据库
            db = Room.databaseBuilder(
                applicationContext,
                AppDatabase::class.java, "database-name"
            ).build()

            // 清空表数据并重置主键
            lifecycleScope.launch {
                db.userDao().clearUsersTable()
                db.userDao().resetPrimaryKey()
                println("Table cleared and primary key reset")
            }
        }
    }

    @Entity(tableName = "users")
    data class User(
        @PrimaryKey val id: Int,
        val name: String
    )

    @Database(entities = [User::class], version = 1)
    abstract class AppDatabase : RoomDatabase() {
        abstract fun userDao(): UserDao
    }

    通过这种方式,可以有效删除表中的数据并处理主键自增序列。

    9问题9:Room使用SQLCipher以及注意事项


    使用 SQLCipher 加密 Room 数据库后,数据库操作(增删改查)与使用普通 Room 数据库时的操作基本相同。关键在于初始化 Room 数据库时配置 SQLCipher,其余部分则与常规 Room 操作一致。

    依赖项

    首先,在你的 build.gradle 文件中添加所需的依赖项:
    groovy
    dependencies {
        implementation 'androidx.room:room-runtime:2.5.0'
        kapt 'androidx.room:room-compiler:2.5.0'
        implementation 'net.zetetic:android-database-sqlcipher:4.5.0'
        implementation 'androidx.sqlite:sqlite:2.2.0'
        implementation 'androidx.sqlite:sqlite-framework:2.2.0'
    }

    配置 SQLCipher 和 Room

    接下来,创建一个 SupportFactory 来使用 SQLCipher 创建加密的 Room 数据库:
    kotlin
    import androidx.room.Room
    import net.sqlcipher.database.SQLiteDatabase
    import net.sqlcipher.database.SupportFactory

    // 初始化 SQLCipher 库
    SQLiteDatabase.loadLibs(context)

    // 创建 SQLCipher SupportFactory
    val passphrase: ByteArray = SQLiteDatabase.getBytes("your_secure_password".toCharArray())
    val factory = SupportFactory(passphrase)

    // 构建 Room 数据库
    val db = Room.databaseBuilder(context, AppDatabase::class.java, "encrypted_database")
        .openHelperFactory(factory)
        .build()

    定义 Room 数据库

    接着,定义你的 Room 数据库和 DAO:
    kotlin
    import androidx.room.Database
    import androidx.room.RoomDatabase

    @Database(entities = [User::class], version = 1)
    abstract class AppDatabase : RoomDatabase() {
        abstract fun userDao(): UserDao
    }

    @Entity
    data class User(
        @PrimaryKey val id: Int,
        val name: String,
        val age: Int
    )

    @Dao
    interface UserDao {
        @Insert
        fun insert(user: User)

        @Query("SELECT * FROM User")
        fun getAll(): List<User>
    }

    使用加密的 Room 数据库

    现在,你可以像使用普通的 Room 数据库一样使用加密的 Room 数据库:
    kotlin
    // 插入数据
    val userDao = db.userDao()
    val user = User(id = 1, name = "Alice", age = 30)
    userDao.insert(user)

    // 查询数据
    val users = userDao.getAll()
    for (user in users) {
        println("User: id=${user.id}, name=${user.name}, age=${user.age}")
    }


    关键点总结

    1. 初始化加密数据库:通过 SQLiteDatabase.loadLibs(context) 初始化 SQLCipher,并使用 SupportFactory 创建加密数据库。
    2. 数据库操作:增删改查操作与普通 Room 数据库一致,不需要额外的加密解密处理,因为 SQLCipher 会自动处理这些。
    3. 密码管理:确保密码安全管理,因为它是数据库安全的核心。

    重要注意事项

    1. 密码管理:确保密码的安全存储和管理。密码的安全性直接关系到数据库的安全性。
    2. 数据库升级:在进行数据库版本升级时,确保兼容性,特别是在涉及到加密数据库时。
    3. 性能影响:使用加密数据库可能会有性能影响,尤其是在大数据量读写时,需要进行性能测试以确保满足应用需求。

    10问题10:Room 中,实体类的字段名称不能以 "is" 开头


    在 Room 中,实体类的字段名称不能以 "is" 开头是因为在生成的代码中,Room 会将这些字段视为布尔值,并生成 getter 和 setter 方法。为了避免这种情况,可以使用 @ColumnInfo 注解来指定数据库中的列名,从而绕过这个问题。

    示例

    假设我们有一个实体类 User,其中包含一个以 "is" 开头的字段 isActive。我们可以使用 @ColumnInfo 注解来更改数据库中的列名。
    kotlin
    import androidx.room.ColumnInfo
    import androidx.room.Entity
    import androidx.room.PrimaryKey

    @Entity
    data class User(
        @PrimaryKey val id: Int,
        val name: String,
        @ColumnInfo(name = "is_active") val isActive: Boolean
    )
    在这个例子中,isActive 字段在数据库中的列名将是 is_active,而不是默认生成的 isActive。这样可以避免 Room 对字段名的解析问题。

    完整示例

    以下是一个完整示例,包括实体类、DAO、数据库和使用代码。

    1. 定义实体类

    kotlin
    import androidx.room.ColumnInfo
    import androidx.room.Entity
    import androidx.room.PrimaryKey

    @Entity
    data class User(
        @PrimaryKey val id: Int,
        val name: String,
        @ColumnInfo(name = "is_active") val isActive: Boolean
    )

    2. 定义 DAO

    kotlin
    import androidx.room.*

    @Dao
    interface UserDao {
        @Insert
        fun insert(user: User)

        @Update
        fun update(user: User)

        @Delete
        fun delete(user: User)

        @Query("SELECT * FROM User")
        fun getAll(): List<User>

        @Query("SELECT * FROM User WHERE id = :id")
        fun getById(id: Int): User?
    }

    3. 定义数据库

    kotlin
    import androidx.room.Database
    import androidx.room.RoomDatabase

    @Database(entities = [User::class], version = 1)
    abstract class AppDatabase : RoomDatabase() {
        abstract fun userDao(): UserDao
    }

    4. 初始化数据库并进行增删改查操作

    kotlin
    import android.os.Bundle
    import androidx.appcompat.app.AppCompatActivity
    import androidx.room.Room

    class MainActivity : AppCompatActivity() {

        private lateinit var db: AppDatabase
        private lateinit var userDao: UserDao

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)

            // 初始化数据库
            db = Room.databaseBuilder(
                applicationContext,
                AppDatabase::class.java, "database-name"
            ).build()
            userDao = db.userDao()

            // 插入数据
            val user = User(id = 1, name = "Alice", isActive = true)
            userDao.insert(user)

            // 更新数据
            val updatedUser = user.copy(isActive = false)
            userDao.update(updatedUser)

            // 查询数据
            val users = userDao.getAll()
            for (user in users) {
                println("User: id=${user.id}, name=${user.name}, isActive=${user.isActive}")
            }

            // 查询单个用户
            val singleUser = userDao.getById(1)
            singleUser?.let {
                println("Single User: id=${it.id}, name=${it.name}, isActive=${it.isActive}")
            }

            // 删除数据
            userDao.delete(updatedUser)
        }
    }
    通过使用 @ColumnInfo 注解,我们可以避免字段名以 "is" 开头导致的问题,并且确保在数据库中使用不同的列名来保持代码的清晰和一致性。


    最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


    推荐阅读

    Harmony:关于鸿蒙系统的内容都总结在这里了
    学完ASM Tree api,再也不怕hook了
    面试题:为什么使用 Bundle 而不使用 HashMap


    扫一扫 关注我的公众号

    如果你想要跟大家分享你的文章,欢迎投稿~


    ┏(^0^)┛明天见!

    继续滑动看下一个
    鸿洋
    向上滑动看下一个

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存