4
votes

I have an idea how my data access layer with Scala Slick should look like, but I'm not sure if it's really possible.

Let's assume I have a User table which has the usual fields like id, email, password, etc.

  object Users extends Table[(String, String, Option[String], Boolean)]("User") {
    def id = column[String]("id", O.PrimaryKey)
    def email = column[String]("email")
    def password = column[String]("password")
    def active = column[Boolean]("active")
    def * = id ~ email ~ password.? ~ active
  }

And I wish to query them in different ways, currently the ugly way is to have a new database session, do the for comprehension and then do different if statements to achieve what I want.

e.g.

  def getUser(email: String, password: String): Option[User] = {
    database withSession { implicit session: Session =>
      val queryUser = (for {
        user <- Users
          if user.email === email &&
             user.password === password &&
             user.active === true
      } //yield and map to user class, etc...
  }

  def getUser(identifier: String): Option[User] = {
    database withSession { implicit session: Session =>
      val queryUser = (for {
        user <- Users
        if user.id === identifier &&
           user.active === true
      } //yield and map to user class, etc...
  }

What I would prefer is to have a private method for the query and then public methods which define queries along the lines of

type UserQuery = User => Boolean

private def getUserByQuery(whereQuery: UserQuery): Option[User] = {
  database withSession { implicit session: Session =>
      val queryUser = (for {
        user <- Users
          somehow run whereQuery here to filter
      } // yield and boring stuff
  }

def getUserByEmailAndPassword(email, pass){ ... define by query and call getUserByQuery ...}

getUserById(id){….}

getUserByFoo{….} 

That way, the query logic is encapsulated in the relevant public functions and the actual querying and mapping to the user object is in a reusable function that other people dont need to be concerned with.

The problem I have is trying to refactor the "where" bit's into functions that I can pass around. Trying to do things like select them in intellij and using the refactoring results in some pretty crazy typing going on.

Does anyone have any examples they could show of doing close to what I am trying to achieve?

2

2 Answers

6
votes

1) wrapping queries in a def means the query statement is re-generated on every single request, and, since query params are not bound, no prepared statement is passed to the underlying DBMS.

2) you're not taking advantage of composition

Instead, if you define parameterized query vals that def query wrappers call, you can get the best of both worlds.

val uBase = for{
  u  <- Users
  ur <- UserRoles if u.id is ur.UserID
} yield (u,ur)

// composition: generates prepared statement one time, on startup
val byRole = for{ roleGroup <- Parameters[String]
  (u,ur) <- uBase
  r <- Roles if(r.roleGroup is roleGroup) && (r.id is ur.roleID)
} yield u

def findByRole(roleGroup: RoleGroup): List[User] = {
  db withSession { implicit ss:SS=>
    byRole(roleGroup.toString).list
  }
}

If you need one-off finders for a single property, use:

val byBar = Foo.createFinderBy(_.bar)
val byBaz = Foo.createFinderBy(_.baz)

Can't remember where, maybe on SO, or Slick user group, but I did see a very creative solution that allowed for multiple bound params, basically a createFinderBy on steroids. Not so useful to me though, as the solution was limited to a single mapper/table object.

At any rate composing for comprehensions seems to do what you're trying to do.

0
votes

I have recently done something similar, one way to do this could be following, write a general select method which takes a predicate

def select(where: Users.type => Column[Boolean]): Option[User] = {
    database withSession { implicit session: Session =>
      val queryUser = (for {
        user <- Users where(user)

      } //yield and map to user class, etc...
  }

and then write the method which passes the actual predicate as a higher order function

def getUserByEmail(email:String):Option[User]={
      select((u: Users.type) => u.*._2 === email)
}
similarly 

def getActiveUserByEmail(email:String):Option[User]={
     select((u: Users.type) => u.*._2 === email && u.*._4 === true)
}