'How to build a simple MongoDB DAO in Scala using SalatDAO' post illustration

How to build a simple MongoDB DAO in Scala using SalatDAO

avatar

Usually a data access object for MongoDB consists of common routine CRUD methods. Those methods should be implemented, tested, maintained just like any other code. In this post, I'm going to show you how to use SalatDAO to vastly simplify the process.

Generally, Salat is used for serialization of case classes, enums and collections to MongoDBObject (a key-value map which can be saved to the database) and back. Salat uses Casbah library as an interface for MongoDB. Also, Salat provides the SalatDAO class. By extending this class, you can add a set of basic CRUD methods to your DAO. See these methods with comments here. Also, wiki might be helpful.

Let me demonstrate the suggested approach on the example of the DAO for the Chocolate case class.

Here is the Chocolate case class:

1
2
3
4
5
6
import com.mongodb.casbah.Imports.ObjectId

case class Chocolate(_id: ObjectId = new ObjectId,
                     name: String,
                     ingredients: String,
                     producer: String)

And here is the data access object for the Chocolate:

1
2
3
4
5
6
7
import com.mongodb.casbah.Imports._
import com.novus.salat.dao.SalatDAO
import com.novus.salat.global._
import com.sysgears.example.domain.Chocolate

class ChocolateDAO extends SalatDAO[Chocolate, ObjectId]
    (collection= MongoConnection()("chocolate_base")("chocolate"))

In order to extend SalatDAO, you need to specify the case class type (Chocolate), the id field type (ObjectId) and initialize the MongoDB collection. In this example "chocolate_base" is a MongoDB database name, and "chocolate" - the collection name.

Now, ChocolateDAO contains all the methods from the BaseDAOMethods trait. However, many of those methods take MongoDBObject as a parameter. It is a flaw in this approach: using these DAO methods makes your code database specific. The easy and handy way to avoid this flaw is to use a case class with the implicit conversion instead of MongoDBObject.

Also, in order to build queries in an agile way, we will add a class that will be pretty similar to the Chocolate case class, but with a field type changed to Option and a default value set to None. That's how we will be able to search only by specific fields:

1
2
3
4
5
6
import com.mongodb.casbah.Imports.ObjectId

case class ChocolateQueryParams(_id: Option[ObjectId] = None,
                                 name: Option[String] = None,
                                 ingredients: Option[String] = None,
                                 producer: Option[String] = None)

Here is an example of implicit conversions for the Chocolate case class and ChocolateQueryParams:

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.mongodb.casbah.Imports._
import com.novus.salat._
import com.novus.salat.global._
import com.sysgears.example.domain.{Chocolate, ChocolateQueryParams}
import scala.language.implicitConversions

object ChocolateConversions {
  implicit def paramsToDBObject(params: ChocolateQueryParams): DBObject =
    grater[ChocolateQueryParams].asDBObject(params)

  implicit def chocolateToDBObject(c: Chocolate): DBObject =
    grater[Chocolate].asDBObject(c)
}

These conversions will help us to use case classes instead of MongoDBObject in SalatDAO method params.

Now, you can use all the SalatDAO methods in the much simpler way (see the example):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import com.sysgears.example.dao.ChocolateConversions._
import com.sysgears.example.dao.ChocolateDAO
import com.sysgears.example.domain.{Chocolate, ChocolateQueryParams}


object Main extends App {
  val chocolateDAO = new ChocolateDAO

  // Data for test:
  val entry = Chocolate(name = "Chocolate santa",
                        ingredients = "cacao, mushrooms, milk",
                        producer = "Kind Chocolate")
  val updateEntry = entry.copy(producer = "Wonka's Chocolate Factory")

  // Create:
  val id = chocolateDAO.insert(entry)
  assert(id.isDefined)

  // Read:
  val getResult = chocolateDAO.findOneById(id.get)
  assert(getResult.isDefined)
  assert(getResult.get == entry.copy(_id = id.get))

  // Update:
  chocolateDAO.update(ChocolateQueryParams(name = Some("Chocolate santa")),
                      updateEntry)
  assert(chocolateDAO.find(
            ChocolateQueryParams(producer = Some("Wonka's Chocolate Factory"))).nonEmpty)

  // Delete:
  chocolateDAO.removeById(id.get)
  assert(chocolateDAO.findOneById(id.get).isEmpty)
}

Basically, we've got CRUD methods for the DAO without implementing any of these methods. This approach will help you to avoid mistakes in the DAO code and save your time. Summarizing the flow, all you need to do is to:

  • create a case class that represents your model
  • add a similar case class (that will help to build queries) with a field type changed from X to Option[X] and set a default value to None
  • create DAO class that extends SalatDAO
  • implement implicit conversions to convert the created case classes into MongoDBObject

All the sources are available on the GitHub repository.

If you're looking for a developer or considering starting a new project,
we are always ready to help!