10

I'm teaching myself some Scala and am currently getting my feet wet with slick (3.1) + play framework, so maybe the answer is simple here and I'm missing something obvious. I have the following model and Table

case class User(id: Long = -1,
                username: String,
                passwordHash: String,
                email: Option[String] = None) 

class Users(tag: Tag) extends Table[User](tag, "USERS") {
    def id = column[Long]("ID", O.PrimaryKey, O.AutoInc)
    def username = column[String]("USERNAME")
    def email = column[Option[String]]("EMAIL")
    def passwordHash = column[String]("PASSWD_HASH")
    def * = (id, username, passwordHash, email) <>((User.apply _).tupled, User.unapply)
  }

Now this above works just fine as it is, but I'd like to add some fields to the User case class that aren't saved in the USER table, namely permissions and roles, like this:

case class User(id: Long = -1,
                username: String,
                passwordHash: String,
                email: Option[String] = None,
                permissions: Seq[String] = Seq.empty,
                roles: Seq[String] = Seq.empty) 

Those are supposed to go into their own respective tables as userid -> permission/role mappings (simple one to many relationships).

Ultimately those should be queried too, but for now I'd just like to ignore the additional fields (purely as an exercise). How do I adjust the original projection in the table to omit/ignore those new fields? Obviously the original mapping

 def * = (id, username, passwordHash, email) <>((User.apply _).tupled, User.unapply)

doesn't work anymore since the touple doesnt match the case class. As far as I can tell it shouldnt be too hard since <> just takes two functions that convert from a touple to a User instance and vice versa and those functions should just ignore the new fields (or fill them with their default values). But I can't figure out how to express that.

I tried adding a new apply() with a shorter signature to the User companion object, but then I get an error that basically tells me that slick doesnt know which apply() to use. Makes sense, but I dont know how to reference one or the other. I did the same thing with an additional constructor for User, the result is the same problem. And I tried writing basic conversion functions like this:

class Users(tag: Tag) extends Table[User](tag, "USERS") {

    def constructUser = (id: Long, username: String, passwordHash: String, email: Option[String]) =>
      User(id, username, passwordHash, email)

    def extractUser = (user: User) => user match {
      case User(id, username, passwordHash, email, permissions, roles) =>
        Some((id, username, passwordHash, email))
    }

    def id = column[Long]("ID", O.PrimaryKey, O.AutoInc)
    def username = column[String]("USERNAME")
    def email = column[Option[String]]("EMAIL")
    def passwordHash = column[String]("PASSWD_HASH")
    def * = (id, username, passwordHash, email) <>(constructUser, extractUser)
  }

Sadly that also leads to an error:

[error]  found   : (Long, String, String, Option[String]) => models.User
[error]  required: ? => ?
[error]     def * = (id, username, passwordHash, email) <>(constructUser, deconstructUser)
user5504273
  • 128
  • 1
  • 6

1 Answers1

21

Your trouble here is your constructUser is function of 4 parameters, while it supposed to be function of single Tuple4

Try to add type signatures.
This sample works for instance ( some simplifications included )

type Data = (Long, String, String, Option[String])

def constructUser: Data => User = {
  case (id, username, passwordHash, email) => User(id, username, passwordHash, email)

}
def extractUser: PartialFunction[User, Data] = {
  case User(id, username, passwordHash, email, _, _) =>
    (id, username, passwordHash, email)
}

def * = (id, username, passwordHash, email) <> (constructUser, extractUser.lift)
Odomontois
  • 15,918
  • 2
  • 36
  • 71
  • 2
    That works and makes perfect sense. Thank you. Sadly I can't upvote yet. :) – user5504273 Oct 29 '15 at 22:13
  • @Odomontois Thanks. The answer works for me too in the above scenario(i.e - add some fields to the User case class that aren't saved in the USER table). But In my case(For example), I have added the another table as a field in the USER table. Instead of "permissions, roles" I have added "accessToken: Option[AccessToken] = None" and "oauthClient: Option[Client] = None". But I want to map those values. How Do I do? Can you please check the below question - http://stackoverflow.com/q/37183156/1584121 – SKK May 13 '16 at 09:58
  • @Odomontois, thank you, this was very helpful! I just wanted to add that this strategy (well, you'll have to change this code a little bit) can be used to eliminate the mismatch between case classes and tables the other way around (that's what I did): if your case classes have fewer fields than rows in the table (for instance, if you don't have an id in your case class). All other techniques I googled were much harder. Thanks again! – George Zorikov Dec 28 '20 at 11:27