Scala Slick and nested case classes - scala

I'm quite new to Scala and need to use slick to build a table mapping for these case classes.
I can do it for a simple case class but the nesting and option parameters are leaving me confused how to do this?
case class Info(fullName: Option[String], dn: Option[String], manager: Option[String], title: Option[String], group: Option[String], sid: Option[String])
case class User(username: String, RiskScore: Float, averageRiskScore: Float, lastSessionId: String, lastActivityTime: Long, info: Info)
I need to end up with a simple table which contains all of the combined parameters.

Given your nested case class definitions, a bidirectional mapping for the * projection similar to the following should work:
case class Info(fullName: Option[String], dn: Option[String], manager: Option[String], title: Option[String], group: Option[String], sid: Option[String])
case class User(username: String, riskScore: Float, averageRiskScore: Float, lastSessionId: String, lastActivityTime: Long, info: Info)
class Users(tag: Tag) extends Table[User](tag, "USERS") {
def username = column[String]("user_name")
def riskScore = column[Float]("risk_score")
def averageRiskScore = column[Float]("average_risk_score")
def lastSessionId = column[String]("last_session_id")
def lastActivityTime = column[Long]("last_acitivity_time")
def fullName = column[Option[String]]("full_name", O.Default(None))
def dn = column[Option[String]]("dn", O.Default(None))
def manager = column[Option[String]]("manager", O.Default(None))
def title = column[Option[String]]("title", O.Default(None))
def group = column[Option[String]]("group", O.Default(None))
def sid = column[Option[String]]("sid", O.Default(None))
def * = (
username, riskScore, averageRiskScore, lastSessionId, lastActivityTime, (
fullName, dn, manager, title, group, sid
)
).shaped <> (
{ case (username, riskScore, averageRiskScore, lastSessionId, lastActivityTime, info) =>
User(username, riskScore, averageRiskScore, lastSessionId, lastActivityTime, Info.tupled.apply(info))
},
{ u: User =>
def f(i: Info) = Info.unapply(i).get
Some((u.username, u.riskScore, u.averageRiskScore, u.lastSessionId, u.lastActivityTime, f(u.info)))
}
)
}
Here is a great relevant Slick article you might find useful.

Related

Type safe Scala Builder pattern with special rules

