2015-05-17 4 views
7

Ich versuche, eine gespeicherte Prozedur von Slick 3.0 (in Play Framework) aufzurufen. Ich habe die Dokumentation immer und immer wieder gelesen, aber leider zeigt die plain SQL docs at Typesafe niemals eine gespeicherte Prozedur an.So rufen Sie eine gespeicherte Prozedur und erhalten Rückgabewert in Slick (mit Scala)

Was ziemlich einfach scheint eine typisch obskure Scala Fehlermeldung verursacht:

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...? 
} 

Der obige Code diesen Compiler-Fehler verursacht:

[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]         ^

Wenn ich eine rein hartcodierte Aufrufanweisung (Entfernen Sie alle ${i.xyz} Referenzen, ich kann es zum Übersetzen bekommen ... aber dann bekomme ich einen Laufzeitfehler melden, dass Update statements should not return a result set.

Das führte mich die Aussage zu einem regelmäßigen sql Aufruf zum Wechsel:

val call: DBIO[Seq[(Int)]] = sql"call app_glimpulse_invitation_pkg.n_send_invitation('xyz', 1000, 1, '[email protected]', NULL, 'I', ${out})".as[(Int)] 
val result: Future[Int] = db.run(call) 

Aber das führt auch nirgends, einen Compiler-Fehler ergeben:

[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]           ^

ich gefunden habe (während Sie durch die Slick-APIs Surfen) a prepareCall auf der Sitzung, aber wieder ... keine Dokumentation zur Verwendung dieser Sache.

Alle und alle Ratschläge würden sehr zutiefst geschätzt werden. Dies ist für mich ein großer Blocker geworden, da wir wirklich einen funktionierenden Anruf bei unseren Postgres Stored Procedures bekommen müssen. Vielen Dank.

+1

Sie Anorm in einem solchen Fall verwenden können. – cchantep

+0

Keine Notwendigkeit, "Anruf" zu verwenden. Verwenden Sie einfach eine einfache 'Select app_glimpulse_invitation_pkg.n_send_invitation (...)' –

+0

Gute Frage. Klarer Punkt, beginnt Ihr erster Aufruf wirklich mit '? =' - das sieht für mich nicht nach gültigem SQL aus ... –

Antwort

3

Nun, nach viel Recherche und Überprüfung der widersprüchlichen Dokumentation, fand ich die Antwort. Leider war es nicht das, was ich suchte:

For database functions that return complete tables or stored procedures please use Plain SQL Queries. Stored procedures that return multiple result sets are currently not supported.

Unterm Strich Slick nicht gespeicherte Funktionen oder Verfahren aus der Box unterstützt, so müssen wir unsere eigenen schreiben.

Die Antwort besteht darin, Slick aus dem Speicher zu entfernen, indem Sie das Sitzungsobjekt abgreifen und dann Standard-JDBC verwenden, um den Prozeduraufruf zu verwalten. Für diejenigen von euch, die mit JDBC vertraut sind, ist das keine Freude ... aber glücklicherweise können wir mit Scala einige schöne Tricks mit Pattern Matching machen, die die Arbeit erleichtern.

Der erste Schritt für mich bestand darin, eine saubere externe API zusammenzustellen. Dies ist, was es am Ende aussehen wie:

val db = Database.forDataSource(DB.getDataSource) 
var response: Option[GPInviteResponse] = None 

db.withSession { 
    implicit session => { 
     val parameters = GPProcedureParameterSet(
      GPOut(Types.INTEGER) :: 
      GPIn(Option(i.token), Types.VARCHAR) :: 
      GPIn(recipientAccountId, Types.INTEGER) :: 
      GPIn(Option(contactType), Types.INTEGER) :: 
      GPIn(contactValue, Types.VARCHAR) :: 
      GPIn(None, Types.INTEGER) :: 
      GPIn(Option(requestType), Types.CHAR) :: 
      GPOut(Types.INTEGER) :: 
      Nil 
     ) 

     val result = execute(session.conn, GPProcedure.SendInvitation, parameters) 
     val rc = result.head.asInstanceOf[Int] 

     Logger(s"FUNC return code: $rc") 
     response = rc match { 
      case 0 => Option(GPInviteResponse(true, None, None)) 
      case _ => Option(GPInviteResponse(false, None, Option(GPError.errorForCode(rc)))) 
     } 
    } 
} 

db.close() 

Hier ist ein kurzer Durchmarsch: Ich habe einen einfachen Container erstellt einen gespeicherte Prozedur-Aufruf zu modellieren. Das GPProcedureParameterSet kann eine Liste von GPIn-, GPOut- oder GPInOut-Instanzen enthalten. Jeder von diesen bildet einen Wert für einen JDBC-Typ ab. Der Behälter sieht wie folgt aus:

case class GPOut(parameterType: Int) extends GPProcedureParameter 
object GPOut 

case class GPIn(value: Option[Any], parameterType: Int) extends GPProcedureParameter 
object GPIn 

case class GPInOut(value: Option[Any], parameterType: Int) extends GPProcedureParameter 
object GPInOut 

case class GPProcedureParameterSet(parameters: List[GPProcedureParameter]) 
object GPProcedureParameterSet 

object GPProcedure extends Enumeration { 
    type GPProcedure = Value 
    val SendInvitation = Value("{?=call app_glimpulse_invitation_pkg.n_send_invitation(?, ?, ?, ?, ?, ?, ?)}") 
} 

Der Vollständigkeit Ich schließe die GPProcedure Aufzählung, so dass Sie sie alle zusammen setzen können.

All dies wird an meine execute() Funktion übergeben. Es ist groß und eklig, riecht nach altmodischem JDBC und ich bin mir sicher, dass ich die Scala ein bisschen verbessern werde. Ich habe das letzte Mal um 3 Uhr morgens fertiggestellt ... aber es funktioniert, und es funktioniert wirklich gut. Beachten Sie, dass diese spezielle Funktion execute() eine List zurückgibt, die alle OUT-Parameter enthält ...Ich muss eine separate executeQuery() Funktion schreiben, um eine Prozedur zu behandeln, die resultSet zurückgibt. (Der Unterschied ist jedoch trivial: Sie schreiben einfach eine Schleife, die eine resultSet.next schnappt und alles in eine List oder irgendeine andere Struktur stopft, die Sie möchten).

Hier ist der große böse Scala < -> JDBC-Mapping execute() Funktion:

def execute(connection: Connection, procedure: GPProcedure, ps: GPProcedureParameterSet) = { 
    val cs = connection.prepareCall(procedure.toString) 
    var index = 0 

    for (parameter <- ps.parameters) { 
     index = index + 1 
     parameter match { 
      // Handle any IN (or INOUT) types: If the optional value is None, set it to NULL, otherwise, map it according to 
      // the actual object value and type encoding: 
      case p: GPOut => cs.registerOutParameter(index, p.parameterType) 
      case GPIn(None, t) => cs.setNull(index, t) 
      case GPIn(v: Some[_], Types.NUMERIC | Types.DECIMAL) => cs.setBigDecimal(index, v.get.asInstanceOf[java.math.BigDecimal]) 
      case GPIn(v: Some[_], Types.BIGINT) => cs.setLong(index, v.get.asInstanceOf[Long]) 
      case GPIn(v: Some[_], Types.INTEGER) => cs.setInt(index, v.get.asInstanceOf[Int]) 
      case GPIn(v: Some[_], Types.VARCHAR | Types.LONGVARCHAR) => cs.setString(index, v.get.asInstanceOf[String]) 
      case GPIn(v: Some[_], Types.CHAR) => cs.setString(index, v.get.asInstanceOf[String].head.toString) 
      case GPInOut(None, t) => cs.setNull(index, t) 

      // Now handle all of the OUT (or INOUT) parameters, these we just need to set the return value type: 
      case GPInOut(v: Some[_], Types.NUMERIC) => { 
       cs.setBigDecimal(index, v.get.asInstanceOf[java.math.BigDecimal]); cs.registerOutParameter(index, Types.NUMERIC) 
      } 
      case GPInOut(v: Some[_], Types.DECIMAL) => { 
       cs.setBigDecimal(index, v.get.asInstanceOf[java.math.BigDecimal]); cs.registerOutParameter(index, Types.DECIMAL) 
      } 
      case GPInOut(v: Some[_], Types.BIGINT) => { 
       cs.setLong(index, v.get.asInstanceOf[Long]); cs.registerOutParameter(index, Types.BIGINT) 
      } 
      case GPInOut(v: Some[_], Types.INTEGER) => { 
       cs.setInt(index, v.get.asInstanceOf[Int]); cs.registerOutParameter(index, Types.INTEGER) 
      } 
      case GPInOut(v: Some[_], Types.VARCHAR) => { 
       cs.setString(index, v.get.asInstanceOf[String]); cs.registerOutParameter(index, Types.VARCHAR) 
      } 
      case GPInOut(v: Some[_], Types.LONGVARCHAR) => { 
       cs.setString(index, v.get.asInstanceOf[String]); cs.registerOutParameter(index, Types.LONGVARCHAR) 
      } 
      case GPInOut(v: Some[_], Types.CHAR) => { 
       cs.setString(index, v.get.asInstanceOf[String].head.toString); cs.registerOutParameter(index, Types.CHAR) 
      } 
      case _ => { Logger(s"Failed to match GPProcedureParameter in executeFunction (IN): index $index (${parameter.toString})") } 
     } 
    } 

    cs.execute() 

    // Now, step through each of the parameters, and get the corresponding result from the execute statement. If there is 
    // no result for the specified column (index), we'll basically end up getting a "nothing" back, which we strip out. 

    index = 0 

    val results: List[Any] = for (parameter <- ps.parameters) yield { 
     index = index + 1 
     parameter match { 
      case GPOut(Types.NUMERIC) | GPOut(Types.DECIMAL) => cs.getBigDecimal(index) 
      case GPOut(Types.BIGINT) => cs.getLong(index) 
      case GPOut(Types.INTEGER) => cs.getInt(index) 
      case GPOut(Types.VARCHAR | Types.LONGVARCHAR | Types.CHAR) => cs.getString(index) 
      case GPInOut(v: Some[_], Types.NUMERIC | Types.DECIMAL) => cs.getInt(index) 
      case GPInOut(v: Some[_], Types.BIGINT) => cs.getLong(index) 
      case GPInOut(v: Some[_], Types.INTEGER) => cs.getInt(index) 
      case GPInOut(v: Some[_], Types.VARCHAR | Types.LONGVARCHAR | Types.CHAR) => cs.getString(index) 
      case _ => { 
       Logger(s"Failed to match GPProcedureParameter in executeFunction (OUT): index $index (${parameter.toString})") 
      } 
     } 
    } 

    cs.close() 

    // Return the function return parameters (there should always be one, the caller will get a List with as many return 
    // parameters as we receive): 

    results.filter(_ != (())) 
} 
+0

Wäre es nicht einfacher gewesen, die JdbcTemplate-Klasse aus dem Spring-Framework zu verwenden? Siehe hierzu: http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html. – costa

+0

'Objekt TestSpringJdbc { Fall Klasse Row (var Wert: Int) def main (args: Array [String]) {var ods = new SQLServerDataSource() ods.setURL (" jdbc: sqlserver: // localhost : 1433; integratedSecurity = true; database = test; ") // val con = ods.getConnection() var jdbcTemplate = new jdbcTemplate (ods) val list = jdbcTemplate.query (" select 1 als Wert union select 2" , New RowMapper [row] { Überschreibung def mapRow (rs: ResultSet, rowNum: int): Zeile = Zeile (rs.getInt (1)) }) list.foreach (println) } } ' – costa

+0

Sie können gespeicherte Prozeduren anstelle der Abfrage, die ich dort hatte, ausführen und Parameter über Karten übergeben. – costa

Verwandte Themen