17

I'm working with Slick 3.1.1 and the problem is that in some cases I want to omit some columns that are fairly heavy and still materialize that subset of columns as a case class.

Consider the following table definition:

class AuditResultTable(tag: Tag) extends Table[AuditResult](tag, AuditResultTableName) {
    def auditResultId: Rep[Long] = column[Long]("AuditResultId", O.PrimaryKey, O.AutoInc)
    def processorId: Rep[Long] = column[Long]("ProcessorId")
    def dispatchedTimestamp: Rep[Timestamp] = column[Timestamp]("DispatchedTimestamp", O.SqlType("timestamp(2)"))
    def SystemAOutput: Rep[Array[Byte]] = column[Array[Byte]]("SystemAOutput", O.SqlType("LONGBLOB"))
    def SystemBOutput: Rep[Array[Byte]]  = column[Array[Byte]]("SystemBOutput", O.SqlType("LONGBLOB"))
    def isSuccessful: Rep[Boolean] = column[Boolean]("IsSuccessful")


def * : ProvenShape[AuditResult] = (processorId, dispatchedTimestamp, systemAOutput, systemBOutput, isSuccessful, auditResultId) <>
  (AuditResult.tupled, AuditResult.unapply) 

}  

val auditResults = TableQuery[AuditResultTable]  

The corresponding case class:

case class AuditResult (
   ProcessorId: Long,
   DispatchedTimestamp: Timestamp,
   SystemAOutput: Array[Byte],
   SystemBOutput: Array[Byte],
   IsSuccessful: Boolean,
   AuditResultId: Long = 0L
 )

And finally the data access query:

def getRecentFailedAuditsQuery(): Query[AuditResultTable, AuditResult, Seq] = {
  auditResults.filterNot(r => r.isSuccessful)
}

I've considered and looked into options presented in this (outdated) answer and others:

  • Having a different projection than the default projection which maps to a "light version of the AuditResult, e.g. AuditResultLight that omits those columns - despite my best effort I couldn't make this work - I feel like this should be the right approach - once I had a "working" projection I still got a Slick error "No matching Shape found. Slick does not know how to map the given types"
  • Building a class hierarchy with an abstract AuditResultTableBase class and two classes that derive from it - one that adds the "heavy" columns and one without them, both with their respective default projection and case classes. This works nicely, but the approach seems wrong and requires a relatively large code change for such an easy thing.
  • Materializing tuples instead of case classes - this of course would work, but I want my data access layer to be strongly typed.

What is the idiomatic / best practice for Slick 3.1 for this problem? Can I use a custom projection for this and if so what would that look like for this particular example / query with SystemAOutput and SystemBOutput being the heavy columns I want to omit?

Community
  • 1
  • 1
BrokenGlass
  • 149,257
  • 27
  • 271
  • 318

1 Answers1

8

I had a similar problem! You have to define the Shape! With help of the documentation I managed to make the approach with a "light" case class work.

First, define the simpler class:

case class AuditResultLight(
  ProcessorId: Long,
  DispatchedTimestamp: Timestamp,
  IsSuccessful: Boolean,
  AuditResultId: Long = 0L
)

Then, you need to create a lifted version of the case class:

case class AuditResultLightLifted(
  ProcessorId: Rep[Long],
  DispatchedTimestamp: Rep[Timestamp],
  IsSuccessful: Rep[Boolean],
  AuditResultId: Rep[Long]
)

Also, you need an implicit object (the Shape) to tell slick how to map one into another:

implicit object AuditResultLightShape 
  extends CaseClassShape(AuditResultLightLifted.tupled, AuditResultLight.tupled)

Now, you can define a query that returns AuditResultLight (not exactly a projection, but as far as I understand it works similarly):

val auditResultsLight = auditResults.map(r => AuditResultLightLifted(r.ProcessorId, r.DispatchedTimestamp, r.IsSuccessful, r.AuditResultId))  

Then, you can define the function that returns failed audits in a light form:

def getRecentFailedAuditsQuery(): Query[AuditResultTable, AuditResultLight, Seq] = {
  auditResultsLight.filterNot(r => r.isSuccessful)
}

A gist with the code: https://gist.github.com/wjur/93712a51d392d181ab7fc2408e4ce48b

The code compiles and executes, but in my case, the issue is that my IDE (IntelliJ) reports Query[Nothing, Nothing, scala.Seq] type for auditResultsLight. I get syntax errors whenever I use auditResultsLight and refer to a field of AuditResultLight in a query. However, because of that, in the end, I decided to use the second approach you suggested (the one with an abstract table). Almost the same amount of code, but with IDE support.

  • 1
    Thank you Wojciech - I had hoped there would be the "one" clearly right answer, but it seems I'm not the only one having trouble. Thanks for the insight on how to properly implement a shape / mapping for this use case (even though IntelliJ doesn't like it) - bounty is all yours. – BrokenGlass Oct 10 '16 at 02:30