简体   繁体   中英

Slick: LIKE query with column as parameter

I have the following SQL query, which I'd like to map to Slick

SELECT * FROM   rates
WHERE  '3113212512' LIKE (prefix || '%') and  present = true
ORDER  BY prefix DESC
LIMIT  1;

However the like symbol is not defined for String:

case class Rate(grid: String, prefix: String, present: Boolean)

class Rates(tag: Tag) extends Table[Rate](tag, "rates") {
  def grid = column[String]("grid", O.PrimaryKey, O.NotNull)
  def prefix = column[String]("prefix", O.NotNull)
  def present = column[Boolean]("present", O.NotNull)

  // Foreign keys
  def ratePlan = foreignKey("rate_plan_fk", ratePlanId, RatePlans)(_.grid)
  def ratePlanId = column[String]("rate_plan_id", O.NotNull)

  def * = (grid, prefix, present) <>(Rate.tupled, Rate.unapply)
}

object Rates extends TableQuery(new Rates(_)) {
  def findActiveRateByRatePlanAndPartialPrefix(ratePlan: String, prefix: String) = {
    DB withSession {
      implicit session: Session =>
        Rates.filter(_.ratePlanId === ratePlan)
          .filter(_.present === true)
          .filter(prefix like _.prefix)
          .sortBy(_.prefix.desc).firstOption
    }
  }
}

Obviously it's logical that something like this won't work:

.filter(prefix like _.prefix)

And:

.filter(_.prefix like prefix)

Will render incorrect SQL, and I'm not even considering the '%' right now (only WHERE clause concerning the prefix:

(x2."prefix" like '3113212512')

Instead of:

'3113212512' LIKE (prefix || '%')

Of course I can solve this using a static query, but I'd like to know whether this would be possible at all?

For clarity, here are some prefixes in the database

31
3113
312532623
31113212

And 31113212 is expected as a result

You could simply rewrite your query in a style that is supported. I think this should be equivalent:

.filter(s => (s.prefix === "") || s.prefix.isEmpty || LiteralColumn(prefix) like s.prefix)

If the conditions are more complex and you need a Case expression, see http://slick.typesafe.com/doc/2.1.0/sql-to-slick.html#case

If you still want the || operator you may be able to define it this way:

/** SQL || for String (currently collides with Column[Boolean] || so other name) */
val thisOrThat = SimpleBinaryOperator[String]("||") 

...

.filter(s => LiteralColumn(prefix) like thisOrThat(s.prefix,"%"))

I didn't try this though and I saw we don't have a test for SimpleBinaryOperator. Open a ticket on github.com/slick/slick if it doesn't work. Probably we should also change it to return a typed function. I added a ticket for that https://github.com/slick/slick/issues/1073 .

Also see http://slick.typesafe.com/doc/2.1.0/userdefined.html#scalar-database-functions

Luckily SimpleBinaryOperator is not even needed here.

UPDATE 2

One actually does not require the concat, as it can be written as follows as well:

filter(s => LiteralColumn(prefix) like (s.prefix ++ "%"))

UPDATE:

After hints by cvogt this is the final result without using internal Slick APIs:

        val concat = SimpleBinaryOperator[String]("||")

        Rates.filter(_.ratePlanId === ratePlan)
          .filter(_.present === true)
          .filter(s => LiteralColumn(prefix) like concat(s.prefix, "%"))
          .sortBy(_.prefix.desc).firstOption

Which perfectly gives:

and ('3113212512' like (x2."prefix" || '%'))

Alternative:

This seems to possible. Note however that the following method uses the Internal API of Slick and may not be compatible in the future due to API changes (see comment by cvogt) I used Slick 2.1.0.

Basically I created an extension to Slick in which I define a custom scheme to implement the required result:

package utils

import scala.language.{higherKinds, implicitConversions}
import scala.slick.ast.ScalaBaseType._
import scala.slick.ast._
import scala.slick.lifted.{Column, ExtensionMethods}

object Helpers {
  implicit class PrefixExtensionMethods[P1](val c: scala.slick.lifted.Column[P1]) extends AnyVal with ExtensionMethods[String, P1] {
    def subPrefixFor[P2, R](prefix: Column[P2])(implicit om: o#arg[String, P2]#to[Boolean, R]) = {
      om.column(Library.Like, prefix.toNode, om.column(new Library.SqlOperator("||"), n, LiteralNode("%")).toNode)
    }
  }
}

This can now be used as follows:

import utils.Helpers._

[...]

  def findActiveRateByRatePlanAndPartialPrefix(ratePlan: String, prefix: String) = {
    DB withSession {
      implicit session: Session =>
        Rates.filter(_.ratePlanId === ratePlan)
          .filter(_.present === true)
          .filter(_.prefix subPrefixFor prefix)
          .sortBy(_.prefix.desc).firstOption
    }
  }

And will render correct result:

and ('3113212512' like (x2."prefix" || '%'))

Notes:

  1. I could probably have some better naming
  2. Had to chain the || and Like using om.column
  3. I introduced new SqlOperator since Or operator renders or and I need concatenation in Postgres ||

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM