如何在运行时动态查询房间数据库?

Posted

技术标签:

【中文标题】如何在运行时动态查询房间数据库?【英文标题】:How to dynamically query the room database at runtime? 【发布时间】:2017-11-01 10:01:19 【问题描述】:

问题

是否可以在运行时构造查询?

用例

@Query("SELECT * FROM playlist " +
        "WHERE playlist_title LIKE '% :playlistTitle %' " +
        "GROUP BY playlist_title " +
        "ORDER BY playlist_title " +
        "LIMIT :limit")
 List<IPlaylist> searchPlaylists(String playlistTitle, int limit);

limit 部分是可选的。也就是说,它应该能够在有限制或无限制的情况下执行相同的查询。

更复杂的用例

在前一种情况下,可以进行两个带限制部分和不带限制部分的静态查询,并且每次都可以使用适当的一个。但有时我们可能不得不处理更复杂的情况,例如构建过滤器。

在这种情况下,与前面的示例不同,将有多个可选部分。对于一个图书表,我们可能需要根据图书所属的类别、作者姓名、价格区间、出版日期等进行过滤。几乎不可能对这些部分的所有组合进行静态查询。

【问题讨论】:

“可选参数”是什么意思?暂时忽略 Room,您将如何使用传统的 android SQLite API 实现可选参数? @CommonsWare 我的意思是我想动态设置或不设置查询的限制参数。 好的。暂时忽略 Room,你将如何使用 SQLiteDatabase 来实现它?如果答案是“我会有两个不同的查询字符串”,那么为什么不使用两个不同的@Query 注释呢?由于注释可以引用static 字段(AFAIK),原则上,您甚至可以通过定义一次查询字符串的核心部分并从每个注释中引用它来减少冗余。 我理解你,我正在重构,因为房间很新,我正在尝试看看是否有更好的方法来做到这一点,否则我将创建两种方法. 【参考方案1】:

对于 Kotlin-Room-ViewModel

@Query("SELECT * FROM schedule_info_table where schedule_month = :monthValue ORDER BY schedule_date_time ASC")
fun getThisMonthSchedules(monthValue: Int): Flow<List<SchedulesInformation>>

【讨论】:

【参考方案2】:

根据documentation,不推荐使用原始查询

【讨论】:

android.arch 架构组件包已弃用,RawQuery is available in androidx【参考方案3】:

Room 支持@RawQuery 注解在运行时构造查询。

第 1 步:制作 DAO 方法

使用@RawQuery 注释而不是普通的@Query 标记DAO 方法。

@Dao
interface BooksDao
    @RawQuery
    List<Book> getBooks(SupportSQLiteQuery query);

第 2 步:构造查询

Room 使用准备好的语句进行安全性和编译时验证。因此,在构造查询时,我们需要分别存储查询字符串和绑定参数。

在本例中,我使用变量queryString 查询字符串和args 绑定参数。

(请注意我使用文本编辑器编写代码。因此可能存在拼写错误或简单的语法错误。如果您发现任何内容,请在 cmets 中告诉我或编辑帖子。)

// Query string
String queryString = new String();

// List of bind parameters
List<Object> args = new ArrayList();

boolean containsCondition = false;

// Beginning of query string
queryString += "SELECT * FROM BOOKS";

// Optional parts are added to query string and to args upon here

if(!authorName.isEmpty())
    queryString += " WHERE";
    queryString += " author_name LIKE ?%";
    args.add(authorName);
    containsCondition = true;


if(fromDate!=null)
    
    if (containsCondition) 
        queryString += " AND";
     else 
        queryString += " WHERE";
        containsCondition = true;
    

    queryString += " publication_date AFTER ?";
    args.add(fromDate.getTime());


if(toDate!=null)
    
    if (containsCondition) 
        queryString += " AND";
     else 
        queryString += " WHERE";
        containsCondition = true;
    

    queryString += " publication_date BEFORE ?";
    args.add(toDate.getTime());


// End of query string
queryString += ";";

第 3 步:执行查询

SimpleSQLiteQuery query = new SimpleSQLiteQuery(queryString, args.toArray());
List<Book> result = booksDao.getBooks(query);

注意事项

与普通的QueryRawQuery 一样支持返回原始游标、实体、POJO 和带有嵌入字段的 POJO RawQuery 支持关系