I'm trying to create a type safe builder of a case class , where its params can be of following types:
required
optional
required but mutually exclusive ->
a. ex. lets say I've 3 params: (param1), (param2, param3). If I have param1, I cannot set param2 or param3. If I can set both param2 and param3, but I cannot set param1
optional but mutually exclusive -> same logic as above but these are optional params. That is optionally I can set either param1 or (param2 and param3).
I figured how to get required and optional cases, but cannot get case 3 and 4.
Any ideas how to proceed.
These checks can be done at runtime, but I want these set of rules being implemented at compile time
case class Person(name: String, /*required*/
address: String, /*optional*/
city: String, /* reqd exclusive*/
county: String, /* reqd exclusive*/
state: String /* reqd exclusive*/,
ssn: String, /* optional exclusive*/
insurance: String, /* opt exclusive*/
passport: String /* opt exclusive*/)
// where (city) and (county, state) are required but are mutually exclusive
// (ssn) and (insurance, passport) are optional but are mutually exclusive.
// If I set passport, I've to set insurance
sealed trait PersonInfo
object PersonInfo {
sealed trait Empty extends PersonInfo
sealed trait Name extends PersonInfo
sealed trait Address extends PersonInfo
type Required = Empty with Name with Address
}
case class PersonBuilder[T <: PersonInfo]
(name: String = "", address: String = "", city: String = "", county: String = "",
state: String = "", ssn: String = "", insurance: String = "",passport: String ="") {
def withName(name: String): PersonBuilder[T with PersonInfo.Name] =
this.copy(name = name)
def withTask(address: String): PersonBuilder[T with PersonInfo.Address ] =
this.copy(address = address)
def withCity(city: String): PersonBuilder[T] =
this.copy(city = city)
def withCountry(county: String): PersonBuilder[T] =
this.copy(county = county)
def withState(state: String): PersonBuilder[T] =
this.copy(state = state)
def withSsn(ssn: String): PersonBuilder[T] =
this.copy(ssn = ssn)
def withInsurance(insurance: String): PersonBuilder[T] =
this.copy(insurance = insurance)
def withPassport(passport: String): PersonBuilder[T] =
this.copy(passport = passport)
def build(implicit ev: T =:= PersonInfo.Required): Person =
Person(name, address, city, county, state, ssn, insurance, passport)
}
here's the build
val testPerson = PersonBuilder[PersonInfo.Empty]()
.withName("foo")
.withSsn("bar")
As mentioned in a comment, if creating a builder is not a hard requirement, a viable choice could be to make those requirements explicit in the types, using sum types for exclusive choices and Options for optional ones, as in the following example:
sealed abstract class Location extends Product with Serializable {
def value: String
}
object Location {
final case class City(value: String) extends Location
final case class County(value: String) extends Location
final case class State(value: String) extends Location
}
sealed abstract class Identity extends Product with Serializable {
def value: String
}
object Identity {
final case class Ssn(value: String) extends Identity
final case class Insurance(value: String) extends Identity
final case class Passport(value: String) extends Identity
}
final case class Person(
name: String,
address: Option[String],
location: Location,
identity: Option[Identity],
)
Scala 3 further introduced enums which makes the definition more compact and readable:
enum Location(value: String) {
case City(value: String) extends Location(value)
case County(value: String) extends Location(value)
case State(value: String) extends Location(value)
}
enum Identity(value: String) {
case Ssn(value: String) extends Identity(value)
case Insurance(value: String) extends Identity(value)
case Passport(value: String) extends Identity(value)
}
final case class Person(
name: String,
address: Option[String],
location: Location,
identity: Option[Identity],
)
And making Options default to None you get a very similar experience to custom-made builders without any additional code:
final case class Person(
name: String,
location: Location,
address: Option[String] = None,
identity: Option[Identity] = None,
)
Person("Alice", Location.City("New York"))
.copy(identity = Some(Identity.Ssn("123456")))
Which you can further refine very easily:
final case class Person(
name: String,
location: Location,
address: Option[String] = None,
identity: Option[Identity] = None
) {
def withAddress(address: String): Person =
this.copy(address = Some(address))
def withIdentity(identity: Identity): Person =
this.copy(identity = Some(identity))
}
Person("Alice", Location.City("New York")).withIdentity(Identity.Ssn("123456"))
You can play around with this code here on Scastie.

Get field names from Scala case class with specific annotation

