4
votes

In learning play-slick, and setting up model classes that are eventually saved to PostgreSQL, I see this pattern (code below). There's a simple case class that acts as a model, and then something that extends Table that handles the relational mapping.

case class Cat(name: String, color: String)

/* Table mapping
 */
class CatsTable(tag: Tag) extends Table[Cat](tag, "CAT") {

  def name = column[String]("name", O.PrimaryKey)
  def color = column[String]("color", O.NotNull)

  def * = (name, color) <> (Cat.tupled, Cat.unapply _)
}

This strikes me in many common cases to be a bunch of unnecessary boilerplate, but I'm just learning so I don't know what I'm missing here. Is there an easier way to start with the case class Cat and end up with an object I can use to CRUD instances of Cat in the database? E.g. it seems unnecessary to specify that an attribute of type String should end up being a column[String], and so on. In other frameworks, I might have to add an annotation or something to indicate which I want to be a primary key, or non-nullable, but I wouldn't really write a separate mapping. By hand-writing these mappings, I'm mostly just spending more time and getting the opportunity to screw it up in subtle ways for otherwise simple cases.

What I'd ideally like is to start with case class Cat, sprinkle magic framework dust on it, and get a CatsTable with reasonable defaults, which I could override/customize as necessary.

When I'm searching for docs on this, I usually end up back at schema code generation but this seems backwards; I don't want to generate the table mappings from an existing/populate RDBMS, I want to start from scratch.

1
Bit late, but an alternative library is http://getquill.io/, which needs less boilerplate than Slick, and also provides a choice of jdbc or truly async db calls. As of June 2017, it feels somewhat less polished - eg I'm currently using a combination of Slick and Quill since I can't get the latter to call stored procedures.thund

1 Answers

6
votes

For future searchers on this question, here's what I've found:

You can't reduce the boilerplate, it has to be there. Slick has a code generation feature, which isn't quite the same; with this if you have a SQL schema, it will generate the table mappings for you.

Now, you might ask, if it can generate the table mappings for you automatically, why then should you have to write them and maintain that? It appears the answer is so that the type definitions can be loaded at compile time. Granted, they can be generated, but then they wouldn't be "type safe" in that the compiler wouldn't be checking your code against those types without actual code to go at compile time.

So this appears to hinge on the perceived value of type safety at this layer. If you think it's necessary, then this code isn't boilerplate. If you think that type safety at this particular layer isn't strictly necessary, then this really is boilerplate.

It would appear to boil down to the larger FP assumption that type safety is always important, which for me is in the end of a bit of a "purity" argument.