【讨论】:

@RawQuery 是否存在 SQL 注入攻击风险,因此存在安全风险? 没有。它不是纯文本查询,因为名称让我们思考。它在内部使用绑定变量。 SimpleSQLiteQuery,更准确地说。 我想推荐一些能让这项工作更轻松的课程。我期待您的反馈。 UpdateRawQueryBuilderSimpleQueryBuilder @tim4dev 谢谢。未经测试,但乍一看还不错。 这应该是公认的答案,因为它解决了原始问题。【参考方案4】:
  @Query("select * from task where state = :states and sentdate between :fromdate and :todate")
  List<Task> getFilterAll(String states, String fromdate, String todate);

这里我们需要使用列名状态。每当需要实现自定义查询时,只需将值通过参数从活动或片段传递到我们将在查询中应用的接口。就像上面代码中的例子(:fromdate:todate

冒号是必须的。您将在我们将提到的查询中使用哪个参数以: 符号开头。

【讨论】:

【参考方案5】:

让它更简单。我将向您展示使用两个变量的 where 子句的示例。这样做:

  @Query("SELECT * FROM Student WHERE stdName1= :myname AND stdId1=:myid")
List<Student> fetchAllData(String myname,int myid);

stdName1 和 stdId1 是列名

【讨论】:

谢谢你们。这:小人物帮助了我。【参考方案6】:

使用 SupportSQLiteQuery。

https://developer.android.com/reference/android/arch/persistence/db/SupportSQLiteQuery

最新版本 1.1.1 现在使用 SupportSQLiteQuery。

带有类型绑定的查询。最好使用此 API 而不是 rawQuery(String, String[]) 因为它允许绑定类型安全 参数。

@Dao
     interface RawDao 
         @RawQuery(observedEntities = User.class)
         LiveData<List<User>> getUsers(SupportSQLiteQuery query);
     

用法:

     LiveData<List<User>> liveUsers = rawDao.getUsers( new 
SimpleSQLiteQuery("SELECT * FROM User ORDER BY name DESC"));

将您的 gradle 更新到 1.1.1

implementation 'android.arch.persistence.room:runtime:1.1.1'
implementation 'android.arch.lifecycle:extensions:1.1.1'
annotationProcessor "android.arch.persistence.room:compiler:1.1.1"

注意:如果您升级到 1.1.1,并且使用 String 而不是 SupportSQLiteQuery,

你会得到错误:

RawQuery 不再允许传递字符串。请用 android.arch.persistence.db.SupportSQLiteQuery。

如上使用 SupportSQLiteQuery 将解决问题。

注意: 确保传入 SupportSQLiteQuery 查询参数,否则会出现此错误:

RawQuery 方法应该有 1 个且只有 1 个类型为 String 的参数 或 SupportSQLiteQuery

【讨论】:

@RawQuery 是否存在 SQL 注入攻击风险,因此存在安全风险? @AJW 否,如果与 SupportSQLiteQuery 一起使用,则在根据 SupportSQLiteQuery 的文档继续之前验证参数。因此 SimpleSQLiteQuery 是安全的,因为它是 SupportSQLiteQuery 的子类 @live-love 使用上面提到的 SupportSQLiteQuery “......最好使用此 API 而不是 rawQuery(String, String[]),因为它允许绑定类型安全参数。”你是什​​么意思“类型安全参数”?这意味着使用独立的 RawQuery 存在风险,而使用 SupportSQLLiteQuery 可以消除该风险?【参考方案7】:

我没有编写多个查询,而是将负值传递给限制子句。因为如果查询发生变化,我必须更新这两个更容易出错的查询。

官方文档 -> 如果 LIMIT 表达式的计算结果为负值,则返回的行数没有上限。你可以在这里找到它https://sqlite.org/lang_select.html 并阅读限制子句部分。

所以我会做这样的事情,

@Query("SELECT * FROM playlist " +
    "WHERE playlist_title LIKE '% :playlistTitle %' " +
    "GROUP BY playlist_title " +
    "ORDER BY playlist_title " +
    "LIMIT :limit")
List<IPlaylist> searchPlaylists(String playlistTitle, int limit);

并在您不想应用过滤器时传递否定值。

return playlistDao.searchPlaylists(title, limit.isPresent() ? limit.get() : -1)

它在我的情况下工作。

更新 [2018 年 12 月 21 日]

如果您使用的是 kotlin,请使用默认值。

@JvmOverloads
@Query("SELECT * FROM playlist " +
        "WHERE playlist_title LIKE '% :playlistTitle %' " +
        "GROUP BY playlist_title " +
        "ORDER BY playlist_title " +
        "LIMIT :limit")
fun searchPlaylists(playlistTitle: String, limit: Int = -1): List<IPlaylist>

@JvmOverloads 使其与 Java 兼容。它为 Java 生成两个单独的方法。

【讨论】:

或者把 Dao 做成一个抽象类,把第二个方法委托给第一个方法,limit 参数设置为 -1。【参考方案8】:

Room 中没有可选参数,但有一个 @RawQuery 注释,您可以在其中将查询作为字符串传递,以便您可以在运行时构建 SQL 查询。我认为这对你有用。

以下是官方文档中的示例:

@Dao
 interface RawDao 
     @RawQuery
     User getUser(String query);
 

你可以这样使用它:

User user = rawDao.getUser("SELECT * FROM User WHERE id = 3 LIMIT 1");

重要提示: RawQuery 方法必须返回非空类型

重要提示:这在 Room 1.1.0-alpha3 中可用

【讨论】:

是的..但问题是它只对读取查询有效。你如何处理写查询? @RawQuery 是否存在 SQL 注入攻击风险,因此存在安全风险?【参考方案9】:

@Anderson K & @Juanky Soriano, 我同意@CommonsWare,

房间库有一些限制,那么我们也可以使用支持 SQLite 数据库的@query() 对房间数据库编写完全动态的查询

String mQuery = "SELECT * FROM foobar WHERE columnName1 IN ('value_1','value_2') and columnName2 In('value_3','value_4')";

AppDatabase appDatabase = Room.databaseBuilder(getApplicationContext(),
        AppDatabase.class, "database-name").build();

Cursor mCursor = AppDatabase.getAppDatabase(context).getOpenHelper().getReadableDatabase().query(myQuery);

现在您可以将光标逐行数据转换为您的 POJO 类。

【讨论】:

【参考方案10】:

根据我的经验(简短),使用 Room 是不可能的,不是因为 Room 限制,而是正如 @CommonsWare 隐含评论的那样,是对 SQLite 的限制。您需要两个查询,因此在您的 DAO 中需要两个方法。

我确实会有类似的东西:

@Query("SELECT * FROM playlist " +
    "WHERE playlist_title LIKE '% :playlistTitle %' " +
    "GROUP BY playlist_title " +
    "ORDER BY playlist_title " +
    "LIMIT :limit")
List<IPlaylist> searchPlaylists(String playlistTitle, int limit);

@Query("SELECT * FROM playlist " +
    "WHERE playlist_title LIKE '% :playlistTitle %' " +
    "GROUP BY playlist_title " +
    "ORDER BY playlist_title ")
List<IPlaylist> searchPlaylists(String playlistTitle);

然后在其他地方进行绕过:

if (limit.isPresent()) 
   return playlistDao.searchPlaylists(title, limit.get());
 else 
   return playlistDao.searchPlaylists(title);

这是我目前能想到的最佳选择。

【讨论】:

您好,在我的情况下它不起作用。特别是这一行“WHERE playlist_title LIKE '% :playlistTitle %'” 当我使用此方法时,“playlistTitle”未使用错误发生 这是一个很好的答案。但自从 Room 引入 @RawQuery 注释后,它已经过时了。 您需要将房间查询字符串与 || 连接起来所以这里变成:LIKE '%' || :播放列表标题 || '%' 。也用 || 连接所有

以上是关于如何在运行时动态查询房间数据库?的主要内容,如果未能解决你的问题,请参考以下文章

如何MyBatis中使用动态SQL查询与注释

如何针对分层对象列表动态构建和存储复杂的 linq 查询?

如何在 PHP 中为动态查询构建参数化 PDO 语句?

如何在 NHibernate 中使用 sql 查询创建表?

如何提高Oracle中动态sql的查询性能

当房间数据库为空并尝试运行此查询时,应用程序崩溃并显示空异常