【问题标题】:Why can't I use Options inside of a slick query为什么我不能在 slick 查询中使用 Options
【发布时间】:2014-07-01 16:10:45
【问题描述】:

为了省去我创建这么多方法的麻烦,我尝试将 Option 传递到我的方法中,然后检查是否定义了 Option,如果是,则应用过滤器。

def getUsers(locationId: Option[Int], companyId: Int, salary: Option[Int]): List[User] = {
  val query = for {
    u <- users if u.companyId === companyId && (locationId.isDefined && u.locationId === locationId.get) && (salary.isDefined && u.salary >= salary.get)

  }
  query.list()
}

我收到错误消息:

polymorphic expression cannot be instantiated to expected type;

IntelliJ errors are expected Boolean actual Column[Boolean].

这种类型的子句是不是在一个灵活的查询中是不可能的,还是我做错了?

【问题讨论】:

    标签: scala playframework slick


    【解决方案1】:

    我不能告诉你为什么,但这是为我编译的:

    def getUsers(locationId: Option[Int], companyId: Int, salary: Option[Int]): List[User] = {
      val query = for {
        u <- users if u.companyId === companyId && locationId.isDefined && u.locationId === locationId.get && salary.isDefined && u.salary >= salary.get
      } yield(u)
      query.list()
    }
    

    请注意,没有括号,并且您必须 yield 一些东西,否则 query 的返回类型将是 Unit

    【讨论】:

    • 它对我的查询是“真实的”,即 where (... ... and true)
    • 嗯对我来说编译没有错误,你是否删除了条件周围的括号?例如。而不是if true &amp;&amp; (id === thatId) 应该是if true &amp;&amp; id === thatId。最后,for理解中的if去糖化为filter的惰性变体,即withFilter,如果你有一个true,它应该可以编译,我只是尝试了id。
    • 是的,它编译得很好,我只是说它在 sql 查询中插入了一个 true
    • 我明白了,我误读了您的评论。老实说,我怀疑这是否可行,但我无法运行实际查询。可能完整的过滤条件从没有意义的光滑转换为 SQL(但在 Scala 中确实如此)。我很好奇 Slick 将如何翻译 None 以及在这种情况下的连续条件。您可以将查询分成小块,例如执行第一个过滤器,然后如果 isDefined 为真,则应用第二个过滤器,依此类推,但这将是冗长的,并且错过了理解的重点。这是我所能做到的,希望其他人能给出更好的答案。跨度>
    • Slick 将 None 翻译为 SQL NULL 包括 SQL 的 3 值逻辑 NULL 传播,因此 (None === a) 为 None 无论 a 的值如何,即使 a 为 None,比较结果仍然是 None,SQL 将其解释为 false。 && 也一样。所以基本上如果表达式中有任何内容为None,则整个表达式将为None,因此过滤器表达式将被视为false,查询结果将为空。
    【解决方案2】:

    当然,在这里看不到任何问题,只需使用过滤器(或 withFilter)并映射选项即可。

    def getUsers(locationId: Option[Int], companyId: Int, salary: Option[Int]): List[User] = (for {
      u <- users filter(u=>
        if (u.companyId === companyId.bind) && 
        (locationId.map(_.bind === u.locationId).getOrElse(true)) && 
        (salary.map(_.bind <= u.salary).getOrElse(true))
      )
    } yield u).list()
    

    使用过滤器可以让您下拉到 Scala 以获取地图或真正的后备表达式。如果您以u &lt; users if... 开头,则无法使用Scala 条件。 bind 调用只是逃避潜在的恶意输入(即,如果参数来自应用程序外部)。

    【讨论】:

      【解决方案3】:

      为什么它不起作用

      正如 cvot 所指出的 in his comment,这不起作用的原因是:

      Slick 将 None 翻译为 SQL NULL,包括 SQL 的 3 值逻辑 NULL 传播,因此 (None === a) 为 None 无论 a 的值如何...基本上如果表达式中的任何内容为 None,则整个表达式将为None,因此过滤器表达式将被视为false,查询结果将为空。

      也就是说,有一种方法可以获得您想要的相同行为(仅在提供可选值时过滤)。

      达到预期行为的方法

      需要注意的关键是,Scala 将 for comprehension 编译为 map / flatMap / withFilter / filter 调用的组合。如果我理解正确的话,Slick 在将 Scala 推导式编译成 SQL 查询时可以使用结果结构。

      这让我们可以分部分构建查询:

      val baseQuery = for {
        u <- users if u.companyId === companyId
      } yield u
      
      val possiblyFilteredByLocation = if (locationId.isDefined) {
        baseQuery.withFilter(u => u.locationId === locationId.get
      } else baseQuery
      
      val possiblyFilteredBySalaryAndOrLocation = if (salary.isDefined) {
        possiblyFilteredByLocation.withFilter(u => u.salary >= salary.get)
      } else possiblyFilteredByLocation
      
      possiblyFilteredBySalaryAndOrLocation.list()
      

      我们可以通过使用varfold 来简化这一过程:

      var query = for {
        u <- users if u.companyId === companyId
      } yield u
      query = locationId.fold(query)(id => query.withFilter(u => u.locationId === id))
      query = salary.fold(query)(salary => query.withFilter(u => u.salary >= salary))
      query.list()
      

      如果我们经常这样做,我们可以将Option 上的这种过滤模式概括为如下所示:

      // Untested, probably does not compile
      implicit class ConditionalFilter(query: Query) {
        def ifPresent[T](value: Option[T], predicate: (Query, T) => Query) = {
          value.fold(query)(predicate(query, _))
        }
      }
      

      然后我们可以将整个过滤器链简化为:

      query
        .ifPresent[Int](locationId, (q, id) => q.withFilter(u => u.locationId === id))
        .ifPresent[Int](salary, (q, s) => q.withFilter(u => u.salary >= s))
        .list()
      

      【讨论】:

        【解决方案4】:

        您可以使用以下解决方案(使用 Slick 3.3.x):

        def getUsers(locationId: Option[Int], companyId: Int, minSalary: Option[Int]) = 
          users.
            .filter(_.company === companyId)
            .filterOpt(locationId)(_.locationId === _)
            .filterOpt(minSalary)(_.salary >= _)
        

        【讨论】:

          【解决方案5】:

          因为 Slick 查询被翻译成 SQL,它没有 Option 类的 isDefined 和 get 方法的概念。 但是您可以通过调用查询之外的方法并传递结果(通过选项上的 map 函数)来解决此问题。

          下面的代码应该可以修复它:

          def getUsers(locationId: Option[Int], companyId: Int, salary: Option[Int]): List[User] = {
              val locationAndSalary = for {
                  locId <- locationId;
                  sal <- salary
              } yield (locId, sal)
          
              locationAndSalary.map(locAndSal => {
                  val query = for {
                      u <- users if u.companyId === companyId && u.locationId === locAndSal._1 && u.salary >= locAndSal._2)  
                  } yield u
                  query.list()
              }).getOrElse(List[User]()) //If the locationID or salary is None, return empty list.
          }
          

          locationAndSalary 可能看起来很奇怪,但我们使用 for 推导式仅在 locationIdsalary 都有值并将结果存储在元组中时才使用值,其中 locationId 位于第一个位置和工资在第二。以下链接对其进行了解释:Scala: for comprehensions with Options

          编辑:根据@Ende Neu 的回答,如果添加 yield 语句,代码会编译,但我仍然认为我的解决方案更像是“Scala 方式”。

          【讨论】:

          • 嗨,但这不是我想要的。我想忽略过滤器的 locationId 和薪水,如果它们是 None 并且只有 companyId。您的查询与返回 List[User]() 如果它们是 None 不同。
          猜你喜欢
          • 2014-05-04
          • 2020-01-07
          • 2018-02-16
          • 1970-01-01
          • 1970-01-01
          • 2013-02-03
          • 2022-10-20
          • 2021-03-28
          • 1970-01-01
          相关资源
          最近更新 更多