在 scala slick 中使用多列进行动态排序

Dynamic order by in scala slick with several columns

我一直在学习scala、playframework和slick,但是我发现了一个问题。 我正在尝试制作一个简单的 CRUD,其中包含一个列表控制器,该列表控制器接收一个自定义筛选字段、一些分页信息(页面大小和编号)以及一个带有字段名称和顺序(asc 或 desc)的字符串元组序列,以及所有内容工作正常,除了按顺序排序,我无法按动态排序。

我从Scadiddle blog得到了基本结构。 所以,基本代码如下:

我有我的基本颜色模型:

case class Color(
  id: Int,
  name: String)

这是一个简单的table定义:

    class ColorsTable(tag: Tag) extends Table[Color](tag, "color") {
        def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
        def name = column[String]("name")
        def * = (id, name) <> ((Color.apply _).tupled, Color.unapply)
      }

在我的回购中我有搜索方法:

def findAll(searchTerm: Option[String], page: Int, top: Int, sortsBy: Seq[(String, SortDirection)]): Future[Seq[Color]] = {
    var query = searchTerm match {
      case Some(term) => colors.filter(_.name like s"%$term%")
      case _ => colors
    }
    val offset = (page - 1) * top
    // This is building the sort clause, matching on each column in the sort
    sortsBy.foreach {
      sortTuple =>
        val (sortColumn, sortDirection) = sortTuple
        query = query.sortBy(sortColumn match {
          case "id" => if (sortDirection == Desc) _.id.desc else _.id.asc
          case _ => if (sortDirection == Desc) _.name.desc else _.name.asc
        })
    }
    // The "list" method actually executes the query
    val colorsQuery = query.drop(offset).take(top).result

    db.run(colorsQuery)
  }

问题是,当我用这个序列调用搜索方法时:

val sortsBy = Seq[(String, SortDirection)](("name", Desc), ("id", Asc))
    colorService.getColors(None, 1, 10, sortsBy).map(colorList => Ok(Json.toJson(colorList)))

生成此查询:

select "id", "name" from "color" order by "id", "name" desc limit 10 offset 0

如您所见,sortBy 的顺序颠倒了(先是 id,然后是 name,而不是 name 和 id 的顺序)。

如果我使用元组而不是 foreach,则会遵守顺序:

query = query.sortBy(
      s => (s.name.desc, s.id.asc)
    )

但是无法生成 动态大小 元组。更令人困惑的是,给我带来麻烦的另一件事是 this part 在光滑的文档中:

Be aware that a single ORDER BY with multiple columns is not equivalent to multiple .sortBy calls but to a single .sortBy call passing a tuple

所以,实际上我可以使用 foreach 并连接订单吗?或者是因为这个限制顺序被颠倒了?

如果 sortBy 只能使用元组,我怎样才能实现动态大小的顺序?

PD:感谢关注,抱歉英语不好

编辑:

感谢您的快速回复,我尝试了您的代码并且看起来不错,遗憾的是我几乎不知道它是如何工作的:((scala 是一种非常好的但很难学习的语言:S)。

当我看到 Higher Kinded Type Should be Enabled 时,我吓坏了,寻找答案 this 并没有给我太多希望来获得一个简单的理解,希望当我完成 Scala 编程,第 3 版书时我会对这到底是怎么回事有更多的了解和了解。

还有一个问题,这是否相当于执行多次 sortBy 调用?这与使用元组相比如何?我仍然对这部分精巧的文档感到困惑:

具有多个列的单个 ORDER BY 不等同于多个 .sortBy 调用,而是一个传递元组的 .sortBy 调用

我检查了我的方法,并通过向 seq 添加反向使其工作正常,当然不像你的代码那样 功能 和漂亮,所以我将使用你的建议并想办法用助手制作其余的过滤器并避免使用变量。 (是的,在另一部分我仍在使用 var,但是当我对 Scala 有更多的了解时,我会让它变得更好)。

自白:在使用多种语言编程 8 年多之后(从 JavaScript 到 Java、C#、Python 等)我不得不重复, Scala 看起来是一门美丽但非常复杂的语言,但我不会放弃学习它

让我们定义 DynamicSortBySupport 助手

object DynamicSortBySupport {
  import slick.ast.Ordering.Direction
  import slick.ast.Ordering
  import slick.lifted.Query
  import slick.lifted.ColumnOrdered
  import slick.lifted.Ordered
  type ColumnOrdering = (String, Direction) //Just a type alias
  trait ColumnSelector {
    val select: Map[String, Rep[_]] //The runtime map between string names and table columns 
  }
  implicit class MultiSortableQuery[A <: ColumnSelector, B, C[_]](query: Query[A, B, C]) {
    def dynamicSortBy(sortBy: Seq[ColumnOrdering]): Query[A, B, C]  =
      sortBy.foldRight(query){ //Fold right is reversing order
        case ((sortColumn, sortOrder), queryToSort) =>
          val sortOrderRep: Rep[_] => Ordered = ColumnOrdered(_, Ordering(sortOrder))
          val sortColumnRep: A => Rep[_] = _.select(sortColumn)
          queryToSort.sortBy(sortColumnRep)(sortOrderRep)
      }
  }
}

并重新定义您的 Table 添加 "sorting map"

class ColorsTable(tag: Tag) extends Table[Color](tag, "color") with DynamicSortBySupport.ColumnSelector {
  def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
  def name = column[String]("name")
  def * = (id, name) <> ((Color.apply _).tupled, Color.unapply)
  val select = Map(
    "id" -> (this.id),
    "name" -> (this.name)
  )
}

最后在你的代码中使用整个东西:

object FindAll extends App {
  import DynamicSortBySupport._
  import slick.ast.Ordering.Direction
  import slick.ast.Ordering
  object colors extends TableQuery(new ColorsTable(_))
  val sortsBy = Seq[(String, Direction)](("name", Ordering.Desc), ("id", Ordering.Asc)) //Replaced 
  val db = Database.forURL("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver="org.h2.Driver") //Just for testing
  findAll(sortsBy)
  def findAll(sortsBy: Seq[(String, Direction)]): Future[Seq[Color]] = {
    val query = colors.dynamicSortBy(sortsBy).result
    db.run(query)
  }
}

注释和评论:

  • 字符串名称和 table 列之间的运行时映射 Map[String, Rep[_]] 可以通过错误处理(现在它只是抛出必须正确管理的运行时异常)或从 [=46 自动派生来改进=]定义本身;
  • 我已经把SortDirection换成了合适的slick.ast.Ordering.Direction,欢迎写个转换器;
  • 您还可以为可选过滤器编写一个帮助程序,例如 filterOption;
  • 注意使用foldRight反转排序顺序;
  • 如果可能,避免 vars 并发挥作用:-)
  • 如果你想 ColumnSelector 在数据层之外(实际上这是一个很好的做法)你可以重写它需要隐含的东西,比如 ColumnSelector[T];