【问题标题】:Slick: create query conjunctions/disjunctions dynamicallySlick:动态创建查询连词/析取词
【发布时间】:2015-02-02 15:36:22
【问题描述】:

我正在尝试为 Slick 表创建类型安全的动态 DSL,但不知道如何实现。

用户可以通过发送表单/json格式的过滤器将过滤器发布到服务器,我需要构建一个带有所有这些的Slick查询。

所以基本上这意味着将代表我的过滤器的 Scala 案例类转换为 Slick 查询。

似乎“谓词”可以有 3 种不同的形状。我已经看到了 CanBeQueryCondition 的特质。我可以折叠这些不同的可能形状吗?

我看过扩展方法 &&|| 并且知道与此有关,但我只是不知道该怎么做。

基本上,我有一个谓词列表,它采用以下类型:

(PatientTable) => Column[Option[Boolean]]

(PatientTable) => Column[Boolean]

对我来说问题是对于所有具有CanBeQueryCondition 的所有 3 种不同类型都没有一个超类型,所以我真的不知道如何折叠带有 && 的谓词,因为一旦添加到列表中这些不同形状的谓词采用非常通用的类型List[(PatientTable) => Column[_ >: Boolean with Option[Boolean]]]

另外,我不确定什么可以被视为 Slick 中的谓词。一个可组合的谓词似乎是Column[Boolean],但实际上filter 方法只接受(PatientTable) => Column[Boolean] 类型的参数

