3

I'm trying to convert anorm queries to slick in one of Play 2.3 samples, but I'm not sure how to implement dynamic sorting.

This is the original method:

def list(page: Int = 0, pageSize: Int = 10, orderBy: Int = 1, filter: String = "%"): Page[(Computer, Option[Company])] = {

    val offest = pageSize * page

    DB.withConnection { implicit connection =>

        val computers = SQL(
        """
          select * from computer 
          left join company on computer.company_id = company.id
          where computer.name like {filter}
          order by {orderBy} nulls last
          limit {pageSize} offset {offset}
        """
        ).on(
            'pageSize -> pageSize,
            'offset -> offest,
            'filter -> filter,
            'orderBy -> orderBy
        ).as(Computer.withCompany *)

        val totalRows = SQL(
        """
          select count(*) from computer 
          left join company on computer.company_id = company.id
          where computer.name like {filter}
        """
        ).on(
            'filter -> filter
        ).as(scalar[Long].single)

        Page(computers, page, offest, totalRows)

    }

}

So far I've got this far with the first query:

val computers_ = (for {
    (computer, company) <- Computer.where(_.name like filter) leftJoin
        Company on (_.companyId === _.id)
} yield (computer, company.?)).list

How do I do the "order by" part in slick, bearing in mind it's a column name passed to the method dynamically as a parameter?

Scala 2.10.4 / Play 2.3 / Slick 2.0.2

Table classes generated by Slick code generator below:

package tables
// AUTO-GENERATED Slick data model
/** Stand-alone Slick data model for immediate use */
object Tables extends {
  val profile = scala.slick.driver.H2Driver
} with Tables

/** Slick data model trait for extension, choice of backend or usage in the cake pattern. (Make sure to initialize this late.) */
trait Tables {
  val profile: scala.slick.driver.JdbcProfile
  import profile.simple._
  import scala.slick.model.ForeignKeyAction
  // NOTE: GetResult mappers for plain SQL are only generated for tables where Slick knows how to map the types of all columns.
  import scala.slick.jdbc.{GetResult => GR}

  /** DDL for all tables. Call .create to execute. */
  lazy val ddl = Company.ddl ++ Computer.ddl

  /** Entity class storing rows of table Company
   *  @param id Database column ID PrimaryKey
   *  @param name Database column NAME  */
  case class CompanyRow(id: Long, name: String)
  /** GetResult implicit for fetching CompanyRow objects using plain SQL queries */
  implicit def GetResultCompanyRow(implicit e0: GR[Long], e1: GR[String]): GR[CompanyRow] = GR{
    prs => import prs._
    CompanyRow.tupled((<<[Long], <<[String]))
  }
  /** Table description of table COMPANY. Objects of this class serve as prototypes for rows in queries. */
  class Company(tag: Tag) extends Table[CompanyRow](tag, "COMPANY") {
    def * = (id, name) <> (CompanyRow.tupled, CompanyRow.unapply)
    /** Maps whole row to an option. Useful for outer joins. */
    def ? = (id.?, name.?).shaped.<>({r=>import r._; _1.map(_=> CompanyRow.tupled((_1.get, _2.get)))}, (_:Any) =>  throw new Exception("Inserting into ? projection not supported."))

    /** Database column ID PrimaryKey */
    val id: Column[Long] = column[Long]("ID", O.PrimaryKey)
    /** Database column NAME  */
    val name: Column[String] = column[String]("NAME")
  }
  /** Collection-like TableQuery object for table Company */
  lazy val Company = new TableQuery(tag => new Company(tag))