final class ContactInfo extends StaticAnnotation{}
case class Person(val id: String,
val firstName: String,
val lastName: String,
#ContactInfo val phoneNumbers: Seq[String],
#ContactInfo val email: String)
def getContactInfoFields[T: TypeTag]: Seq[String] = {
???
}
Expected output getContactInfoFields[Person] = ("phoneNumbers", "email")
Riffing off the answer to a similar question on SO I've tried
def getContactInfoFields[T: TypeTag]: Seq[String] = {
val fields = typeOf[T].members.collect{ case s: TermSymbol => s }.
filter(s => s.isVal || s.isVar)
fields.filter(_.annotations.exists(_.isInstanceOf[ContactInfo]))
.map(x=>x.name.toString).toSeq
}
However in practice this is returning an empty sequence. What am I missing?
You could represent this information at the type level.
sealed trait ContactInfo
case class PhoneNumbers(numbers: Seq[String]) extends ContactInfo
case class Email(email: String) extends ContactInfo
case class Person(id: String, firstName: String, lastName: String, phoneNumbers: PhoneNumbers, email: Email)
def contactInfo[T: TypeTag] = typeOf[T].members.filter(!_.isMethod).map(_.typeSignature).collect {
case t if t <:< typeOf[ContactInfo] => t.typeSymbol.name.toString
}
Calling contactInfo[Person] returns Iterable[String] = List(Email, PhoneNumbers)
Thank you everyone for all your help! I've managed to come up with a working solution. As it turns out I was trying to compare JavaUniverse.reflection.Types to Scala Universe reflections types which is why the filter statement was failing. The below function returns as expected
def listContactInfoFields(symbol: TypeSymbol): Seq[String] = {
val terms = symbol.asClass.primaryConstructor.typeSignature.paramLists.head
val annotatedFields = terms.filter(_.annotations.exists(_.tree.tpe =:= typeOf[ContactInfo]))
annotatedFields.map(_.name.toString)
}

Slick mapped tables using traits throws exception

I'm stuck trying to define mapped tables using traits in slick 3.1.0. Since there is nothing mentioned in the official docs, I'm not even sure whether this is possible or not. Here is what I have so far:
Table definition:
class PersonTable(tag: Tag) extends Table[PersonModel](tag, "person") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def firstName = column[String]("first_name", O.Length(PersonDb.FirstNameColumnLength))
def lastName = column[String]("last_name", O.Length(PersonDb.LastNameColumnLength))
def * = (id.?, firstName, lastName) <> (PersonModelImpl.tupled, PersonModelImpl.unapply _)
}
PersonModel:
trait PersonModel {
def id: Option[Int]
def firstName: String
def lastName: String
}
PersonModelImpl:
case class PersonModelImpl(
override val id: Option[Int],
override val firstName: String,
override val lastName: String)
extends PersonModel
Compiling the code above causes an error:
Compilation error[type mismatch;
found : slick.lifted.MappedProjection[models.PersonModelImpl,(Option[Int], String, String]
required: slick.lifted.ProvenShape[models.PersonModel]]
However changing ...extends Table[PersonModel]... to ...extends Table[PersonModelImpl]... in the table definition works flawlessly.
So basically my question is:
Is it possible to use traits as TableElementType in mapped tables?
If yes, what am I doing wrong?
Answers:
Yes, but it requires having the correct projection function (*).
You have the wrong type on *. In order for the implicit to resolve to the right MappedProjection, the types have to match exactly.
You should be able to resolve both by doing:
def * = {
val applyAsPersonModel: (Option[Int], String, String) => PersonModel =
(PersonModelImpl.apply _)
val unapplyAsPersonModel: PersonModel => Option[(Option[Int], String, String)] =
{
// TODO: Handle any other subclasses of PersonModel you expect.
case personImpl: PersonModelImpl => PersonModelImpl.unapply(personImpl)
}
(id.?, firstName, lastName) <> (applyAsPersonModel.tupled, unapplyAsPersonModel)
}
Note the TODO. You'll get an exception if you try to insert any non-PersonModelImpl instances unless you add additional case statements to that partial function. Alternatively, you can just create a new PersonModelImpl instance to pass to unapply:
val unapplyAsPersonModel: PersonModel => Option[(Option[Int], String, String)] =
{ person: PersonModel =>
PersonModelImpl.unapply(
PersonModelImpl(person.id, person.firstName, person.lastName)
)
}

Scala Type Erasure on case classes how to solve

I have a trait and two case classes that extends it:
trait Authenticatable {
val email: String
val pass: String
val id: Long
val sessionid: String
}
case class Admin(
id: Long,
email: String,
pass: String,
sessionid: Option[String] = None) extends Authenticatable
case class Client(
id: Long,
email: String,
pass: String,
sessionid: Option[String] = None) extends Authenticatable
And I have functions witch should authenticate user, make copy of object with new sessionid and return it.
def auth(email: String, password: String): Try[Admin] ={
checkPass(models.Admin.findBy(sqls"email = $email"), password)
}
def auth(email: String, password: String, customer: Customer): Try[Customer] ={
checkPass(models.Customer.findBy(sqls"email = $email"), password)
}
private def checkPass (model: Option[Authenticatable], password: String): Try[Authenticatable]={
model match {
case Some(m) => check(password, m.pass).map(_ => m)
case _ => Failure(new Exception("Authentication failure!"))
}
}
The problem is: I can't make copy of object in auth function because function checkPass returns Authenticatable and not Client or Admin class and Authenticatable doesn't have copy method of case classes.
What is the right way to solve this problem?
If you use type parameters, you can avoid throwing away the information that checkPass will always return the same type of Authenticable as was given to it:
private def checkPass[A <: Authenticatable](model: Option[A], password: String): Try[A] =
// exactly the same body here
This means that in auth you can have e.g.:
def auth(email: String, password: String): Try[Admin] =
checkPass(models.Admin.findBy(sqls"email = $email"), password)
.map(_.copy(sessionid = Some("whatever")))
I would propose to add an abstract method to Authenticable which sets session ID and which is implemented by individual case classes by using of the generated copy method.
trait Authenticable {
def email: String
def pass: String
def id: Long
def sessionid: Option[String]
def setSessionId(id: String): Authenticable
}
case class Admin(
id: Long,
email: String,
pass: String,
sessionid: Option[String] = None) extends Authenticable {
def setSessionId(id: String) = copy(sessionid = Some(id))
}
case class Client(
id: Long,
email: String,
pass: String,
sessionid: Option[String] = None) extends Authenticable {
def setSessionId(id: String) = copy(sessionid = Some(id))
}

In Slick, how to implement multiple projections for a Table?

Here are the codes (I'm using Slick 2.1):
case class UserRecord(id: Long,
mID: String,
userName: Option[String],
firstName: Option[String],
lastName: Option[String],
fullName: Option[String],
email: String,
avatarUrl: Option[String],
createTime: Timestamp,
updateTime: Timestamp,
status: Int,
socialProviders: Int)
case class UserProfile (
userName: String,
firstName: Option[String],
lastName: Option[String],
fullName: Option[String],
email: String,
avatarURL: Option[String])
class UserTable(tag: Tag) extends Table[UserRecord](tag, "User") {
def id = column[Long]("id", O.AutoInc)
def mID = column[String]("mID", O.PrimaryKey)
def username = column[Option[String]]("username")
def firstName = column[Option[String]]("firstname")
def lastName = column[Option[String]]("lastname")
def fullName = column[Option[String]]("fullname")
def email = column[String]("email")
def avatarUrl = column[Option[String]]("avataurl")
def createTime = column[Timestamp]("createTime")
def updateTime = column[Timestamp]("updateTime")
def status = column[Int]("status")
def socialProviders = column[Int]("socialProviders")
def * = (id, mID, username, firstName, lastName, fullName,
email, avatarUrl, createTime, updateTime, status, socialProviders) <>(UserRecord.tupled, UserRecord.unapply _)
def profile = (username, firstName, lastName, fullName, email, avatarUrl) <> (UserProfile.tupled, UserProfile.unapply _)
}
I tried to create two mappings * and profile in the Table class, however, Slick complains about this:
[error] Slick does not know how to map the given types.
[error] Possible causes: T in Table[T] does not match your * projection. Or you use an unsupported type in a Query (e.g. scala List).
[error] Required level: scala.slick.lifted.FlatShapeLevel
[error] Source type: (scala.slick.lifted.Column[Option[String]], scala.slick.lifted.Column[Option[String]], scala.slick.lifted.Column[Option[String]], scala.slick.lifted.Column[Option[String]], scala.slick.lifted.Column[String], scala.slick.lifted.Column[Option[String]])
[error] Unpacked type: (String, Option[String], Option[String], Option[String], String, Option[String])
[error] Packed type: Any
[error] def profile = (username, firstName, lastName, fullName, email, avatarUrl) <> (UserProfile.tupled, UserProfile.unapply _)
[error] ^
I saw a blog about this, but it looks quite complex and has no explanation..
Does anyone have ideas about this? Thanks!
Slick expects Username to be Option[String] in the 'profile' projection
case class UserProfile (
userName: String, // Here
firstName: Option[String],
lastName: Option[String],
fullName: Option[String],
email: String,
avatarURL: Option[String])
Clearly Slick here doesn't know how to map the columns with the case class .