You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Room数据库批量更新editedName:基于前缀后缀列表的SQL查询优化

问题

我的Channels实体包含两个核心属性:val name: String(服务器返回的原始名称)和val editedName: String(初始值与name一致,会根据用户设置的前缀后缀列表修改)。前缀后缀列表存储在PrefixesAndSuffixesSetting实体的val prefixes: List<String>和val suffixes: List<String>中。

处理规则示例

原始name列表:

  1. ABC TestChannel XZ
  2. ABC TestChannel VW
  3. 123 Test XY
  4. Test VW
  5. A TestChannel Xy

前缀列表:["ABC", "A"],后缀列表:["XZ", "VW"]

处理后的editedName应为:

  1. TestChannel
  2. TestChannel
  3. 123 Test XY
  4. Test
  5. TestChannel XY

当用户修改前缀后缀列表时,editedName需同步更新。例如移除后缀"XZ"后,第一条的editedName应变为TestChannel XZ。

当前实现与问题

目前通过内存遍历所有Channel的方式实现更新,代码如下:

suspend fun updatePrefixesAndSuffixes(prefixes: List<String>, suffixes: List<String>) {
    viewModelScope.launch {
        _nameProcessState.value = ChannelNameProcessState.Loading
        val allChannels = withContext(Dispatchers.IO) {
            dao.getAllChannels()
        }
        _nameProcessState.value = ChannelNameProcessState.GetChannels(100)
        val editedChannels = allChannels.map { channel ->
            var modifiedName = channel.name
            prefixes.forEach { prefix ->
                if (modifiedName.startsWith(prefix)) {
                    modifiedName = modifiedName.removePrefix(prefix).trim()
                }
            }
            _nameProcessState.value = ChannelNameProcessState.DatabaseInsertion(50)
            suffixes.forEach { suffix ->
                if (modifiedName.endsWith(suffix)) {
                    modifiedName = modifiedName.removeSuffix(suffix).trim()
                }
            }
            _nameProcessState.value = ChannelNameProcessState.DatabaseInsertion(100)
            channel.copy(editedName = modifiedName)
        }

        // Update the edited channels in the database
        dao.updateChannels(editedChannels)
        _nameProcessState.value = ChannelNameProcessState.Success
    }
}

该功能可用,但内存占用过高,执行时打开其他频道列表Fragment会触发OOM崩溃。

尝试的Room批量更新方案及问题

尝试编写Room的UPDATE查询实现批量更新,逻辑如下:

  1. 若Channel的name同时包含某前缀和某后缀,则editedName移除该前缀和后缀;
  2. 若仅包含某前缀或某后缀,则editedName移除对应内容;
  3. 否则保持editedName不变。

尝试的查询代码如下:

@Query("UPDATE channels SET editedName = " +
        "CASE " +
        "WHEN name LIKE '%' || :prefixes || '%' AND name LIKE '%' || :suffixes || '%' " +
        "     THEN REPLACE(REPLACE(name, :prefixes, ''), :suffixes, '') " +
        "WHEN name LIKE '%' || :prefixes || '%' " +
        "     THEN REPLACE(name, :prefixes, '') " +
        "WHEN name LIKE '%' || :suffixes || '%' " +
        "     THEN REPLACE(name, :suffixes, '') " +
        "ELSE editedName " +
        "END")
suspend fun updateChannelsWithPrefixesAndSuffixes(prefixes: List<String>, suffixes: List<String>)

但SQL无法直接处理List类型的前缀后缀参数,因此该方案无法生效。

想请教:是否可以通过Room/SQLite查询实现该需求?或需其他封装方式?还是应继续使用现有内存遍历方案?


解决方案

一、优化现有内存遍历方案

如果不想改动数据库查询逻辑,可先优化内存占用问题:

  • 分批处理数据:不要一次性加载所有Channel到内存,分页从DAO获取数据(比如每次取100条),处理完一批就更新一批到数据库,释放当前批内存后再处理下一批。
  • 减少不必要的对象持有:分批处理能降低同时存在的Channel对象数量,避免内存过载。
  • 优化状态更新频率:当前代码在map中频繁更新UI状态,会增加额外开销,可改为每处理完一批更新一次进度。

优化后的示例代码:

suspend fun updatePrefixesAndSuffixes(prefixes: List<String>, suffixes: List<String>) {
    viewModelScope.launch {
        _nameProcessState.value = ChannelNameProcessState.Loading
        val totalChannels = withContext(Dispatchers.IO) {
            dao.getChannelCount()
        }
        var processedCount = 0
        val batchSize = 100
        var offset = 0

        while (offset < totalChannels) {
            val batchChannels = withContext(Dispatchers.IO) {
                dao.getChannelsBatch(offset, batchSize)
            }
            val editedBatch = batchChannels.map { channel ->
                var modifiedName = channel.name
                prefixes.forEach { prefix ->
                    if (modifiedName.startsWith(prefix)) {
                        modifiedName = modifiedName.removePrefix(prefix).trim()
                    }
                }
                suffixes.forEach { suffix ->
                    if (modifiedName.endsWith(suffix)) {
                        modifiedName = modifiedName.removeSuffix(suffix).trim()
                    }
                }
                processedCount++
                channel.copy(editedName = modifiedName)
            }
            withContext(Dispatchers.IO) {
                dao.updateChannels(editedBatch)
            }
            _nameProcessState.value = ChannelNameProcessState.Processing(processedCount * 100 / totalChannels)
            offset += batchSize
        }
        _nameProcessState.value = ChannelNameProcessState.Success
    }
}

需在DAO中添加对应的分页查询和计数方法:

@Query("SELECT COUNT(*) FROM channels")
suspend fun getChannelCount(): Int

@Query("SELECT * FROM channels LIMIT :limit OFFSET :offset")
suspend fun getChannelsBatch(offset: Int, limit: Int): List<Channel>

二、Room/SQLite批量更新方案

SQLite本身不支持直接传入List参数,但可通过以下方式实现:

1. 动态生成SQL语句

通过Room的@RawQuery注解,动态拼接处理所有前缀和后缀的SQL逻辑。核心思路是:

  • 对前缀列表,生成判断name是否以任意前缀开头的条件,并依次移除匹配的前缀;
  • 对后缀列表,生成判断name是否以任意后缀结尾的条件,并依次移除匹配的后缀;
  • 将逻辑拼接到UPDATE语句中。

示例实现:

// DAO中添加RawQuery方法
@RawQuery
suspend fun updateChannelsWithDynamicPrefixSuffix(query: SupportSQLiteQuery)

// 构建动态SQL的方法
fun buildUpdateQuery(prefixes: List<String>, suffixes: List<String>): SupportSQLiteQuery {
    val sb = StringBuilder("UPDATE channels SET editedName = name")

    // 处理前缀:依次移除匹配的前缀
    prefixes.forEach { prefix ->
        val escapedPrefix = prefix.replace("'", "''")
        sb.append(" CASE")
        sb.append(" WHEN editedName LIKE '$escapedPrefix%' THEN TRIM(SUBSTR(editedName, ${prefix.length + 1}))")
        sb.append(" ELSE editedName END")
    }

    // 处理后缀:依次移除匹配的后缀
    suffixes.forEach { suffix ->
        val escapedSuffix = suffix.replace("'", "''")
        sb.append(" CASE")
        sb.append(" WHEN editedName LIKE '%$escapedSuffix' THEN TRIM(SUBSTR(editedName, 1, LENGTH(editedName) - ${suffix.length}))")
        sb.append(" ELSE editedName END")
    }

    return SimpleSQLiteQuery(sb.toString())
}

调用方式:

val query = buildUpdateQuery(prefixes, suffixes)
dao.updateChannelsWithDynamicPrefixSuffix(query)

注意:必须转义前缀后缀中的SQL特殊字符(如单引号),避免SQL注入风险。

2. 使用临时表(适合大量前缀后缀场景)

如果前缀后缀数量极大,可先将列表插入临时表,再通过JOIN关联处理更新。但该方式逻辑复杂,一般动态生成SQL即可满足需求。

三、其他思路:流式处理数据

如果数据库中Channel数量极大,分批处理仍有压力,可使用Room的Flow分页加载:

  • 通过DAO返回Flow<List<Channel>>,每次收集一批数据处理,处理完后再收集下一批;
  • 确保所有数据处理在IO线程执行,处理完成后立即更新数据库并释放对象。

内容的提问来源于stack exchange,提问作者Alex Mutschl

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 07:55:56