  /** Entity class storing rows of table Computer
   *  @param id Database column ID PrimaryKey
   *  @param name Database column NAME 
   *  @param introduced Database column INTRODUCED 
   *  @param discontinued Database column DISCONTINUED 
   *  @param companyId Database column COMPANY_ID  */
  case class ComputerRow(id: Long, name: String, introduced: Option[java.sql.Timestamp], discontinued: Option[java.sql.Timestamp], companyId: Option[Long])
  /** GetResult implicit for fetching ComputerRow objects using plain SQL queries */
  implicit def GetResultComputerRow(implicit e0: GR[Long], e1: GR[String], e2: GR[Option[java.sql.Timestamp]], e3: GR[Option[Long]]): GR[ComputerRow] = GR{
    prs => import prs._
    ComputerRow.tupled((<<[Long], <<[String], <<?[java.sql.Timestamp], <<?[java.sql.Timestamp], <<?[Long]))
  }
  /** Table description of table COMPUTER. Objects of this class serve as prototypes for rows in queries. */
  class Computer(tag: Tag) extends Table[ComputerRow](tag, "COMPUTER") {
    def * = (id, name, introduced, discontinued, companyId) <> (ComputerRow.tupled, ComputerRow.unapply)
    /** Maps whole row to an option. Useful for outer joins. */
    def ? = (id.?, name.?, introduced, discontinued, companyId).shaped.<>({r=>import r._; _1.map(_=> ComputerRow.tupled((_1.get, _2.get, _3, _4, _5)))}, (_:Any) =>  throw new Exception("Inserting into ? projection not supported."))

    /** Database column ID PrimaryKey */
    val id: Column[Long] = column[Long]("ID", O.PrimaryKey)
    /** Database column NAME  */
    val name: Column[String] = column[String]("NAME")
    /** Database column INTRODUCED  */
    val introduced: Column[Option[java.sql.Timestamp]] = column[Option[java.sql.Timestamp]]("INTRODUCED")
    /** Database column DISCONTINUED  */
    val discontinued: Column[Option[java.sql.Timestamp]] = column[Option[java.sql.Timestamp]]("DISCONTINUED")
    /** Database column COMPANY_ID  */
    val companyId: Column[Option[Long]] = column[Option[Long]]("COMPANY_ID")

    /** Foreign key referencing Company (database name FK_COMPUTER_COMPANY_1) */
    lazy val companyFk = foreignKey("FK_COMPUTER_COMPANY_1", companyId, Company)(r => r.id, onUpdate=ForeignKeyAction.Restrict, onDelete=ForeignKeyAction.Restrict)
  }
  /** Collection-like TableQuery object for table Computer */
  lazy val Computer = new TableQuery(tag => new Computer(tag))
}

UPDATE - SOLUTION The final solution is in this question.

Community
  • 1
  • 1
Caballero
  • 11,546
  • 22
  • 103
  • 163

3 Answers3

3

My first answer plugs in the sorting function at the right place, but quickly grows complicated because of Slick's complicated typing. You can avoid these typing issues by using Slick's query composition to modify the query directly based on the desired ordering.

def list(page: Int = 0, pageSize: Int = 10, orderBy: Int = 1, filter: String = "%") = {
   //..
   val q = for {
     (computer, company) <- Computer.where(_.name like filter) leftJoin
                            Company on (_.companyId === _.id)
   } yield (computer, company.?)

   val sortedQ = orderBy match {
     case 1 => q.sortBy(_._1.id)
     case 2 => q.sortBy(_._1.description)
     // Others
   }

   val pagedQ = sortedQ.drop(page * pageSize).take(pageSize)

   pagedQ.list
}
DCKing
  • 4,253
  • 2
  • 28
  • 43
  • Thanks for your effort, it's really close now, but... if I leave `yield (computer, company)` - company not optional, I'm getting `SlickException: Read NULL value for ResultSet column Path` - this is because of left join company sometimes results in nulls. If I leave company optional `yield (computer, company.?)`, `case 3 => c.sortBy(_._2.name)`, which is referring to a column in company is giving me `Cannot resolve symbol name`, I'm guessing because company at this point is optional... – Caballero Jun 12 '14 at 15:49
  • @Caballero Yeah it's getting ridiculous! I don't actually know how to overcome that final problem... Perhaps that's good material for a different SO question? – DCKing Jun 12 '14 at 16:09
  • Thanks for you help so far. Yes, this is super annoying. I hope somebody from Slick team will pitch in eventually. – Caballero Jun 12 '14 at 16:43
  • @Caballero If you want that, I *really* recommend starting a new question named something like "how to sort optional columns" or something like that. Other people are much likelier to answer a question that is 1) more focused and 2) doesn't have any answers yet. – DCKing Jun 12 '14 at 17:11
  • Done that: http://stackoverflow.com/questions/24190955/slick-dynamic-sortby-in-a-query-with-left-join – Caballero Jun 12 '14 at 17:59
1

The difference between Slick and Anorm is that Slick's queries are checked by the Scala compiler. Implementing such a dynamic parameter takes a bit more effort in Slick, but you get type safety in return. It is made particularly cumbersome to do in this case since your query ordering is a join of multiple tables.

In general, it should look roughly like this:

def orderings(code: Int): ((Computer, Company)) => Column[_] = {
   code match {
      case 1 => _._1.id
      case 2 => _._1.description
      // Other orderings
   }
)

def list(page: Int = 0, pageSize: Int = 10, orderBy: Int = 1, filter: String = "%") = {
   //..
   val computers_ = (for {
     (computer, company) <- Computer.where(_.name like filter) leftJoin
                            Company on (_.companyId === _.id)
   } yield (computer, company.?))
   .sortBy(orderings(orderBy).nullsLast)
   .drop(page * pageSize)
   .take(pageSize)
   .list
   //..

}

The general idea to map the integers you receive to the Slick columns on which you want to sort is the answer to your question.

DCKing
  • 4,253
  • 2
  • 28
  • 43
  • Thanks, this looks like the right direction, however `1 -> _._1.id` throws an error `missing parameter type for expanded function ((x$4) => 1.$minus$greater(x$4._1.id))`. Any ideas? – Caballero Jun 12 '14 at 10:57
  • Perhaps try `1 -> q => q._1.id`? If that doesn't work, try not using a map but a pattern `match` to accomplish the same. Unfortunately Slick's types are quite complicated, so you have to fiddle around a bit to make it work. – DCKing Jun 12 '14 at 12:25
  • No this doesn't work, and I have no idea how or where to use match in this situation. I've tried every possible combination, down to `1 -> (q => q.map(n => n._1.name))` which still returns `Query[Column[String], String]` instead of `Column[_]`. This looks like a dead end... – Caballero Jun 12 '14 at 12:50
  • The problem is the Map signature, it should be (Computer, Company) => Column[_]. Sort takes a function from tuples of tables to tuple of column, not from Query to Column. – pedrofurla Jun 12 '14 at 14:25
  • @pedrofurla Is this what you mean - `Map[Int, (Computer, Company) => Column[_]]`? If so, it doesn't work either. – Caballero Jun 12 '14 at 14:37
  • @Caballero I had access to a Slick codebase and could check the exact types. My updated answer should work. – DCKing Jun 12 '14 at 14:55
  • @DCKing thanks, it's going further now. It doesn't compile with `case 1 => _._1.id` but it compiles with `case 1 => (a, b) => a.id`, is this the same? The next error however is `.sortBy(orderings(orderBy).nullsLast)`, it says `cannot resolve symbol nullsLast`. When I remove nullsLast, the error is `type mismatch; found : (models.Computer, models.Company) => Column[_] required: ((Computer, scala.slick.lifted.MappedProjection[Option[CompanyRow],(Option[Long], Option[String])])) => ?` – Caballero Jun 12 '14 at 15:08
  • @Caballero: About the first error: strange, that works for me. Are you sure you've not made typos? About the second error: this is apparently due to the fact that you're lifting the `Company` to an option. This is becoming too complicated: I'll write you a different answer. – DCKing Jun 12 '14 at 15:32
1

Not sure if this is the best idea in the world but you could technically use shapeless to help you get a numbered tuple element, this will obviously be at the cost of compile-time safety. First convert the Company case class into a tuple with Company.unapply and then use shapeless's at(N) method (note that it's a 0-based index). Here's what that would look like:

def list(page: Int = 0, pageSize: Int = 10, orderBy: Int = 1, filter: String = "%") = {
   //..
   val computers_ = (for {
     (computer, company) <- Computer.where(_.name like filter) leftJoin
                            Company on (_.companyId === _.id)
   } yield (computer, company.?))
   .sortBy(Company.unapply(_._1).get.at(orderBy-1).nullsLast)
   .drop(page * pageSize)
   .take(pageSize)
   .list
   //..
}

In order to do this you will need shapeless:

<dependency>
    <groupId>com.chuusai</groupId>
    <artifactId>shapeless_2.11</artifactId>
    <version>2.3.1</version>
</dependency>

...and the following import:

import shapeless.syntax.std.tuple._

Use this technique at your own risk.