Home > OS >  Non-relational database in statically typed languages (rethinkdb, Scala)
Non-relational database in statically typed languages (rethinkdb, Scala)

Time:08-31

I'm still pretty new to Scala and arrived at some kind of typing-roadblock.

Non-SQL databases such as mongo and rethinkdb do not enforce any scheme for their tables and manage data in json format. I've been struggling to get the java API for rethinkdb to work on Scala and there seems to be surprisingly low information on how to actually use the results returned from the database.

Assuming a simple document schema such as this:

{
    "name": "melvin",
    "age": 42,
    "tags": ["solution"]
}

I fail to get how to actually this data in Scala. After running a query, for example, by running something like r.table("test").run(connection), I receive an object from which I can iterate AnyRef objects. In the python word, this most likely would be a simple dict. How do I convey the structure of this data to Scala, so I can use it in code (e.g., query fields of the returned documents)?

CodePudding user response:

From a quick scan of the docs and code, the Java Rethink client uses Jackson to handle deserialization of the JSON received from the DB into JVM objects. Since by definition every JSON object received is going to be deserializable into a JSON AST (Abstract Syntax Tree: a representation in plain Scala objects of the structure of a JSON document), you could implement a custom Jackson ObjectMapper which, instead of doing the usual Jackson magic with reflection, always deserializes into the JSON AST.

For example, Play JSON defers the actual serialization/deserialization to/from JSON to Jackson: it installs a module into a vanilla ObjectMapper which specially takes care of instances of JsValue, which is the root type of Play JSON's AST. Then something like this should work:

import com.fasterxml.jackson.databind.ObjectMapper
import play.api.libs.json.jackson.PlayJsonModule

// Use Play JSON's ObjectMapper... best to do this before connecting
RethinkDB.setResultMapper(new ObjectMapper().registerModule(new PlayJsonModule(JsonParserSettings())))

run(connection) returns a Result[AnyRef] in Scala notation. There's an alternative version, run(connection, typeRef), where the second argument specifies a result type; this is passed to the ObjectMapper to ensure that every document will either fail to deserialize or be an instance of that result type:

import play.api.libs.json.JsValue

val result = r.table("table").run(connection, classOf[JsValue]) : Result[JsValue]

You can then get the next element from the result as a JsValue and use the usual Play JSON machinery to convert the JsValue into your domain type:

import play.api.libs.json.Json

case class MyDocument(name: String, age: Int, tags: Seq[String])

object MyDocument {
  implicit val jsonFormat = Json.format[MyDocument]
}

// result is a Result[JsValue] ... may need an import MyDocument.jsonFormat or similar
val myDoc = Json.fromJson[MyDocument](result.next()).asOpt[MyDocument] : Option[MyDocument]

There's some ability with enrichments to improve the Scala API to make a lot of this machinery more transparent.

You could do similar things with the other Scala JSON ASTs (e.g. Circe, json4s), but might have to implement functionality similar to what Play does with the ObjectMapper yourself.

  • Related