Room数据库使用一些坑
本文作者
作者:未扬帆的小船
链接:
https://juejin.cn/post/7386844718141374473
本文由作者授权发布。
1. 分页查询(Paging Library)
java
// 在Dao中使用PagingSource
@Query("SELECT * FROM your_table")
PagingSource<Integer, YourEntity> getAllData();
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
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的内存管理
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();
// 处理数据
});
java
@Query("SELECT * FROM your_table")
Cursor getAllDataCursor();
在使用Cursor时,要注意及时关闭Cursor以释放资源。
8. 数据库设计优化
使用Cursor怎么保证会释放资源 怎么释放?
1. 手动管理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
java
try (Cursor cursor = yourDao.getAllDataCursor()) {
if (cursor != null && cursor.moveToFirst()) {
do {
// 处理每一行数据
} while (cursor.moveToNext());
}
} // try-with-resources会自动调用cursor.close()
3. 在ViewModel或Repository中管理Cursor
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
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
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
java
@Query("SELECT * FROM your_table")
CursorWrapper getAllDataCursor();
通过这些方法,可以确保Cursor在使用完后被及时关闭,避免内存泄漏和OOM问题。
1. 确保多个操作的原子性
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);
}
}
2. 在查询操作中使用@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;
}
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);
}
}
使用@Transaction确保了数据库操作的一致性和完整性,尤其在涉及多个表的复杂操作时非常有用。
方法1:使用@Query执行DELETE语句
java
@Dao
public interface UserDao {
@Query("DELETE FROM users")
void deleteAllUsers();
}
java
@Dao
public interface UserDao {
@Query("DELETE FROM users LIMIT :batchSize")
void deleteUsersInBatch(int batchSize);
}
方法2:使用Room的事务(@Transaction)
java
@Dao
public interface UserDao {
@Transaction
@Query("DELETE FROM users")
void deleteAllUsers();
}
方法3:使用WorkManager进行后台操作
方法4:适当的异常处理和内存管理
示例代码
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);
}
总结
通过使用适当的查询语句、事务、后台任务和内存管理技术,你可以在使用Room框架时优雅地处理大量数据的删除操作,避免OOM错误的发生,并提高应用程序的性能和稳定性。
SQLite 缓存的工作原理:
Room 中的缓存:
如何优化数据访问和内存管理:
合理使用分页查询:对于大量数据,使用分页技术来限制一次查询返回的数据量,以减少内存消耗。 优化查询语句:确保查询语句简洁高效,只检索必要的数据列,避免一次性检索过多的数据。 使用事务:对于大量的插入、更新或删除操作,使用事务来批量处理,减少频繁的数据库操作对性能的影响。 避免内存泄漏:确保在不需要时及时释放数据库访问对象和结果集,避免不必要的内存持有。 使用合适的数据库管理策略:根据应用程序的需求和性能需求,选择合适的数据库管理和操作策略,如Room、SQLite直接操作或者其他ORM框架。
调整SQLite缓存的方式:
如果你确定需要调整SQLite的缓存大小,可以通过SQLite的PRAGMA语句来调整。但这通常需要在SQLite库编译时进行配置,并且需要在底层C代码中进行设置,不适用于在Android应用程序中动态调整。
当你使用SELECT column1, column2 FROM your_table从数据库中查询数据时,需要注意以下几点,以确保数据处理的正确性和效率:
1. 列名的一致性
2. 防止SQL注入
3. 资源管理
4. 处理空值
5. 索引优化
6. 查询性能
7. 并发控制
8. 异常处理
示例代码
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())
}
}
}
}
关键点解释
kotlin
val cursor: Cursor? = db.rawQuery(
"SELECT column1, column2 FROM your_table WHERE column1 > ?",
arrayOf("value")
)
使用参数化查询来防止SQL注入。
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。
kotlin
val column1 = if (column1Index != -1) it.getString(column1Index) else null
val column2 = if (column2Index != -1) it.getString(column2Index) else null
检查列索引是否有效,并处理可能的空值。
总结
通过以上方法和注意事项,可以确保在进行数据库查询时既能有效获取数据,又能防止常见问题,如SQL注入、资源泄漏和空值处理问题。
获取总记录数:在进行分页查询之前,先查询数据库中的总记录数,根据总记录数来判断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.")
}
}
}
}
关键点解释
kotlin
@Query("SELECT COUNT(*) FROM users")
suspend fun getUserCount(): Int
定义一个查询方法来获取数据库中的总记录数。
kotlin
val totalPages = (userCount + pageSize - 1) / pageSize
根据总记录数和每页的记录数计算总页数。
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越界问题,确保查询不会尝试跳过超过数据库中实际存在的记录数。
clearUsersTable() 和 DELETE FROM users 在功能上是相同的,都用于删除表中的所有数据,但在实现细节和使用场景上可能会有所不同。
clearUsersTable()
kotlin
@Dao
interface UserDao {
@Query("DELETE FROM users")
suspend fun clearUsersTable()
}
DELETE FROM users
sql
DELETE FROM users;
详细解释
功能:
两者都是用来删除表中的所有记录。
通过 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
}
注意事项
事务管理:
虽然单个 DELETE FROM users 操作通常是原子的,但在复杂的业务逻辑中,可以将多个相关操作放在一个事务中,以确保数据一致性。
使用协程 (suspend) 确保删除操作在后台线程中进行,不阻塞主线程,适合在 Android 应用中使用。
如果需要重置主键自增序列,可以使用 TRUNCATE TABLE 而不是 DELETE,但 Room 不支持 TRUNCATE 语句。如果有这种需求,可能需要手动执行原生 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
}
通过这种方式,可以有效删除表中的数据并处理主键自增序列。
依赖项
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
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 数据库
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 数据库
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}")
}
关键点总结
初始化加密数据库:通过 SQLiteDatabase.loadLibs(context) 初始化 SQLCipher,并使用 SupportFactory 创建加密数据库。 数据库操作:增删改查操作与普通 Room 数据库一致,不需要额外的加密解密处理,因为 SQLCipher 会自动处理这些。 密码管理:确保密码安全管理,因为它是数据库安全的核心。
重要注意事项
密码管理:确保密码的安全存储和管理。密码的安全性直接关系到数据库的安全性。 数据库升级:在进行数据库版本升级时,确保兼容性,特别是在涉及到加密数据库时。 性能影响:使用加密数据库可能会有性能影响,尤其是在大数据量读写时,需要进行性能测试以确保满足应用需求。
示例
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
)
完整示例
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)
}
}
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!