Skip to content

Instantly share code, notes, and snippets.

@yujikiriki
Forked from almeidap/BaseDAO.scala
Created April 5, 2014 19:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yujikiriki/9996830 to your computer and use it in GitHub Desktop.
Save yujikiriki/9996830 to your computer and use it in GitHub Desktop.
package core.dao
import scala.concurrent.Future
import play.api.Logger
import reactivemongo.core.commands.LastError
import reactivemongo.core.errors.DatabaseException
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 10148 => {
Left(OperationNotAllowedException("", nestedException = e))
}
case 11000 => {
Left(DuplicateResourceException(nestedException = e))
}
}
})
}
}
handling.getOrElse(Left(UnexpectedServiceException(exception.getMessage, nestedException = exception)))
}
}
}
package core.helpers
import scala.concurrent.ExecutionContext
/**
* Helper around implicit contexts.
*
* @author Pedro De Almeida (almeidap)
*/
trait ContextHelper {
implicit def ec: ExecutionContext = ExecutionContext.Implicits.global
}
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[T](field: String, data: T)(implicit writer: Writes[T]): JsObject = set(Json.obj(field -> data))
def set(data: JsObject): JsObject = Json.obj("$set" -> 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))
def inc(field: String, amount: Int) = Json.obj("$inc" -> Json.obj(field -> amount))
def or(criterias: JsObject*): JsObject = Json.obj("$or" -> criterias)
def gt[T](field: String, value: T)(implicit writer: Writes[T]) = Json.obj(field -> Json.obj("$gt" -> value))
def lt[T](field: String, value: T)(implicit writer: Writes[T]) = Json.obj(field -> Json.obj("$lt" -> value))
def query[T](query: T)(implicit writer: Writes[T]): JsObject = Json.obj("$query" -> query)
def orderBy[T](query: T)(implicit writer: Writes[T]): JsObject = Json.obj("$orderby" -> query)
}
package core.dao
import org.joda.time.DateTime
import scala.concurrent.Future
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
/**
* 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]")
collection.find(query).cursor[T].collect[List]()
}
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, query: JsObject): Future[Either[ServiceException, JsObject]] = {
val data = updated(query)
Logger.debug(s"Updating by query: [collection=$collectionName, id=$id, query=$data]")
Recover(collection.update(DBQueryBuilder.id(id), data)) {
data
}
}
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(data: JsObject) = {
data.validate((__ \ '$set).json.update(
__.read[JsObject].map{ o => o ++ Json.obj("updated" -> DateTime.now) }
)).fold(
error => data,
success => success
)
}
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.dao
import scala.concurrent.Future
import play.api.Logger
import play.api.libs.json.{Reads, Json, JsObject}
import reactivemongo.api.Cursor
import reactivemongo.api.gridfs.{ReadFile, GridFS}
import reactivemongo.bson.{BSONValue, BSONObjectID}
import core.db.DBQueryBuilder
import core.exceptions.ServiceException
/* Implicits */
import play.modules.reactivemongo.json.ImplicitBSONHandlers._
import reactivemongo.api.gridfs.Implicits.DefaultReadFileReader
/**
* DAO for MongoDB `GridFS` files.
*
* @author Pedro De Almeida (almeidap)
*/
trait FileDAO extends BaseDAO {
lazy val gfs = GridFS(db, collectionName)
def find(query: JsObject = Json.obj()): Cursor[ReadFile[BSONValue]] = {
Logger.debug(s"Finding files: [collection=$collectionName, query=$query]")
gfs.find(query)
}
def findById(id: String): Future[Option[ReadFile[BSONValue]]] = find(DBQueryBuilder.id(id)).headOption
def findOne(query: JsObject = Json.obj()): Future[Option[ReadFile[BSONValue]]] = {
Logger.debug(s"Finding one file: [collection=$collectionName, query=$query]")
gfs.find(query).headOption
}
def removeById(id: String): Future[Either[ServiceException, Boolean]] = {
Recover(gfs.remove(BSONObjectID(id))) {
true
}
}
override def ensureIndexes = {
// Let's build an index on our gridfs chunks collection if none:
gfs.ensureIndex.map {
case status =>
Logger.info(s"GridFS index: [collection=$collectionName, status=$status]")
List(status)
}
}
}
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 core.helpers.ContextHelper
/**
* Helper around `MongoDB` resources.
*
* @author Pedro De Almeida (almeidap)
*/
trait MongoHelper extends ContextHelper{
lazy val db = ReactiveMongoPlugin.db
}
object MongoHelper extends MongoHelper {
def identify(bson: BSONValue) = bson.asInstanceOf[BSONObjectID].stringify
}
package core.exceptions
/**
* Trait for service exceptions.
*
* @author Pedro De Almeida (almeidap)
*/
trait ServiceException extends Exception {
val message: String
val nestedException: Throwable
}
package core.exceptions
import reactivemongo.core.commands.LastError
import core.exceptions.ServiceException
/**
* Service-related exceptions.
*
* @author Pedro De Almeida (almeidap)
*/
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