Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom mapping to nested case class structure in Slick (more than 22 columns)

Tags:

scala

slick

I'm trying to map a DB row with more than 22 columns to a case class tree. I'd rather not using HList as I don't want to work with that API, and also for some exponential compilation time feedbacks that I've read somewhere...

I have read this thread answered by Stefan Zeiger: How can I handle a > 22 column table with Slick using nested tuples or HLists?

I've seen this test which shows how to define a custom mapping function and I'd like to do that:

https://github.com/slick/slick/blob/2.1/slick-testkit/src/main/scala/com/typesafe/slick/testkit/tests/JdbcMapperTest.scala#L129-141

def * = (
        id,
        (p1i1, p1i2, p1i3, p1i4, p1i5, p1i6),
        (p2i1, p2i2, p2i3, p2i4, p2i5, p2i6),
        (p3i1, p3i2, p3i3, p3i4, p3i5, p3i6),
        (p4i1, p4i2, p4i3, p4i4, p4i5, p4i6)
      ).shaped <> ({ case (id, p1, p2, p3, p4) =>
        // We could do this without .shaped but then we'd have to write a type annotation for the parameters
        Whole(id, Part.tupled.apply(p1), Part.tupled.apply(p2), Part.tupled.apply(p3), Part.tupled.apply(p4))
      }, { w: Whole =>
        def f(p: Part) = Part.unapply(p).get
        Some((w.id, f(w.p1), f(w.p2), f(w.p3), f(w.p4)))
      })

The problem is that I can't make it!

I've tried by smaller steps.

class UserTable(tag: Tag) extends TableWithId[User](tag,"USER") {
  override def id = column[String]("id", O.PrimaryKey)
  def role = column[UserRole.Value]("role", O.NotNull)
  def login = column[String]("login", O.NotNull)
  def password = column[String]("password", O.NotNull)
  def firstName = column[String]("first_name", O.NotNull)
  def lastName = column[String]("last_name", O.NotNull)
  //
  def * = (id, role, login, password, firstName, lastName) <> (User.tupled,User.unapply)
  //
  def login_index = index("idx_user_login", login, unique = true)
}

It seems that when I call

(id, (firstName, lastName)).shaped

The type is ShapedValue[(Column[String], (Column[String], Column[String])), Nothing]

While this one seems to work fine

(id, firstName, lastName).shaped

The U type parameter is not Nothing but as expected (String, String, String)

I don't really understand how all the Slick internals are working. Can someone explain me why I can't make my code work? Is there a missing import or something?

I guess I need to get a value of type

ShapedValue[(Column[String], (Column[String], Column[String])), (String, (String, String))]

but I don't know why it returns me Nothing and don't really understand where these implicit Shape parameters come from...

What I want is just to be able to easily split my column into 2 case classes

Thanks

like image 439
Sebastien Lorber Avatar asked Feb 03 '15 17:02

Sebastien Lorber


3 Answers

Also, have the same problem with the 22 columns limitation, the test case helps very much. Not sure why the example code not working for you, the following code works fine for me,

case class UserRole(var role: String, var extra: String)
case class UserInfo(var login: String, var password: String, var firstName: String, var lastName: String)

case class User(id: Option[String], var info: UserInfo, var role: UserRole)

class UserTable(tag: Tag) extends Table[User](tag, "USER") {

  def id = column[String]("id", O.PrimaryKey)
  def role = column[String]("role", O.NotNull)
  def extra = column[String]("extra", O.NotNull)
  def login = column[String]("login", O.NotNull)
  def password = column[String]("password", O.NotNull)
  def firstName = column[String]("first_name", O.NotNull)
  def lastName = column[String]("last_name", O.NotNull)

  /** Projection */
  def * = (
    id,
    (login, password, firstName, lastName),
    (role, extra)
  ).shaped <> (

  { case (id, userInfo, userRole) =>
    User(Option[id], UserInfo.tupled.apply(userInfo), UserRole.tupled.apply(userRole))
  },
  { u: User =>
      def f1(p: UserInfo) = UserInfo.unapply(p).get
      def f2(p: UserRole) = UserRole.unapply(p).get
      Some((u.id.get, f1(u.info), f2(u.role)))
  })

  def login_index = index("id_user_login", login, unique = true)
}
like image 195
lzongren Avatar answered Nov 16 '22 03:11

lzongren


As Izongren answered it works fine, but it can be hard to write such code as it's annoying to work with very long tuples... I decided to do it "step by step" and always providing types explicitly to avoid type inference problemes and now it works fine.

case class Patient(
                    id: String = java.util.UUID.randomUUID().toString,
                    companyScopeId: String,
                    assignedToUserId: Option[String] = None,
                    info: PatientInfo
                    ) extends ModelWithId