【问题讨论】:

    标签: scala slick play-slick


    【解决方案1】:

    我正在用我最终构建的东西来回答我自己的问题。

    让我们定义一个简单的案例类和行映射器

    case class User(
                        id: String = java.util.UUID.randomUUID().toString,
                        companyScopeId: String,
                        firstName: Option[String] = None,
                        lastName: Option[String] = None
                        ) 
    
    
    class UserTable(tag: Tag) extends Table[User](tag,"USER") {
      override def id = column[String]("id", O.PrimaryKey)
      def companyScopeId = column[String]("company_scope_id", O.NotNull)
      def firstName = column[Option[String]]("first_name", O.Nullable)
      def lastName = column[Option[String]]("last_name", O.Nullable)
    
      def * = (id, companyScopeId, firstName, lastName) <>
        (User.tupled,User.unapply)
    }
    

    Slick 中谓词的概念

    我假设“谓词”的概念可以放在TableQuery.filter 中。但是这种类型相当复杂,因为它是一个接受Table 并返回具有隐式CanBeQueryCondition 的类型的函数

    不幸的是,有 3 种不同的类型都有 CanBeQueryCondition 并将它们放在一个列表中以折叠成一个谓词似乎并不容易(即 filter 很容易应用,但 &amp;&amp; 和 @ 987654329@ 运营商很难申请(据我所知))。但幸运的是,我们似乎可以使用.? 扩展方法轻松地将Boolean 转换为Colunm[Boolean]Column[Option[Boolean]]

    所以让我们定义我们的谓词类型:

    type TablePredicate[Item, T <: Table[Item]] = T => Column[Option[Boolean]]
    

    折叠谓词列表(即使用连词/分离,即组合 AND 和 OR 子句)

    现在我们只有一种类型,所以我们可以轻松地将谓词列表折叠成一个

      // A predicate that never filter the result
      def matchAll[Item, T <: Table[Item]]: TablePredicate[Item,T] = { table: T => LiteralColumn(1) === LiteralColumn(1) }
    
      // A predicate that always filter the result
      def matchNone[Item, T <: Table[Item]]: TablePredicate[Item,T] = { table: T => LiteralColumn(1) =!= LiteralColumn(1) }
    
      def conjunction[Item, T <: Table[Item]](predicates: TraversableOnce[TablePredicate[Item, T]]): TablePredicate[Item,T]  = {
        if ( predicates.isEmpty ) matchAll[Item,T]
        else {
          predicates.reduce { (predicate1, predicate2) => table: T =>
            predicate1(table) && predicate2(table)
          }
        }
      }
    
      def disjunction[Item, T <: Table[Item]](predicates: TraversableOnce[TablePredicate[Item, T]]): TablePredicate[Item,T] = {
        if ( predicates.isEmpty ) matchNone[Item,T]
        else {
          predicates.reduce { (predicate1, predicate2) => table: T =>
            predicate1(table) || predicate2(table)
          }
        }
      }
    

    动态过滤案例类

    从这些谓词原语中,我们可以开始创建基于案例类的动态、可组合和类型安全的查询 DSL。

    case class UserFilters(
                               companyScopeIds: Option[Set[String]] = None,
                               firstNames: Option[Set[String]] = None,
                               lastNames: Option[Set[String]] = None
                               ) {
    
      type UserPredicate = TablePredicate[User,UserTable]
    
    
      def withFirstNames(firstNames: Set[String]): UserFilters = this.copy(firstNames = Some(firstNames))
      def withFirstNames(firstNames: String*): UserFilters = withFirstNames(firstNames.toSet)
    
      def withLastNames(lastNames: Set[String]): UserFilters = this.copy(lastNames = Some(lastNames))
      def withLastNames(lastNames: String*): UserFilters = withLastNames(lastNames.toSet)
    
      def withCompanyScopeIds(companyScopeIds: Set[String]): UserFilters = this.copy(companyScopeIds = Some(companyScopeIds))
      def withCompanyScopeIds(companyScopeIds: String*): UserFilters = withCompanyScopeIds(companyScopeIds.toSet)
    
    
      private def filterByFirstNames(firstNames: Set[String]): UserPredicate = { table: UserTable => table.firstName inSet firstNames }
      private def filterByLastNames(lastNames: Set[String]): UserPredicate = { table: UserTable => table.lastName inSet lastNames }
      private def filterByCompanyScopeIds(companyScopeIds: Set[String]): UserPredicate = { table: UserTable => (table.companyScopeId.? inSet companyScopeIds) }
    
    
      def predicate: UserPredicate = {
        // Build the list of predicate options (because filters are actually optional)
        val optionalPredicates: List[Option[UserPredicate]] = List(
          firstNames.map(filterByFirstNames(_)),
          lastNames.map(filterByLastNames(_)),
          companyScopeIds.map(filterByCompanyScopeIds(_))
        )
        // Filter the list to remove None's
        val predicates: List[UserPredicate] = optionalPredicates.flatten
        // By default, create a conjunction (AND) of the predicates of the represented by this case class
        conjunction[User,UserTable](predicates)
      }
    
    }
    

    注意.? 用于companyScopeId 字段的用法,它允许将非可选列适合我们的Slick 谓词定义

    使用 DSL

    val Users = TableQuery(new UserTable(_))
    
    val filter1 = UserFilters().withLastNames("lorber","silhol").withFirstName("robert")
    val filter2 = UserFilters().withFirstName("sebastien")
    
    val filter = disjunction[User,UserTable](Set(filter1.predicate,filter2.predicate))
    
    val users = Users.filter(filter.predicate).list
    
    // results in 
    // ( last_name in ("lorber","silhol") AND first_name in ("robert") ) 
    // OR 
    // ( first_name in ("sebastien") )
    

    结论

    这远非完美,但只是初稿,至少可以给你一些灵感 :) 我希望 Slick 能够更容易地构建在其他查询 DSL 中非常常见的东西(比如 Hibernate/JPA Criteria API)

    另请参阅Gist 了解最新解决方案

    【讨论】:

    • 我已经看到你在 github 上创建了一个名为“slick-criteria”的新存储库;)干得好,Sebastien!
    • 谢谢 :) 是的,这可能是个好主意,即使项目不会包含太多东西
    • 哇,这是金子!感谢分享!
    【解决方案2】:

    “折叠”已经是这里的关键字。或“减少”,因为您不需要种子值。 buildFilter.reduce(_ &amp;&amp; _)

    【讨论】:

    • 最近几天我在 stackoverflow 上读过几次“动态过滤”。也许值得记录它。你怎么看?我可以帮忙。
    • 是的,这是每个构建带有表单过滤器的 UI 的人都需要的功能。我有点惊讶这不是 Slick 提供的用于查询的主要功能,因为它非常常见。 Hibernate 和 JPA 多年来一直使用 Criteria API
    • 我不太明白。 .filter 与 .reduce 或 .fold 结合使用是一种很好的方法。你还需要什么? Slick 的可组合性使其成为现实,这是 Slick 的主要特点。
    • 记录它是个好主意。如果有人可以提交 PR,那就太好了。 slick.typesafe.com/doc/2.1.0(请参阅顶部github上的编辑此页面)
    【解决方案3】:

    似乎想要一个更通用的版本:Dynamic OR filtering - Slick。我认为我在此页面上的最后一个示例正是您想要的 - 这正是 cvogt 提出的。我希望这有帮助。

    【讨论】:

    • 感谢 Thoefer,这是一个好主意,我会进行调查。但是,在您的示例中,所有谓词似乎都具有相同的shape,我的意思是您的谓词列表都是Column[Boolean],或者都是Colunm[Option[Boolean]]。此外,我希望折叠过滤器实际上保持可组合性,以便能够组合非常复杂的连词和析取词。类似于 filter1.build &amp;&amp; filter2.build || filter3.build 的东西,其中 3 个部分中的每一个都已经是一组折叠的谓词。
    • 很高兴它有帮助!对我来说,这听起来像是应该在 slick 之上的东西,我认为这对于 slick 来说是开箱即用的。希望您分享一个可能的解决方案,听起来很有趣。
    • 其实我开始成功了。看来我可以使用.?Colunm[Boolean] 谓词转换为Column[Option[Boolean]],从而能够在我可以调用&amp;&amp;|| 的常见类型下使我的列表同质化。我编写了一个组合谓词函数(带有表参数)的折叠函数。它只是稍微复杂一点,但不是那么多。将尝试查看它的去向并在此处发布答案
    【解决方案4】:

    我一直在寻找同样的东西,并遇到了这个问题 - 接受的答案对我最终得到的启发非常大。详情here

    我对已接受答案的唯一 cmets - TablePredicate[Item, T &lt;: Table[Item]] 可以简化为 TablePredicate[T &lt;: Table[_]] 因为 Item 从未使用过(至少在示例中)。 LiteralColumn(1) === LiteralColumn(1) 也可以只是 LiteralColumn(Some(true))(使生成的查询稍微不那么尴尬)——我很确定只要再做一些工作,这些就可以完全消除。

    【讨论】:

      猜你喜欢
      • 2021-09-09
      • 1970-01-01
      • 2018-09-22
      • 2021-12-09
      • 2011-09-17
      • 1970-01-01
      • 2015-01-20
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多