27、复杂查询:@RawQuery原始查询、子查询、JOIN查询、聚合函数
Room 的 DAO 注解确实好用,但说实话,遇到真正复杂的查询场景时,光靠 @Query 那套模板字符串有时候会让人抓狂。我早期做项目时,就碰到过一个报表需求——要跨三张表做聚合,还要动态拼接筛选条件。当时用 @Query 硬写了一个超长的 SQL,结果维护起来简直噩梦。
后来我学乖了。Room 其实给了我们好几把「重武器」:@RawQuery、子查询、JOIN 查询、聚合函数。今天咱们就把这些家伙挨个捋一遍。
27.1 @RawQuery:把 SQL 控制权完全交给你
@RawQuery 和 @Query 最大的区别是什么?@Query 在编译时就固定了 SQL 模板,而 @RawQuery 允许你在运行时动态构建 SQL 语句。说白了,它就是一个「逃生舱」——当 Room 的静态检查满足不了你时,就用它。
@Query:编译时校验 SQL,性能更好,但不够灵活@RawQuery:运行时执行,完全动态,但失去了编译期检查
来看一个实际例子。假设我们要做一个搜索功能,用户可以选择按「标题」「作者」「标签」来搜索,而且可以多条件组合。用 @Query 你得写 N 个方法,用 @RawQuery 一个就够了:
@Dao
interface ArticleDao {
@RawQuery(observedEntities = [Article::class])
fun searchArticles(query: SupportSQLiteQuery): List<Article>
}
// 调用时动态构建 SQL
fun buildSearchQuery(keyword: String, filterBy: String): SupportSQLiteQuery {
val sql = when (filterBy) {
"title" -> "SELECT * FROM articles WHERE title LIKE ?"
"author" -> "SELECT * FROM articles WHERE author LIKE ?"
"tag" -> "SELECT * FROM articles WHERE tag LIKE ?"
else -> "SELECT * FROM articles WHERE title LIKE ? OR author LIKE ?"
}
return SimpleSQLiteQuery(sql, arrayOf("%$keyword%"))
}
@RawQuery。如果查询条件是固定的,老老实实用 @Query,编译期就能帮你揪出 SQL 拼写错误。
27.2 子查询:嵌套查询的艺术
子查询说白了就是「查询里套查询」。Room 完全支持,你直接在 @Query 里写嵌套 SELECT 就行。我印象最深的一个场景是:查「最近一周内,评论数超过 10 条的热门文章」。这用子查询写起来特别自然:
@Query("""
SELECT * FROM articles
WHERE id IN (
SELECT article_id FROM comments
WHERE created_at > :weekAgo
GROUP BY article_id
HAVING COUNT(*) > :minCount
)
""")
fun getHotArticles(weekAgo: Long, minCount: Int): List<Article>
为什么会这样?因为子查询把「筛选逻辑」和「主查询」分开了,代码读起来就像在描述业务需求。你想想看,如果不用子查询,你得先查评论表拿到 article_id 列表,再查文章表——多了一次网络往返(如果是跨进程)或者多了一次内存操作。
27.3 JOIN 查询:把表连起来
JOIN 是关系型数据库的灵魂。Room 里 JOIN 查询的写法跟原生 SQL 一模一样。常用的有三种:
| JOIN 类型 | 说明 | 使用场景 |
|---|---|---|
| INNER JOIN | 只返回匹配的行 | 查「有评论的文章」 |
| LEFT JOIN | 返回左表所有行,右表无匹配则补 NULL | 查「所有文章及其评论数(包括没评论的)」 |
| CROSS JOIN | 笛卡尔积,慎用 | 极少用,除非你知道自己在做什么 |
来看一个 LEFT JOIN 的实战例子。我们要查「每篇文章的标题、作者、以及它的最新一条评论内容」:
data class ArticleWithLatestComment(
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "author") val author: String,
@ColumnInfo(name = "latest_comment") val latestComment: String?
)
@Query("""
SELECT a.title, a.author, c.content AS latest_comment
FROM articles a
LEFT JOIN comments c ON a.id = c.article_id
WHERE c.id = (
SELECT MAX(id) FROM comments WHERE article_id = a.id
) OR c.id IS NULL
""")
fun getArticlesWithLatestComment(): List<ArticleWithLatestComment>
这里用了 LEFT JOIN 加子查询来取「最新评论」。注意看,c.id IS NULL 这个条件是为了保留那些没有评论的文章——LEFT JOIN 的特性就是右表无匹配时补 NULL。
ArticleWithLatestComment)。不要想着用 @Relation 注解去映射 JOIN 结果,那是给嵌套关系用的,JOIN 的结果是扁平的。
27.4 聚合函数:COUNT、SUM、AVG、MAX、MIN
聚合函数在 Room 里用起来很简单,但有个细节很多人会忽略——返回值类型。来看几个典型用法:
@Dao
interface StatsDao {
// 统计总数,返回 Int
@Query("SELECT COUNT(*) FROM articles")
fun getArticleCount(): Int
// 统计平均值,返回 Double
@Query("SELECT AVG(price) FROM products WHERE category = :cat")
fun getAvgPrice(cat: String): Double
// 分组统计,返回自定义数据类
@Query("""
SELECT category, COUNT(*) AS count, AVG(price) AS avg_price
FROM products
GROUP BY category
""")
fun getCategoryStats(): List<CategoryStat>
}
data class CategoryStat(
val category: String,
val count: Int,
@ColumnInfo(name = "avg_price") val avgPrice: Double
)
嗯,这里要注意:COUNT(*) 返回的是 Int,AVG 返回的是 Double。如果你用 SUM 对整数字段求和,Room 会返回 Long。这些类型映射是固定的,搞错了编译期就会报错。
我个人习惯把聚合查询的结果封装成单独的数据类,就像上面的 CategoryStat。这样代码结构清晰,而且 Room 会自动帮你做列名映射——只要 @ColumnInfo 写对了就行。
27.5 综合实战:一个带统计的仪表盘
最后咱们来一个综合案例。假设我们要做一个「文章仪表盘」,展示:
- 文章总数
- 最近 7 天新增文章数
- 评论最多的前 5 篇文章
- 每个分类的文章数
这个需求涉及了聚合函数、子查询、JOIN、以及 @RawQuery 的混合使用。我当时的做法是拆成四个 DAO 方法,然后在 Repository 层组装:
@Dao
interface DashboardDao {
@Query("SELECT COUNT(*) FROM articles")
fun getTotalCount(): Int
@Query("SELECT COUNT(*) FROM articles WHERE created_at > :since")
fun getRecentCount(since: Long): Int
@Query("""
SELECT a.title, COUNT(c.id) AS comment_count
FROM articles a
LEFT JOIN comments c ON a.id = c.article_id
GROUP BY a.id
ORDER BY comment_count DESC
LIMIT 5
""")
fun getTopCommentedArticles(): List<ArticleCommentCount>
@RawQuery(observedEntities = [Article::class])
fun getCategoryStats(query: SupportSQLiteQuery): List<CategoryStat>
}
// 在 Repository 中动态构建分类统计的 SQL
fun getCategoryStats(): List<CategoryStat> {
val sql = """
SELECT category, COUNT(*) AS count
FROM articles
WHERE category IS NOT NULL
GROUP BY category
ORDER BY count DESC
"""
return dashboardDao.getCategoryStats(SimpleSQLiteQuery(sql))
}
@RawQuery 保留了动态能力。你想想看,如果全部塞到一个 @RawQuery 里,代码会变成什么样?维护起来绝对想骂人。
好了,关于复杂查询的内容就这些。记住一个原则:能用 @Query 就别用 @RawQuery,能用 JOIN 就别用多层子查询,能用聚合函数就别在 Java/Kotlin 代码里手动算。Room 帮你做了很多事,但前提是你得把 SQL 写对。
公众号:蓝海资料掘金营,微信 deep3321