case class PatientInfo(
                        firstName: Option[String] = None,
                        lastName: Option[String] = None,
                        gender: Option[Gender.Value] = None,
                        alias: Option[String] = None,
                        street: Option[String] = None,
                        city: Option[String] = None,
                        postalCode: Option[String] = None,
                        phone: Option[String] = None,
                        mobilePhone: Option[String] = None,
                        email: Option[String] = None,
                        age: Option[AgeRange.Value] = None,
                        companySeniority: Option[CompanySeniorityRange.Value] = None,
                        employmentContract: Option[EmploymentContract.Value] = None,
                        socialStatus: Option[SocialStatus.Value] = None,
                        jobTitle: Option[String] = None
                        )

class PatientTable(tag: Tag) extends TableWithId[Patient](tag,"PATIENT") {
  override def id = column[String]("id", O.PrimaryKey)
  def companyScopeId = column[String]("company_scope_id", O.NotNull)
  def assignedToUserId = column[Option[String]]("assigned_to_user_id", O.Nullable)

  def firstName = column[Option[String]]("first_name", O.Nullable)
  def lastName = column[Option[String]]("last_name", O.Nullable)
  def gender = column[Option[Gender.Value]]("gender", O.Nullable)
  def alias = column[Option[String]]("alias", O.Nullable)
  def street = column[Option[String]]("street", O.Nullable)
  def city = column[Option[String]]("city", O.Nullable)
  def postalCode = column[Option[String]]("postal_code", O.Nullable)
  def phone = column[Option[String]]("phone", O.Nullable)
  def mobilePhone = column[Option[String]]("mobile_phone", O.Nullable)
  def email = column[Option[String]]("email", O.Nullable)
  def age = column[Option[AgeRange.Value]]("age", O.Nullable)
  def companySeniority = column[Option[CompanySeniorityRange.Value]]("company_seniority", O.Nullable)
  def employmentContract = column[Option[EmploymentContract.Value]]("employment_contract", O.Nullable)
  def socialStatus = column[Option[SocialStatus.Value]]("social_status", O.Nullable)
  def jobTitle = column[Option[String]]("job_title", O.Nullable)
  def role = column[Option[String]]("role", O.Nullable)



  private type PatientInfoTupleType = (Option[String], Option[String], Option[Gender.Value], Option[String], Option[String], Option[String], Option[String], Option[String], Option[String], Option[String], Option[AgeRange.Value], Option[CompanySeniorityRange.Value], Option[EmploymentContract.Value], Option[SocialStatus.Value], Option[String])
  private type PatientTupleType = (String, String, Option[String], PatientInfoTupleType)
  //
  private val patientShapedValue = (id, companyScopeId, assignedToUserId,
    (
      firstName, lastName, gender, alias, street, city, postalCode,
      phone, mobilePhone,email, age, companySeniority, employmentContract, socialStatus, jobTitle
      )
    ).shaped[PatientTupleType]
  //
  private val toModel: PatientTupleType => Patient = { patientTuple =>
    Patient(
      id = patientTuple._1,
      companyScopeId = patientTuple._2,
      assignedToUserId = patientTuple._3,
      info = PatientInfo.tupled.apply(patientTuple._4)
    )
  }
  private val toTuple: Patient => Option[PatientTupleType] = { patient =>
    Some {
      (
        patient.id,
        patient.companyScopeId,
        patient.assignedToUserId,
        (PatientInfo.unapply(patient.info).get)
        )
    }
  }

  def * = patientShapedValue <> (toModel,toTuple)
}
like image 24
Sebastien Lorber Avatar answered Nov 16 '22 01:11

Sebastien Lorber


Also, you can use this way

class UserTable(tag: Tag) extends TableWithId[User](tag,"USER") {
  def id = column[String]("id", O.PrimaryKey)
  def role = column[String]("role", O.NotNull)
  def extra = column[String]("extra", O.NotNull)
  def login = column[String]("login", O.NotNull)
  def password = column[String]("password", O.NotNull)
  def firstName = column[String]("first_name", O.NotNull)
  def lastName = column[String]("last_name", O.NotNull)

  def loginMap = (login, password, firstName, lastName) <> (UserInfo.tupled, UserInfo.unapply)

  def roleMap = (role, extra) <> (Role.tupled, Role.unapply)

  override def * = (id, roleMap, loginMap) <> (User.tupled,User.unapply)

  def login_index = index("idx_user_login", login, unique = true)
}
like image 40
Majid Hosseini Avatar answered Nov 16 '22 03:11

Majid Hosseini