Question

In Scala how can I most directly send the results of a MySql query to a JSON string? We're working within the "play" framework and are using "slick" to connect to MySQL, so I'm more specifically trying to results of a slick's StaticQuery SELECT directly to a play JsValue object. However any solution to the general problem is welcome.

The select parameters of the query string are inserted dynamically and they have known MySQL types, but I don't generally know the specific parameters or the number of them at compile time.

I've got a feeling that there's a clever way to do this with case classes and Map objects, but I'm hoping that someone has already solved this problem more simply. For example there is this related Java Gist.

For example I currently have:

import play.api.libs.json._
import scala.slick.jdbc.{GetResult, StaticQuery => Q}

...

case class SrResult(id: Int, user_id: Int, title: String)
implicit val getSrResult = GetResult(r => SrResult(r.<<, r.<<, r.<<))

val qSql = "SELECT id,user_id,title from MYTABLE LIMIT 10"
val q = Q.queryNA[SrResult](qSql)

I will be inserting the "is,user_id,title" into the qSql string dynamically, so Q.queryNA's return type "SrResult" is not known at complile time.

For completeness here is an example table that I'm working with:

CREATE TABLE `MYTABLE` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `USER_ID` int(11) NOT NULL,
  `TITLE` varchar(254) NOT NULL,
  `DESCRIPTION` text
  `ARCHIVED` tinyint(1) NOT NULL,
  `CREATED_AT` bigint(20) NOT NULL,
  `END_DATE` date NULL,
  PRIMARY KEY (`ID`),
  KEY `TEST_USER_INDEX` (`USER_ID`),
  CONSTRAINT `TEST_USER_FK` FOREIGN KEY (`USER_ID`) REFERENCES `USERS` (`ID`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=1058 DEFAULT CHARSET=utf8;
Was it helpful?

Solution 2

I ended up working at the lower JDBC level parsing the ResultSet from an executeQuery through Play's Json tools. For example:

import play.api.libs.json._
import utils.DatabaseConfig.driver.simple._
import slick.session.Database.threadLocalSession
import java.sql.{Connection, DriverManager, ResultSet};
import java.util.Date

...

val rs = statement.executeQuery("SELECT * FROM USERS")

jsValueResult = sqlResultSetToJson(rs)

where

def sqlResultSetToJson(rs: ResultSet): JsValue = {
  // This is loosely ported from https://gist.github.com/kdonald/2137988

  val rsmd = rs.getMetaData
  val columnCount = rsmd.getColumnCount

  // It may be faster to collect each line into a Seq or other iterable
  // and pass that to Json.arr() at the end.
  var qJsonArray: JsArray = Json.arr()
  while (rs.next) {
    var index = 1

    var rsJson: JsObject = Json.obj()
    while (index <= columnCount) {
      // Unfortunately jdbc ResultSetMetaData doesn't expose a reliable
      // getTableName method.  It returns the "originalTableName" which doesn't
      // include table aliases defined in the SELECT statement.
      // Therefore the table name needs to be hard coded into each column
      // name in the SELECT command.
      //
      // We should also be checking that there are no duplicate columnLabel's
      // The Json constructors will just mindlessly append items with dup names
      // to the JsObject.
      val column = rsmd.getColumnLabel(index)
      val columnLabel = column.toLowerCase()

      val value = rs.getObject(column)
      if (value == null) {
        rsJson = rsJson ++ Json.obj(
          columnLabel -> JsNull
        )
      } else if (value.isInstanceOf[Integer]) {
        println(value.asInstanceOf[Integer])
        rsJson = rsJson ++ Json.obj(
          columnLabel -> value.asInstanceOf[Int]
        )
      } else if (value.isInstanceOf[String]) {
        println(value.asInstanceOf[String])
        rsJson = rsJson ++ Json.obj(
          columnLabel -> value.asInstanceOf[String]
        )
      } else if (value.isInstanceOf[Boolean]) {
        rsJson = rsJson ++ Json.obj(
          columnLabel -> value.asInstanceOf[Boolean]
        )
      } else if (value.isInstanceOf[Date]) {
        rsJson = rsJson ++ Json.obj(
          columnLabel -> value.asInstanceOf[Date].getTime
        )
      } else if (value.isInstanceOf[Long]) {
        rsJson = rsJson ++ Json.obj(
          columnLabel -> value.asInstanceOf[Long]
        )
      } else if (value.isInstanceOf[Double]) {
        rsJson = rsJson ++ Json.obj(
          columnLabel -> value.asInstanceOf[Double]
        )
      } else if (value.isInstanceOf[Float]) {
        rsJson = rsJson ++ Json.obj(
          columnLabel -> value.asInstanceOf[Float]
        )
      } else if (value.isInstanceOf[BigDecimal]) {
        rsJson = rsJson ++ Json.obj(
          columnLabel -> value.asInstanceOf[BigDecimal]
        )
      // } else if (value.isInstanceOf[Byte]) {
      //   rsJson = rsJson ++ Json.obj(
      //     columnLabel -> value.asInstanceOf[Byte]
      //   )
      // } else if (value.isInstanceOf[Array[Byte]]) {
      //   rsJson = rsJson ++ Json.obj(
      //     columnLabel -> value.asInstanceOf[Array[Byte]]
      //   )
      } else {
        throw new IllegalArgumentException("Unmappable object type: " + value.getClass)
      }
      index += 1
    }
    qJsonArray = qJsonArray :+ rsJson
  }
  qJsonArray
}

OTHER TIPS

I explain in this Stackoverflow answer how to get a Map out of a Slick plain SQL query. You will need to adapt it to restrict it to only the fields you need. From the Map it should be trivial to get a JSON object with your favorite JSON library.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top