Reactive Scala Driver for MongoDB
Asynchronous & Non-Blocking
BSON readers and writers
In order to get and store data with MongoDB, ReactiveMongo provides an extensible mechanism to appropriately read and write data from/to BSON. This makes usage of MongoDB much less verbose and more natural.
This is articulated around the concept of BSONWriter
and BSONReader
.
As long as you are working with BSONValue
s, some default implementations of readers and writers are provided by the following import.
import reactivemongo.api.bson._
Some specific typeclasses are available to only work with BSON documents: BSONDocumentWriter
and BSONDocumentReader
.
Custom reader
Getting values follows the same principle using getAsTry(String)
method. This method is parametrized with a type that can be transformed into a BSONValue
using a BSONReader
instance that is implicitly available in the scope (again, the default readers are already imported if you imported reactivemongo.api.bson._
.) If the value could not be found, or if the reader could not deserialize it (often because the type did not match), None
will be returned.
import reactivemongo.api.bson.BSONString
val albumTitle2 = album2.getAsTry[String]("title")
// Some("Everybody Knows this is Nowhere")
val albumTitle3 = album2.getAsTry[BSONString]("title")
// Some(BSONString("Everybody Knows this is Nowhere"))
In order to read values of custom types, a custom instance of BSONReader
, or of BSONDocumentReader
, must be resolved (in the implicit scope).
A BSONReader
for a custom class:
package object custom {
class Score(val value: Float)
import reactivemongo.api.bson._
implicit val scoreReader: BSONReader[Score] = BSONReader.from[Score] { bson =>
bson.asTry[BSONNumberLike].flatMap(_.toFloat).map(new Score(_))
}
}
Once a custom BSONReader
(or BSONDocumentReader
) is defined, it can thus be used in aDocument.getAsTry[MyValueType]("docProperty")
.
A BSONDocumentReader
for a custom case class:
import reactivemongo.api.bson._
val personReader1: BSONDocumentReader[Person] =
BSONDocumentReader.from[Person] { bson =>
for {
name <- bson.getAsTry[String]("fullName")
age <- bson.getAsTry[BSONNumberLike]("personAge").flatMap(_.toInt)
} yield new Person(name, age)
}
Once a custom BSONDocumentReader
can be resolved, it can be used when working with a query result.
import scala.concurrent.{ ExecutionContext, Future }
import reactivemongo.api.bson.{ BSONDocument, BSONDocumentReader }
import reactivemongo.api.bson.collection.BSONCollection
// Provided the `Person` case class is defined, with its `BSONDocumentReader`
implicit def personReader2: BSONDocumentReader[Person] = ???
def findPerson(personCollection: BSONCollection, name: String)(implicit ec: ExecutionContext): Future[Option[Person]] = personCollection.find(BSONDocument("fullName" -> name)).one[Person]
Custom writer
It’s also possible to write a value of a custom type, a custom instance of BSONWriter
, or of BSONDocumentWriter
must be available.
import reactivemongo.api.bson._
case class Score(value: Float)
implicit val scoreWriter1: BSONWriter[Score] =
BSONWriter.from[Score] { score =>
scala.util.Success(BSONDouble(score.value))
}
// Uses `BSONDouble` to write `Float`,
// for compatibility with MongoDB numeric values
Each value that can be written using a BSONWriter
can be used directly when calling a BSONDocument
constructor.
val album2 = reactivemongo.api.bson.BSONDocument(
"title" -> "Everybody Knows this is Nowhere",
"releaseYear" -> 1969)
Note that this does not use implicit conversions, but rather implicit type classes.
import reactivemongo.api.bson._
// Declare it as implicit for resolution
val personWriter0: BSONDocumentWriter[Person] =
BSONDocumentWriter[Person] { person =>
BSONDocument("fullName" -> person.name, "personAge" -> person.age)
}
Once a BSONDocumentWriter
is available, an instance of the custom class can be inserted or updated to the MongoDB.
import scala.concurrent.{ ExecutionContext, Future }
import reactivemongo.api.bson.BSONDocumentWriter
import reactivemongo.api.bson.collection.BSONCollection
// Provided the `Person` case class is defined, with its `BSONDocumentWriter`
implicit def personWriter: BSONDocumentWriter[Person] = ???
def create(personCollection: BSONCollection, person: Person)(implicit ec: ExecutionContext): Future[Unit] = {
val writeResult = personCollection.insert.one(person)
writeResult.map(_ => {/*once this is successful, just return successfully*/})
}
Utility factories
Some factories are available to create handlers for common types.
Iterable:
Factories to handle BSON array are provided: { BSONReader, BSONWriter }.{ iterable, sequence }
import reactivemongo.api.bson.{ BSONReader, BSONWriter, Macros }
case class Element(str: String, v: Int)
val elementHandler = Macros.handler[Element]
val setReader: BSONReader[Set[Element]] =
BSONReader.iterable[Element, Set](elementHandler readTry _)
val seqWriter: BSONWriter[Seq[Element]] =
BSONWriter.sequence[Element](elementHandler writeTry _)
// ---
import reactivemongo.api.bson.{ BSONArray, BSONDocument }
val fixture = BSONArray(
BSONDocument("str" -> "foo", "v" -> 1),
BSONDocument("str" -> "bar", "v" -> 2))
setReader.readTry(fixture)
// Success: Set(Element("foo", 1), Element("bar", 2))
seqWriter.writeTry(Seq(Element("foo", 1), Element("bar", 2)))
// Success: fixture
Tuples:
Factories to create handler for tuple types (up to 5-arity) are provided.
If an array is the wanted BSON representation:
import reactivemongo.api.bson.{ BSONArray, BSONReader, BSONWriter }
val readerArrAsStrInt = BSONReader.tuple2[String, Int]
val writerStrIntToArr = BSONWriter.tuple2[String, Int]
val arr = BSONArray("Foo", 20)
readerArrAsStrInt.readTry(arr) // => Success(("Foo", 20))
writerStrIntToArr.writeTry("Foo" -> 20)
// => Success: arr = ['Foo', 20]
If a document representation is wanted:
import reactivemongo.api.bson.{
BSONDocument, BSONDocumentReader, BSONDocumentWriter
}
val writerStrIntToDoc = BSONDocumentWriter.tuple2[String, Int]("name", "age")
writerStrIntToDoc.writeTry("Foo" -> 20)
// => Success: {'name': 'Foo', 'age': 20}
val readerDocAsStrInt = BSONDocumentReader.tuple2[String, Int]("name", "age")
reader.readTry(BSONDocument("name" -> "Foo", "age" -> 20))
// => Success(("Foo", 20))
Partial function:
There new factories based on partial functions: collect
and collectFrom
.
BSON reader:
import reactivemongo.api.bson.{ BSONReader, BSONInteger }
val intToStrCodeReader = BSONReader.collect[String] {
case BSONInteger(0) => "zero"
case BSONInteger(1) => "one"
}
intToStrCodeReader.readTry(BSONInteger(0)) // Success("zero")
intToStrCodeReader.readTry(BSONInteger(1)) // Success("one")
intToStrCodeReader.readTry(BSONInteger(2))
// => Failure(ValueDoesNotMatchException(..))
intToStrCodeReader.readOpt(BSONInteger(3)) // None (as failed)
BSON writer:
import scala.util.Success
import reactivemongo.api.bson.{ BSONWriter, BSONInteger }
val strCodeToIntWriter0 = BSONWriter.collect[String] {
case "zero" => BSONInteger(0)
case "one" => BSONInteger(1)
}
val strCodeToIntWriter1 = BSONWriter.collectFrom[String] {
case "zero" => Success(BSONInteger(0))
case "one" => Success(BSONInteger(1))
}
strCodeToIntWriter1.writeTry("zero") // Success(BSONInteger(0))
strCodeToIntWriter1.writeTry("one") // Success(BSONInteger(1))
strCodeToIntWriter1.writeTry("3")
// => Failure(IllegalArgumentException(..))
strCodeToIntWriter1.writeOpt("4") // None (as failed)
BSON document writer:
import reactivemongo.api.bson.{ BSONDocument, BSONDocumentWriter }
case class Bar(value: String)
val writer1 = BSONDocumentWriter.collect[Bar] {
case Bar(value) if value.nonEmpty =>
BSONDocument("value" -> value)
}
val writer2 = BSONDocumentWriter.collectFrom[Bar] {
case Bar(value) if value.nonEmpty =>
scala.util.Success(BSONDocument("value" -> value))
}
Macros
To ease the implementation of readers or writers for your custom types (case classes and sealed traits), ReactiveMongo provides some helper Macros.
case class Person(name: String, age: Int)
import reactivemongo.api.bson._
val personHandler: BSONDocumentHandler[Person] = Macros.handler[Person]
// Or only ...
val separatePersonReader: BSONDocumentReader[Person] = Macros.reader[Person]
val separatePersonWriter: BSONDocumentWriter[Person] = Macros.writer[Person]
The BSONHandler
provided by Macros.handler
gathers both BSONReader
and BSONWriter
traits.
The Macros.reader
can be used to generate only the BSONReader
, while the Macros.writer
is for BSONWriter
.
The A
type parameter (e.g. with A
being Person
, Macros.reader[Person]
) defines a type for a case class, or for a sealed trait with subclasses.
This type will be the basis for the auto-generated implementation.
Some other types with matching
apply
-unapply
might work but behaviour is undefined. Since macros will match theapply
-unapply
pair you are free to overload these methods in the companion object.
Case class mapping:
For the case classes, the fields get mapped into BSON properties with respective names, and BSON handlers are pulled from implicit scope to (de)serialize them (in the previous Person
example, the handlers for String
are resolved for the name
property).
So in order to use custom types as properties in case classes, the appropriate handlers are in scope.
For example if you have case class Foo(bar: Bar)
and want to create a handler for it is enough to put an implicit handler for Bar
in it’s companion object. That handler might itself be macro generated, or written by hand.
The macros are currently limited to case classes whose constructor doesn’t take more than 22 parameters (due to Scala not generating
apply
andunapply
in the other cases).
The default values for the class properties can be used by BSON reader when the corresponding BSON value is missing, with MacroOptions.ReadDefaultValues
.
import reactivemongo.api.bson.{
BSONDocument, BSONDocumentReader, Macros, MacroOptions
}
case class FooWithDefault1(id: Int, title: String = "default")
{
val reader: BSONDocumentReader[FooWithDefault1] =
Macros.using[MacroOptions.ReadDefaultValues].reader[FooWithDefault1]
reader.readTry(BSONDocument("id" -> 1)) // missing BSON title
// => Success: FooWithDefault1(id = 1, title = "default")
}
Sealed trait and union types:
Sealed traits are also supported as union types, with each of their subclasses considered as a disjoint case.
import reactivemongo.api.bson.{ BSONHandler, Macros }
sealed trait Tree
case class Node(left: Tree, right: Tree) extends Tree
case class Leaf(data: String) extends Tree
object Tree {
implicit val node = Macros.handler[Node]
implicit val leaf = Macros.handler[Leaf]
implicit val bson: BSONHandler[Tree] = Macros.handler[Tree]
}
The handler
, reader
and writer
macros each have a corresponding extended macro: readerOpts
, writerOpts
and handlerOpts
.
These ‘Opts’ suffixed macros can be used to explicitly define the UnionType
.
sealed trait Color
case object Red extends Color
case object Blue extends Color
case class Green(brightness: Int) extends Color
case class CustomColor(code: String) extends Color
object Color {
import reactivemongo.api.bson.Macros
import reactivemongo.api.bson.MacroOptions.{
AutomaticMaterialization, UnionType, \/
}
// Use `UnionType` to define a subset of the `Color` type,
type PredefinedColor =
UnionType[Red.type \/ Green \/ Blue.type] with AutomaticMaterialization
val predefinedColor = Macros.handlerOpts[Color, PredefinedColor]
}
As for the UnionType
definition, Foo \/ Bar \/ Baz
is interpreted as type Foo
or type Bar
or type Baz
. The option AutomaticMaterialization
is used there to automatically try to materialize the handlers for the sub-types (disabled by default).
The other options available to configure the typeclasses generation at compile time are the following.
Verbose
: Print out generated code during compilation.SaveClassName
: Indicate to theBSONWriter
to add a “className” field in the written document along with the other properties. The value for this meta field is the fully qualified name of the class. This is the default behaviour when the target type is a sealed trait (the “className” field is used as discriminator).
Value classes
Specific macros are new available for Value classes (any type which complies with <: AnyVal
).
package object values {
import reactivemongo.api.bson.{ BSONHandler, BSONReader, BSONWriter, Macros }
final class FooVal(val value: String) extends AnyVal
val vh: BSONHandler[FooVal] = Macros.valueHandler[FooVal]
val vr: BSONReader[FooVal] = Macros.valueReader[FooVal]
val vw: BSONWriter[FooVal] = Macros.valueWriter[FooVal]
}
Configuration
This macro utilities offer 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]
}
Annotations
Some annotations are also available to configure the macros.
The @Key
annotation allows to specify the field name for a class property.
For example, it is convenient to use when you’d like to leverage the MongoDB _id
index but you don’t want to actually use _id
in your code.
import reactivemongo.api.bson.Macros.Annotations.Key
case class Website(@Key("_id") url: String)
// Generated handler will map the `url` field in your code to as `_id` field
The @Ignore
can be applied on the class properties to be ignored.
import reactivemongo.api.bson.Macros.Annotations.Ignore
case class Foo(
bar: String,
@Ignore lastAccessed: Long = -1L
)
When a field annotated with @Ignore
must be read (using Macros.reader
or Macros.handler
), then a default value must be defined for this field, either using standard Scala syntax (in previous example ` = -1) or using
@DefaultValue` annotation (see below).
The @Flatten
can be used to indicate to the macros that the representation of a property must be flatten rather than a nested document.
import reactivemongo.api.bson.BSONDocument
import reactivemongo.api.bson.Macros.Annotations.Flatten
case class Range(start: Int, end: Int)
case class LabelledRange(
name: String,
@Flatten range: Range)
// Flattened with macro as bellow:
BSONDocument("name" -> "foo", "start" -> 0, "end" -> 1)
// Rather than:
// BSONDocument("name" -> "foo", "range" -> BSONDocument(
// "start" -> 0, "end" -> 1))
The @DefaultValue
can be used with MacroOptions.ReadDefaultValues
to specify a default value only used when reading from BSON.
import reactivemongo.api.bson.{
BSONDocument, BSONDocumentReader, Macros, MacroOptions
}
import Macros.Annotations.DefaultValue
case class FooWithDefault2(
id: Int,
@DefaultValue("default") title: String)
{
val reader: BSONDocumentReader[FooWithDefault2] =
Macros.using[MacroOptions.ReadDefaultValues].reader[FooWithDefault2]
reader.readTry(BSONDocument("id" -> 1)) // missing BSON title
// => Success: FooWithDefault2(id = 1, title = "default")
}
The @Reader
allows to indicate a specific BSON reader that must be used for a property, instead of resolving such reader from the implicit scope.
import reactivemongo.api.bson.{
BSONDocument, BSONDouble, BSONString, BSONReader
}
import reactivemongo.api.bson.Macros,
Macros.Annotations.Reader
val scoreReader: BSONReader[Double] = BSONReader.collect[Double] {
case BSONString(v) => v.toDouble
case BSONDouble(b) => b
}
case class CustomFoo1(
title: String,
@Reader(scoreReader) score: Double)
val reader = Macros.reader[CustomFoo1]
reader.readTry(BSONDocument(
"title" -> "Bar",
"score" -> "1.23" // accepted by annotated scoreReader
))
// Success: CustomFoo1(title = "Bar", score = 1.23D)
In a similar way, the @Writer
allows to indicate a specific BSON writer that must be used for a property, instead of resolving such writer from the implicit scope.
import reactivemongo.api.bson.{ BSONString, BSONWriter }
import reactivemongo.api.bson.Macros,
Macros.Annotations.Writer
val scoreWriter2: BSONWriter[Double] = BSONWriter[Double] { d =>
BSONString(d.toString) // write double as string
}
case class CustomFoo2(
title: String,
@Writer(scoreWriter2) score: Double)
val writer = Macros.writer[CustomFoo2]
writer.writeTry(CustomFoo2(title = "Bar", score = 1.23D))
// Success: BSONDocument("title" -> "Bar", "score" -> "1.23")
Troubleshooting:
The mapped type can also be defined inside other classes, objects or traits but not inside functions (known macro limitation). In order to work you should have the case class in scope (where you call the macro), so you can refer to it by it’s short name - without package. This is necessary because the generated implementations refer to it by the short name to support nested declarations. You can work around this with local imports.
object lorem {
case class Ipsum(v: String)
}
implicit val handler = {
import lorem.Ipsum
reactivemongo.api.bson.Macros.handler[Ipsum]
}
Provided handlers
The following handlers are provided by ReactiveMongo, to read and write the BSON values.
BSON type | Value type |
---|---|
BSONArray | Any collection |
BSONBinary | Array[Byte] |
BSONBoolean | Boolean |
BSONDocument | Map[K, V] |
BSONDateTime | java.time.Instant |
BSONDouble | Double |
BSONInteger | Int |
BSONLong | Long |
BSONString | String |
Optional value
An optional value can be added to a document using the Option
type (e.g. for an optional string, Option[String]
).
Using BSONBooleanLike
, it is possible to read the following BSON values as boolean.
BSON type | Rule |
---|---|
BSONInteger | true if > 0 |
BSONDouble | true if > 0 |
BSONNull | always false |
BSONUndefined | always false |
Using BSONNumberLike
, it is possible to read the following BSON values as number.
BSONInteger
BSONLong
BSONDouble
BSONDateTime
: the number of milliseconds since epoch.BSONTimestamp
: the number of milliseconds since epoch.
Map
handler
A handler is available to write and read Scala Map
as BSON, provided the value types are supported.
import scala.util.Try
import reactivemongo.api.bson._
def bsonMap = {
val input: Map[String, Int] = Map("a" -> 1, "b" -> 2)
// Ok as key and value (String, Int) are provided BSON handlers
val doc: Try[BSONDocument] = BSON.writeDocument(input)
val output = doc.flatMap { BSON.readDocument[Map[String, Int]](_) }
}
For cases where you can to serialize a Map
whose key type is not String
(which is required for BSON document keys), the typeclasses KeyWriter
and KeyReader
can be used.
import scala.util.Try
import reactivemongo.api.bson._
final class FooKey(val value: String)
object FooKey {
val bar = new FooKey("bar")
val lorem = new FooKey("lorem")
implicit val keyWriter: KeyWriter[FooKey] = KeyWriter[FooKey](_.value)
implicit val keyReader: KeyReader[FooKey] =
KeyReader[FooKey] { new FooKey(_) }
}
def bsonMapCustomKey = {
val input: Map[FooKey, Int] = Map(
FooKey.bar -> 1, FooKey.lorem -> 2)
// Ok as key and value (String, Int) are provided BSON handlers
val doc: Try[BSONDocument] = BSON.writeDocument(input)
val output = doc.flatMap { BSON.readDocument[Map[FooKey, Int]](_) }
}
Concrete examples
Troubleshooting
Make sure an instance of KeyReader
(or KeyWriter
) can be resolved from the implicit scope for the key type.
could not find implicit value for parameter e: reactivemongo.api.bson.BSONDocumentReader[Map[..not string..,String]]
Previous: Overview of the ReactiveMongo BSON library / Next: BSON extra libraries