Skip to content

Instantly share code, notes, and snippets.

@anatolyg
Forked from almeidap/BaseDAO.scala
Created July 19, 2013 20:31
Show Gist options
  • Save anatolyg/6042119 to your computer and use it in GitHub Desktop.
Save anatolyg/6042119 to your computer and use it in GitHub Desktop.
package core.dao
import play.api.Logger
import reactivemongo.core.commands.LastError
import reactivemongo.core.errors.DatabaseException
import scala.concurrent.Future
import core.db.MongoHelper
import core.exceptions._
/* Implicits */
import play.modules.reactivemongo.json.BSONFormats._
/**
* Base DAO for MongoDB resources.
*
* @author Pedro De Almeida (almeidap)
*/
trait BaseDAO extends MongoHelper {
val collectionName: String
def ensureIndexes: Future[List[Boolean]]
def Recover[S](operation: Future[LastError])(success: => S): Future[Either[ServiceException, S]] = {
operation.map {
lastError => lastError.inError match {
case true => {
Logger.error(s"DB operation did not perform successfully: [lastError=$lastError]")
Left(DBServiceException(lastError))
}
case false => {
Right(success)
}
}
} recover {
case exception =>
Logger.error(s"DB operation failed: [message=${exception.getMessage}]")
// TODO: better failure handling here
val handling: Option[Either[ServiceException, S]] = exception match {
case e: DatabaseException => {
e.code.map(code => {
Logger.error(s"DatabaseException: [code=${code}, isNotAPrimaryError=${e.isNotAPrimaryError}]")
code match {
case 11000 => {
Left(DuplicateResourceException(nestedException = e))
}
case 10148 => {
Left(OperationNotAllowedException("", nestedException = e))
}
}
})
}
}
handling.getOrElse(Left(UnexpectedServiceException(exception.getMessage, nestedException = exception)))
}
}
}
package core.db
import play.api.libs.json.{Writes, Json, JsObject}
import reactivemongo.bson.BSONObjectID
/* Implicits */
import play.modules.reactivemongo.json.BSONFormats._
/**
* Query builder wrapping common queries and MongoDB operators.
*
* TODO: create a real query `builder`
*
* @author Pedro De Almeida (almeidap)
*/
object DBQueryBuilder {
def id(objectId: String): JsObject = id(BSONObjectID(objectId))
def id(objectId: BSONObjectID): JsObject = Json.obj("_id" -> objectId)
def set(field: String, data: JsObject): JsObject = set(Json.obj(field -> data))
def set(data: JsObject): JsObject = Json.obj("$set" -> data)
def set[T](field: String, data: T)(implicit writer: Writes[T]): JsObject = set(Json.obj(field -> data))
def set[T](data: T)(implicit writer: Writes[T]): JsObject = Json.obj("$set" -> data)
def push[T](field: String, data: T)(implicit writer: Writes[T]): JsObject = Json.obj("$push" -> Json.obj(field -> data))
def pull[T](field: String, query: T)(implicit writer: Writes[T]): JsObject = Json.obj("$pull" -> Json.obj(field -> query))
def unset(field: String): JsObject = Json.obj("$unset" -> Json.obj(field -> 1))
}
package core.dao
import play.api.Logger
import play.api.libs.json._
import play.modules.reactivemongo.json.collection.JSONCollection
import reactivemongo.api.indexes.{Index, IndexType}
import reactivemongo.bson._
import core.db.DBQueryBuilder
import core.exceptions.ServiceException
import core.models.TemporalModel
import org.joda.time.DateTime
import scala.concurrent.Future
/**
* DAO for MongoDB documents.
*
* @author Pedro De Almeida (almeidap)
*/
trait DocumentDAO[T <: TemporalModel] extends BaseDAO {
lazy val collection = db.collection[JSONCollection](collectionName)
def insert(document: T)(implicit writer: Writes[T]): Future[Either[ServiceException, T]] = {
document._id = Some(BSONObjectID.generate)
document.created = Some(DateTime.now)
document.updated = Some(DateTime.now)
Logger.debug(s"Inserting document: [collection=$collectionName, data=$document]")
Recover(collection.insert(document)) {
document
}
}
def find(query: JsObject = Json.obj())(implicit reader: Reads[T]): Future[List[T]] = {
Logger.debug(s"Finding documents: [collection=$collectionName, query=$query, c=$collection]")
collection.find(query).cursor[T].toList
}
def findById(id: String)(implicit reader: Reads[T]): Future[Option[T]] = findOne(DBQueryBuilder.id(id))
def findById(id: BSONObjectID)(implicit reader: Reads[T]): Future[Option[T]] = findOne(DBQueryBuilder.id(id))
def findOne(query: JsObject)(implicit reader: Reads[T]): Future[Option[T]] = {
Logger.debug(s"Finding one: [collection=$collectionName, query=$query]")
collection.find(query).one[T]
}
def update(id: String, document: T)(implicit writer: Writes[T]): Future[Either[ServiceException, T]] = {
document.updated = Some(new DateTime())
Logger.debug(s"Updating document: [collection=$collectionName, id=$id, document=$document]")
Recover(collection.update(DBQueryBuilder.id(id), DBQueryBuilder.set(document))) {
document
}
}
def update(id: String, data: JsObject): Future[Either[ServiceException, JsObject]] = {
val update = data ++ updated
Logger.debug(s"Updating data: [collection=$collectionName, id=$id, update=$update]")
Recover(collection.update(DBQueryBuilder.id(id), DBQueryBuilder.set(update))) {
update
}
}
def push[S](id: String, field: String, data: S)(implicit writer: Writes[S]): Future[Either[ServiceException, S]] = {
Logger.debug(s"Pushing to document: [collection=$collectionName, id=$id, field=$field data=$data]")
Recover(collection.update(DBQueryBuilder.id(id), DBQueryBuilder.push(field, data)
)) {
data
}
}
def pull[S](id: String, field: String, query: S)(implicit writer: Writes[S]): Future[Either[ServiceException, Boolean]] = {
Logger.debug(s"Pulling from document: [collection=$collectionName, id=$id, field=$field query=$query]")
Recover(collection.update(DBQueryBuilder.id(id), DBQueryBuilder.pull(field, query))) {
true
}
}
def unset(id: String, field: String): Future[Either[ServiceException, Boolean]] = {
Logger.debug(s"Unsetting from document: [collection=$collectionName, id=$id, field=$field]")
Recover(collection.update(DBQueryBuilder.id(id), DBQueryBuilder.unset(field))) {
true
}
}
def remove(id: String): Future[Either[ServiceException, Boolean]] = remove(BSONObjectID(id))
def remove(id: BSONObjectID): Future[Either[ServiceException, Boolean]] = {
Logger.debug(s"Removing document: [collection=$collectionName, id=$id]")
Recover(
collection.remove(DBQueryBuilder.id(id))
) {
true
}
}
def remove(query: JsObject, firstMatchOnly: Boolean = false): Future[Either[ServiceException, Boolean]] = {
Logger.debug(s"Removing document(s): [collection=$collectionName, firstMatchOnly=$firstMatchOnly, query=$query]")
Recover(
collection.remove(query, firstMatchOnly = firstMatchOnly)
) {
true
}
}
def updated = Json.obj("updated" -> DateTime.now)
def ensureIndex(
key: List[(String, IndexType)],
name: Option[String] = None,
unique: Boolean = false,
background: Boolean = false,
dropDups: Boolean = false,
sparse: Boolean = false,
version: Option[Int] = None,
options: BSONDocument = BSONDocument()) = {
val index = Index(key, name, unique, background, dropDups, sparse, version, options)
Logger.info(s"Ensuring index: $index")
collection.indexesManager.ensure(index)
}
}
package core.models
import reactivemongo.bson.BSONObjectID
/**
* Base model for `identifiable` documents.
*
* @author Pedro De Almeida (almeidap)
*/
trait IdentifiableModel {
var _id: Option[BSONObjectID]
def identify = _id.map(value => value.stringify).getOrElse("")
}
package core.db
import play.api.Play.current
import play.modules.reactivemongo.ReactiveMongoPlugin
import reactivemongo.bson.{BSONObjectID, BSONValue}
import scala.concurrent.ExecutionContext
/**
* Helper around `MongoDB` resources.
*
* @author Pedro De Almeida (almeidap)
*/
trait MongoHelper {
implicit def ec: ExecutionContext = ExecutionContext.Implicits.global
lazy val db = ReactiveMongoPlugin.db
}
object MongoHelper extends MongoHelper {
def identify(bson: BSONValue) = bson.asInstanceOf[BSONObjectID].stringify
}
package core.exceptions
import reactivemongo.core.commands.LastError
/**
* Service-related exceptions.
*
* @author Pedro De Almeida (almeidap)
*/
trait ServiceException extends Exception {
val message: String
val nestedException: Throwable
}
case class UnexpectedServiceException(
message: String,
nestedException: Throwable = null
) extends ServiceException
case class DBServiceException(
message: String,
lastError: Option[LastError] = None,
nestedException: Throwable = null
) extends ServiceException
object DBServiceException {
def apply(lastError: LastError): ServiceException = {
DBServiceException(lastError.errMsg.getOrElse(lastError.message), Some(lastError))
}
}
case class DuplicateResourceException(
message: String = "error.duplicate.resource",
nestedException: Throwable = null
) extends ServiceException
case class OperationNotAllowedException(
message: String = "error.operation.not.allowed",
nestedException: Throwable = null
) extends ServiceException
case class ResourceNotFoundException(
id: String,
message: String = "error.resource.not.found",
nestedException: Throwable = null
) extends ServiceException
package core.models
import org.joda.time.DateTime
/**
* Base model for `temporal` documents.
*
* @author Pedro De Almeida (almeidap)
*/
trait TemporalModel extends IdentifiableModel {
var created: Option[DateTime]
var updated: Option[DateTime]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment