I'm trying to call a stored procedure from Slick 3.0 (in Play Framework). I've been over and over the documentation, but unfortunately the plain SQL docs at Typesafe never show calling a stored procedure.
What seems pretty straightforward is causing a typically obscure Scala error message:
val f = Try {
val call: DBIO[Int] = sqlu"?=call app_glimpulse_invitation_pkg.n_send_invitation(${i.token}, ${i.recipientAccountId.getOrElse(None)}, ${i.email}, ${i.phoneNumber}, ${requestType})"
val result: Future[Int] = db.run(call)
val r = Await.result(result, Duration.Inf) // should only return one; use .seq.count(_.id != null)) to validate
val z = result.value.get.get // should return the stored procedure return value...?
}
The above code causes this compiler error:
[error] /Users/zbeckman/Projects/Glimpulse/Server-2/project/glimpulse-server/app/controllers/GPInviteService/GPInviteService.scala:120: could not find implicit value for parameter e: slick.jdbc.SetParameter[Product with Serializable]
[error] val call: DBIO[Int] = sqlu"?=call app_glimpulse_invitation_pkg.n_send_invitation(${i.token}, ${i.recipientAccountId.getOrElse(None)}, ${i.email}, ${i.phoneNumber}, ${requestType})"
[error] ^
If I use a purely hard-coded call statement (remove all the ${i.xyz}
references, I can get it to compile... but then, I get a runtime error reporting that Update statements should not return a result set.
That led me to changing the statement to a regular sql
call:
val call: DBIO[Seq[(Int)]] = sql"call app_glimpulse_invitation_pkg.n_send_invitation('xyz', 1000, 1, 'me@here.com', NULL, 'I', ${out})".as[(Int)]
val result: Future[Int] = db.run(call)
But that also leads nowhere, yielding a compile error:
[error] /Users/zbeckman/Projects/Glimpulse/Server-2/project/glimpulse-server/app/controllers/GPInviteService/GPInviteService.scala:126: type mismatch;
[error] found : slick.driver.PostgresDriver.api.DBIO[Seq[Int]]
[error] (which expands to) slick.dbio.DBIOAction[Seq[Int],slick.dbio.NoStream,slick.dbio.Effect.All]
[error] required: slick.dbio.DBIOAction[Int,slick.dbio.NoStream,Nothing]
[error] val result: Future[Int] = db.run(call)
[error] ^
I did find (while browsing through the Slick APIs) a prepareCall
on the session, but again... no documentation on how to use this thing.
Any and all advice would be very deeply appreciated. This has become a huge blocker for me, as we really need to get a working call to our Postgres stored procedures. Thank you.
Well, after much research and review of conflicting documentation, I found the answer. Unfortunately, it wasn't the one I was looking for:
Bottom line, Slick does not support stored functions or procedures out of the box, so we have to write our own.
The answer is to drop down out of Slick by grabbing the session object, and then use standard JDBC to manage the procedure call. For those of you familiar with JDBC, that's not a joy... but, fortunately, with Scala we can do some pretty nice tricks with pattern matching that make the job easier.
The first step for me was putting together a clean external API. This is what it ended up looking like:
Here's a quick walkthrough: I created a simple container to model a stored procedure call. The GPProcedureParameterSet can contain a list of GPIn, GPOut, or GPInOut instances. Each of these maps a value to a JDBC type. The container looks like this:
For completeness I'm including the GPProcedure enumeration so you can put it all together.
All of this gets handed to my
execute()
function. It's big and nasty, smells like old-fashioned JDBC, and I'm sure I'll improve the Scala quite a bit. I literally finished this up at 3am last night... but it works, and it works really well. Note that this particularexecute()
function returns aList
containing all of the OUT parameters... I'll have to write a separateexecuteQuery()
function to handle a procedure that returns aresultSet
. (The difference is trivial though: you just write a loop that grabs aresultSet.next
and stuff it all into aList
or whatever other structure you would like).Here's the big nasty Scala<->JDBC mapping
execute()
function: