Reactive Scala Driver for MongoDB

Asynchronous & Non-Blocking

ReactiveMongo Biːsən

These libraries are intended to replace (at some point after release 1.0) the current BSON library (shipped along with ReactiveMongo driver).

The motivation for that is to fix some issues (OOM), to bring multiple API and performance improvements (simpler & better).

Highlights

BSON types and handlers

The main API library migrates both the BSON value types and the handler typeclasses.

It can be configured in a build.sbt as below.

libraryDependencies += "org.reactivemongo" %% "reactivemongo-bson-api" % "0.19.2"

See Scaladoc

Documents and values

The names of the BSON value types are the same as the current BSON library, except the package that is reactivemongo.api.bson (instead of reactivemongo.bson).

import scala.util.Try
import reactivemongo.api.bson._

val bsonDouble = BSONDouble(12.34D)

val bsonStr = BSONString("foo")

val bsonInt = BSONInteger(2345)

// BSON array: [ 12.34, "foo", 2345 ]
val bsonArray = BSONArray(bsonDouble, bsonStr, bsonInt)

val bsonEmptyDoc: BSONDocument = BSONDocument.empty

/* BSON Document:
{
  'foo': 'bar',
  'lorem': 2,
  'values': [ 12.34, "foo", 2345 ],
  'nested': {
    'position': 1000,
    'score': 1.2
  }
}
*/
val bsonDoc = BSONDocument(
  "foo" -> "bar", // as BSONString
  "lorem" -> 2, // as BSONInteger
  "values" -> bsonArray,
  "nested" -> BSONDocument(
    "position" -> 1000,
    "score" -> 1.2D // as BSONDouble
  )
)

val bsonBin = BSONBinary(Array[Byte](0, 1, 2), Subtype.GenericBinarySubtype)
// See Subtype

val bsonObjectID = BSONObjectID.generate()

val bsonBool = BSONBoolean(true)

val bsonDateTime = BSONDateTime(System.currentTimeMillis())

val bsonRegex = BSONRegex("/foo/bar/", "g")

val bsonJavaScript = BSONJavaScript("lorem(0)")

val bsonJavaScriptWs = BSONJavaScriptWS("bar", BSONDocument("bar" -> 1))

val bsonTimestamp = BSONTimestamp(45678L)

val bsonLong = BSONLong(Long.MaxValue)

val bsonZeroDecimal = BSONDecimal.PositiveZero

val bsonDecimal: Try[BSONDecimal] =
  BSONDecimal.fromBigDecimal(BigDecimal("12.23"))

val bsonNull = BSONNull

val bsonMinKey = BSONMinKey

val bsonMaxKey = BSONMaxKey

Documents:

The API for BSONDocument has been slightly updated, with the function getAs renamed as getAsOpt (to be consistent with getAsTry).

New field utilities are provided for the most common types:

import reactivemongo.api.bson._

def foo(doc: BSONDocument): Unit = {
  val i: Option[Int] = doc.int("fieldNameInt")
  val d: Option[Double] = doc.double("fieldNameDouble")
  val l: Option[Long] = doc.long("fieldNameLong")
  val s: Option[String] = doc.string("fieldNameStr")
  val a: Option[Seq[BSONValue]] = doc.array("fieldNameArr")
  val b: Option[Boolean] = doc.booleanLike("fieldNameBool")
  val c: Option[BSONDocument] = doc.child("nestedDoc")
  val _: List[BSONDocument] = doc.children("arrayOfDocs")
}

Note: The BSONDocument factories have been optimized and support more use cases.

Document benchmarks
Coefficient between new/old throughput (op/s; =1: no change, 1+: better thrpt). Source: BSONDocumentBenchmark, BSONDocumentHandlerBenchmark

Numeric values:

The traits BSONNumberLike and BSONBooleanLike are also kept in the new API, to generalize the handling of numerical and boolean values.

import scala.util.Try
import reactivemongo.api.bson._

val doc = BSONDocument("ok" -> 1.0D /* BSON double */ )

val bsonNumLike: Try[BSONNumberLike] = doc.getAsTry[BSONNumberLike]("ok")
val intLike: Try[Int] = bsonNumLike.flatMap(_.toInt) // =Success(1)

val bsonBoolLike: Try[BSONBooleanLike] = doc.getAsTry[BSONBooleanLike]("ok")
val boolLike: Try[Boolean] = bsonBoolLike.flatMap(_.toBoolean) // =Success(true)

Now Float is handled as a BSON double (as Double, as it’s now possible to have several Scala types corresponding to the same BSON type).

The Decimal128 type introduced by MongoDB 3.4 is also supported, as BSONDecimal, and can be read or write as java.math.BigDecimal.

import scala.util.Try
import reactivemongo.api.bson._

def readFloat(
  doc: BSONDocument,
  n: String
)(implicit r: BSONReader[Float]): Try[Float] = doc.getAsTry[Float](n)

Still to make the API simpler, the BSON singleton types (e.g. BSONNull) are also defined with a trait, to be able to reference them without .type suffix.

import reactivemongo.api.bson.BSONNull

def useNullBefore(bson: BSONNull.type) = println(".type was required")

def useNullNow(bson: BSONNull) = print("Suffix no longer required")

Binary values:

The BSONBinary extractor now only bind subtype:

import reactivemongo.api.bson.{ BSONBinary, Subtype }

def binExtractor = {
  BSONBinary(Array[Byte](0, 1, 2), Subtype.GenericBinarySubtype) match {
    case genBin @ BSONBinary(Subtype.GenericBinarySubtype) =>
      genBin.byteArray

    case _ => ???
  }
}

Miscellaneous:

Summary:

BSON Description JVM type
BSONBinary binary data Array[Byte]
BSONBoolean boolean Boolean
BSONDateTime UTC Date Time java.util.Date
BSONDecimal Decimal128 java.math.BigDecimal
BSONDouble 64-bit IEEE 754 floating point Double
BSONInteger 32-bit integer Int
BSONJavaScript JavaScript code None
BSONJavaScriptWS JavaScript scoped code None
BSONLong 64-bit integer Long
BSONMaxKey max key None
BSONMinKey min key None
BSONNull null None
BSONObjectID 12 bytes default id type in MongoDB None
BSONRegex regular expression None
BSONString UTF-8 string String
BSONSymbol deprecated in the protocol None
BSONTimestamp special date type used in MongoDB internals None
BSONUndefined deprecated in the protocol None

Reader and writer typeclasses

As for the current BSON library, the new API provides an extensible typeclass mechanism, to define how to get and set data as BSON in a typesafe way.

The names of these typeclasses are unchanged (BSONReader and BSONWriter), except the package that is reactivemongo.api.bson (instead of reactivemongo.bson).

In the current BSON library, BSONReader and BSONWriter are defined with two type parameters:

BSONReader[B <: BSONValue, T]

BSONWriter[T, B <: BSONValue]

The new API has been simplified, with only the T type parameter kept.

import reactivemongo.api.bson._

// read a String from BSON, whatever is the specific BSON value type
def stringReader: BSONReader[String] = ???

Not only it makes the API simpler, but it also allows to read different BSON types as a target Scala type (before only supported for numeric/boolean, using the dedicated typeclasses). For example, the Scala numeric types (BigDecimal, Double, Float, Int, Long) can be directly read from any consistent BSON numeric type (e.g. 1.0 as integer 1), without having to use BSONNumberLike.

Also, handler functions readTry and writeTry returns Try, for a safer representation of possible failures.

The new API is also safer, replacing BSONReader.read and BSONWriter.write respectively with BSONReader.readTry and BSONWriter.writeTry, so that serialization errors can be handle at typelevel. In a similar way, BSONObjectID.parse now returns Try.

Like the current BSON library, some specific typeclasses are available (with same names) to only work with BSON documents: BSONDocumentReader and BSONDocumentWriter.

Some new handlers are provided by default, like those for Java Time types.

Note: The handler for java.util.Date is replaced the handler for java.time.Instant.

The error handling has also been improved, with more details (Note: DocumentKeyNotFoundException is the previous API is replaced with BSONValueNotFoundException in the new one).

Reader benchmarks
Coefficient between new/old throughput (op/s; =1: no change, 1+: better thrpt). Source: BSON reader benchmarks
Macros

The new library also provide similar macros, to easily materialized document readers and writers for Scala case classes and sealed traits.

case class Person(name: String, age: Int)

import reactivemongo.api.bson._

val personHandler: BSONDocumentHandler[Person] = Macros.handler[Person]

// Or only ...
val personReader: BSONDocumentReader[Person] = Macros.reader[Person]
val personWriter: BSONDocumentWriter[Person] = Macros.writer[Person]

This macro utilities offer new configuration mechanism.

The macro configuration can be used to specify a field naming, to customize the name of each BSON field corresponding to Scala field.

import reactivemongo.api.bson._

val withPascalCase: BSONDocumentHandler[Person] = {
  implicit def cfg: MacroConfiguration = MacroConfiguration(
    fieldNaming = FieldNaming.PascalCase)

  Macros.handler[Person]
}

withPascalCase.writeTry(Person(name = "Jane", age = 32))
/* Success {
  BSONDocument("Name" -> "Jane", "Age" -> 32)
} */

In a similar way, when using macros with sealed family/trait, the strategy to name the discriminator field and to set a Scala type as discriminator value can be configured.

import reactivemongo.api.bson._

sealed trait Family1
case class Foo1(bar: String) extends Family1
case class Lorem1(ipsum: Int) extends Family1

implicit val foo1Handler = Macros.handler[Foo1]
implicit val lorem1Handler = Macros.handler[Lorem1]

val family1Handler: BSONDocumentHandler[Family1] = {
  implicit val cfg: MacroConfiguration = MacroConfiguration(
    discriminator = "_type",
    typeNaming = TypeNaming.SimpleName.andThen(_.toLowerCase))

  Macros.handler[Family1]
}

The nested type Macros.Options is replaced by similar type MacrosOptions.

Note: The Macros.Options.SaveSimpleName of the previous BSON library has been removed in favour of a configuration factory using similar TypeNaming.

Note: A new option MacroOptions.DisableWarnings allows to specifically exclude macro warnings.

Compatibility and migration

A compatibility library is available, that provides conversions between the previous and the new APIs. It can be configured in the build.sbt as below.

libraryDependencies += "org.reactivemongo" %% "reactivemongo-bson-compat" % "0.19.2"

Then the conversions can be imported where required:

import reactivemongo.api.bson.compat._

Another compatibility library is available for the package org.bson.

libraryDependencies += "org.reactivemongo" %% "reactivemongo-bson-msb-compat" % "0.19.2"

Then the conversions between those two API/packages can be imported as below.

import reactivemongo.api.bson.msb._

GeoJSON

A new GeoJSON library is provided, with the geometry types and the corresponding handlers to read from and write them to appropriate BSON representation.

It can be configured in the build.sbt as below.

libraryDependencies += "org.reactivemongo" %% "reactivemongo-bson-geo" % "0.19.2"

Then the GeoJSON types can be imported and used:

import reactivemongo.api.bson._

// { type: "Point", coordinates: [ 40, 5 ] }
val geoPoint = GeoPoint(40, 5)

// { type: "LineString", coordinates: [ [ 40, 5 ], [ 41, 6 ] ] }
val geoLineString = GeoLineString(
  GeoPosition(40D, 5D, None),
  GeoPosition(41D, 6D))

More GeoJSON examples

GeoJSON ReactiveMongo Description
Position GeoPosition Position coordinates
Point GeoPoint Single point with single position
LineString GeoLineString Simple line
LinearRing GeoLinearRing Simple (closed) ring
Polygon GeoPolygon Polygon with at least one ring
MultiPoint GeoMultiPoint Collection of points
MultiLineString GeoMultiLineString Collection of LineString
MultiPolygon GeoMultiPolygon Collection of polygon
GeometryCollection GeoGeometryCollection Collection of geometry objects

See Scaladoc

Monocle

(Experimental)

The library that provides Monocle based optics, for BSON values.

It can be configured in the build.sbt as below.

libraryDependencies += "org.reactivemongo" %% "reactivemongo-bson-monocle" % "0.19.2"

Then the utilities can be imported and used:

import reactivemongo.api.bson._

import reactivemongo.api.bson.monocle._ // new library

val barDoc = BSONDocument(
  "lorem" -> 2,
  "ipsum" -> BSONDocument("dolor" -> 3))

val topDoc = BSONDocument(
  "foo" -> 1,
  "bar" -> barDoc)

// Simple field
val lens1 = field[BSONInteger]("foo")
val updDoc1: BSONDocument = lens1.set(BSONInteger(2))(topDoc)
// --> { "foo": 1, ... }

// Nested field
val lens2 = field[BSONDocument]("bar").
  composeOptional(field[Double]("lorem"))

val updDoc2 = lens2.set(1.23D)(topDoc)
// --> { ..., "bar": { "lorem": 1.23, ... } }

More monocle examples

See Scaladoc