/salat-core/src/main/scala/com/novus/salat/dao/SalatDAO.scala
Scala | 523 lines | 206 code | 47 blank | 270 comment | 33 complexity | da0c112500cb44a9b663f96a611708f7 MD5 | raw file
Possible License(s): Apache-2.0
- /*
- * Copyright (c) 2010 - 2012 Novus Partners, Inc. (http://www.novus.com)
- *
- * Module: salat-core
- * Class: SalatDAO.scala
- * Last modified: 2012-12-05 12:24:48 EST
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * Project: http://github.com/novus/salat
- * Wiki: http://github.com/novus/salat/wiki
- * Mailing list: http://groups.google.com/group/scala-salat
- * StackOverflow: http://stackoverflow.com/questions/tagged/salat
- */
- package com.novus.salat.dao
- import com.mongodb.casbah.Imports._
- import com.mongodb.casbah.MongoCursorBase
- import com.novus.salat._
- import com.mongodb.casbah.commons.{ MongoDBObject, Logging }
- import com.mongodb.{ WriteConcern, DBObject }
- /** Sample DAO implementation.
- * @param collection MongoDB collection
- * @param mot implicit manifest for ObjectType
- * @param mid implicit manifest for ID
- * @param ctx implicit [[com.novus.salat.Context]]
- * @tparam ObjectType class to be persisted
- * @tparam ID _id type
- */
- abstract class SalatDAO[ObjectType <: AnyRef, ID <: Any](val collection: MongoCollection)(implicit mot: Manifest[ObjectType],
- mid: Manifest[ID], ctx: Context)
- extends com.novus.salat.dao.DAO[ObjectType, ID] with Logging {
- dao =>
- /** Supplies the [[com.novus.salat.Grater]] from the implicit [[com.novus.salat.Context]] and `ObjectType` manifest */
- val _grater = grater[ObjectType](ctx, mot)
- /** Force type hints when objects are persisted. Used to support a DAO typed to an abstract superclass or trait.
- * Should be overriden and forced to true when you want to select
- */
- val forceTypeHints = {
- val isProxy = _grater.isInstanceOf[ProxyGrater[_]]
- // safety check - if you never type hint, then deserializing using a proxy grater is impossible
- require(!isProxy || ctx.typeHintStrategy.when != TypeHintFrequency.Never,
- "Abstract class hierarchies cannot be deserialized when the context '%s' type hint strategy is NeverTypeHint".format(ctx.name))
- isProxy
- }
- /** If you are mixing and matching abstract and concrete DAOs, turn this on in the concrete DAOs to ensure that querying on a
- * mixed collection will only yield results in the child collection.
- */
- val appendTypeHintToQueries = false
- /** A central place to modify find, count and update queries before executing them.
- * @param query query to decorate
- * @return decorated query for execution
- */
- def decorateQuery(query: DBObject) = {
- if (appendTypeHintToQueries) {
- query(ctx.typeHintStrategy.typeHint) = ctx.typeHintStrategy.encode(_grater.clazz.getName).asInstanceOf[AnyRef]
- }
- query
- }
- /** A central place to modify DBOs before inserting, saving, or updating.
- * @param toPersist object to be serialized
- * @return decorated DBO for persisting
- */
- def decorateDBO(toPersist: ObjectType) = {
- val dbo = _grater.asDBObject(toPersist)
- if (forceTypeHints) {
- // take advantage of the mutability of DBObject by cramming in a type hint
- dbo(ctx.typeHintStrategy.typeHint) = ctx.typeHintStrategy.encode(toPersist.getClass.getName).asInstanceOf[AnyRef]
- }
- dbo
- }
- /** Inner abstract class to facilitate working with child collections using a typed parent id -
- * no cascading support will be offered, but you can override saves and deletes in the parent DAO
- * to manually cascade children as you like.
- *
- * Given parent class `Foo` and child class `Bar`:
- * {{{
- * case class Foo(_id: ObjectId, // etc )
- * case class Bar(_id: ObjectId,
- * parentId: ObjectId, // this refers back to a parent in Foo collection
- * // etc )
- *
- * object FooDAO extends SalatDAO[Foo, ObjectId](collection = MongoConnection()("db")("fooCollection")) {
- *
- * // and here is a child DAO you can use within FooDAO to work with children of type Bar whose parentId field matches
- * // the supplied parent id of an instance of Foo
- * val bar = new ChildCollection[Bar, ObjectId](collection = MongoConnection()("db")("barCollection"),
- * parentIdField = "parentId") { }
- *
- * }
- * }}}
- *
- * @param collection MongoDB collection
- * @param parentIdField parent id field key
- * @param mct implicit manifest for `ChildType`
- * @param mcid implicit manifest for `ChildID`
- * @param ctx implicit [[com.novus.salat.Context]]
- * @tparam ChildType type of child object
- * @tparam ChildID type of child _id field
- */
- abstract class ChildCollection[ChildType <: AnyRef, ChildID <: Any](override val collection: MongoCollection,
- val parentIdField: String)(implicit mct: Manifest[ChildType],
- mcid: Manifest[ChildID], ctx: Context)
- extends SalatDAO[ChildType, ChildID](collection) {
- childDao =>
- override lazy val description = "SalatDAO[%s,%s](%s) -> ChildCollection[%s,%s](%s)".format(
- mot.erasure.getSimpleName, mid.erasure.getSimpleName, dao.collection.name,
- mct.erasure.getSimpleName, mcid.erasure.getSimpleName, childDao.collection.name)
- /** @param parentId parent id
- * @return base query object for a single parent id
- */
- def parentIdQuery(parentId: ID): DBObject = {
- decorateQuery(MongoDBObject(parentIdField -> parentId))
- }
- /** @param parentIds list of parent ids
- * @return base query object for a list of parent ids
- * TODO - replace list with traversable
- */
- def parentIdsQuery(parentIds: List[ID]): DBObject = {
- MongoDBObject(parentIdField -> MongoDBObject("$in" -> parentIds))
- }
- /** Count the number of documents matching the parent id.
- * @param parentId parent id
- * @param query object for which to search
- * @param fieldsThatMustExist list of field keys that must exist
- * @param fieldsThatMustNotExist list of field keys that must not exist
- * @return count of documents matching the search criteria
- */
- def countByParentId(parentId: ID, query: DBObject = MongoDBObject.empty, fieldsThatMustExist: List[String] = Nil, fieldsThatMustNotExist: List[String] = Nil): Long = {
- childDao.count(parentIdQuery(parentId) ++ query, fieldsThatMustExist, fieldsThatMustNotExist)
- }
- /** @param parentId parent id
- * @param query object for which to search
- * @return list of child ids matching parent id and search criteria
- */
- def idsForParentId(parentId: ID, query: DBObject = MongoDBObject.empty): List[ChildID] = {
- childDao.collection.find(parentIdQuery(parentId) ++ query, MongoDBObject("_id" -> 1)).map(_.expand[ChildID]("_id").get).toList
- }
- /** @param parentIds list of parent ids
- * @param query object for which to search
- * @return list of child ids matching parent ids and search criteria
- */
- def idsForParentIds(parentIds: List[ID], query: DBObject = MongoDBObject.empty): List[ChildID] = {
- childDao.collection.find(parentIdsQuery(parentIds) ++ query, MongoDBObject("_id" -> 1)).map(_.expand[ChildID]("_id").get).toList
- }
- /** @param parentId parent id
- * @param query object for which to search
- * @return list of child objects matching parent id and search criteria
- */
- def findByParentId(parentId: ID, query: DBObject = MongoDBObject.empty): SalatMongoCursor[ChildType] = {
- childDao.find(parentIdQuery(parentId) ++ query)
- }
- /** @param parentIds list of parent ids
- * @param query object for which to search
- * @return list of child objects matching parent ids and search criteria
- */
- def findByParentIds(parentIds: List[ID], query: DBObject = MongoDBObject.empty): SalatMongoCursor[ChildType] = {
- childDao.find(parentIdsQuery(parentIds) ++ query)
- }
- /** @param parentId parent id
- * @param query object for which to search
- * @return list of child objects matching parent id and search criteria
- */
- def findByParentId(parentId: ID, query: DBObject, keys: DBObject): SalatMongoCursor[ChildType] = {
- childDao.find(parentIdQuery(parentId) ++ query, keys)
- }
- /** @param parentIds parent ids
- * @param query object for which to search
- * @return list of child objects matching parent ids and search criteria
- */
- def findByParentIds(parentIds: List[ID], query: DBObject, keys: DBObject): SalatMongoCursor[ChildType] = {
- childDao.find(parentIdsQuery(parentIds) ++ query, keys)
- }
- /** @param parentId parent id
- * @param o object with which to update the document(s) matching `parentId`
- * @param upsert if the database should create the element if it does not exist
- * @param multi if the update should be applied to all objects matching
- * @param wc write concern
- * @tparam A type view bound to DBObject
- */
- def updateByParentId[A <% DBObject](parentId: ID, o: A, upsert: Boolean, multi: Boolean, wc: WriteConcern = collection.writeConcern) {
- childDao.update(parentIdQuery(parentId), o, upsert, multi, wc)
- }
- /** @param parentIds parent ids
- * @param o object with which to update the document(s) matching `parentIds`
- * @param upsert if the database should create the element if it does not exist
- * @param multi if the update should be applied to all objects matching
- * @param wc write concern
- * @tparam A type view bound to DBObject
- */
- def updateByParentIds[A <% DBObject](parentIds: List[ID], o: A, upsert: Boolean, multi: Boolean, wc: WriteConcern = collection.writeConcern) {
- childDao.update(parentIdsQuery(parentIds), o, upsert, multi, wc)
- }
- /** Remove documents matching parent id
- * @param parentId parent id
- * @param wc write concern
- */
- def removeByParentId(parentId: ID, wc: WriteConcern = collection.writeConcern) {
- childDao.remove(parentIdQuery(parentId), wc)
- }
- /** Remove documents matching parent ids
- * @param parentIds parent ids
- * @param wc write concern
- */
- def removeByParentIds(parentIds: List[ID], wc: WriteConcern = collection.writeConcern) {
- childDao.remove(parentIdsQuery(parentIds), wc)
- }
- /** Projection typed to a case class, trait or abstract superclass.
- * @param parentId parent id
- * @param field field to project on
- * @param query (optional) object for which to search
- * @param mr implicit manifest typed to `R`
- * @param ctx implicit [[com.novus.salat.Context]]
- * @tparam R type of projected field
- * @return (List[R]) of the objects found
- */
- def projectionsByParentId[R <: CaseClass](parentId: ID, field: String, query: DBObject = MongoDBObject.empty)(implicit mr: Manifest[R], ctx: Context): List[R] = {
- childDao.projections(parentIdQuery(parentId) ++ query, field)(mr, ctx)
- }
- /** Projection typed to a case class, trait or abstract superclass.
- * @param parentIds parent ids
- * @param field field to project on
- * @param query (optional) object for which to search
- * @param mr implicit manifest typed to `R`
- * @param ctx implicit [[com.novus.salat.Context]]
- * @tparam R type of projected field
- * @return (List[R]) of the objects found
- */
- def projectionsByParentIds[R <: CaseClass](parentIds: List[ID], field: String, query: DBObject = MongoDBObject.empty)(implicit mr: Manifest[R], ctx: Context): List[R] = {
- childDao.projections(parentIdsQuery(parentIds) ++ query, field)(mr, ctx)
- }
- /** Projection typed to a type for which Casbah or mongo-java-driver handles conversion
- * @param parentId parent id
- * @param field field to project on
- * @param query (optional) object for which to search
- * @param mr implicit manifest typed to `R`
- * @param ctx implicit [[com.novus.salat.Context]]
- * @tparam R type of projected field
- * @return (List[R]) of the objects found
- */
- def primitiveProjectionsByParentId[R <: Any](parentId: ID, field: String, query: DBObject = MongoDBObject.empty)(implicit mr: Manifest[R], ctx: Context): List[R] = {
- childDao.primitiveProjections(parentIdQuery(parentId) ++ query, field)(mr, ctx)
- }
- /** Projection typed to a type for which Casbah or mongo-java-driver handles conversion
- * @param parentIds parent ids
- * @param field field to project on
- * @param query (optional) object for which to search
- * @param mr implicit manifest typed to `R`
- * @param ctx implicit [[com.novus.salat.Context]]
- * @tparam R type of projected field
- * @return (List[R]) of the objects found
- */
- def primitiveProjectionsByParentIds[R <: Any](parentIds: List[ID], field: String, query: DBObject = MongoDBObject.empty)(implicit mr: Manifest[R], ctx: Context): List[R] = {
- childDao.primitiveProjections(parentIdsQuery(parentIds) ++ query, field)(mr, ctx)
- }
- }
- /** Default description is the case class simple name and the collection.
- */
- override lazy val description = "SalatDAO[%s,%s](%s)".format(mot.erasure.getSimpleName, mid.erasure.getSimpleName, collection.name)
- /** @param t instance of ObjectType
- * @param wc write concern
- * @return if insert succeeds, ID of inserted object
- */
- def insert(t: ObjectType, wc: WriteConcern) = {
- val dbo = decorateDBO(t)
- val wr = collection.insert(dbo, wc)
- val error = wr.getCachedLastError
- if (error == null || (error != null && error.ok())) {
- dbo.getAs[ID]("_id")
- }
- else {
- throw SalatInsertError(description, collection, wc, wr, List(dbo))
- }
- }
- /** @param docs collection of `ObjectType` instances to insert
- * @param wc write concern
- * @return list of object ids
- * TODO: flatten list of IDs - why on earth didn't I do that in the first place?
- */
- def insert(docs: Traversable[ObjectType], wc: WriteConcern = defaultWriteConcern) = if (docs.nonEmpty) {
- val dbos = docs.map(decorateDBO(_)).toList
- val wr = collection.insert(dbos: _*)
- val lastError = wr.getCachedLastError
- if (lastError == null || (lastError != null && lastError.ok())) {
- dbos.map {
- dbo =>
- dbo.getAs[ID]("_id") orElse collection.findOne(dbo).flatMap(_.getAs[ID]("_id"))
- }
- }
- else {
- throw SalatInsertError(description, collection, wc, wr, dbos)
- }
- }
- else Nil
- /** @param query query
- * @tparam A type view bound to DBObject
- * @return list of IDs
- */
- def ids[A <% DBObject](query: A): List[ID] = {
- collection.find(decorateQuery(query), MongoDBObject("_id" -> 1)).map(_.expand[ID]("_id").get).toList
- }
- /** @param t object for which to search
- * @tparam A type view bound to DBObject
- * @return (Option[ObjectType]) Some() of the object found, or <code>None</code> if no such object exists
- */
- def findOne[A <% DBObject](t: A) = collection.findOne(decorateQuery(t)).map(_grater.asObject(_))
- /** @param id identifier
- * @return (Option[ObjectType]) Some() of the object found, or <code>None</code> if no such object exists
- */
- def findOneById(id: ID) = collection.findOneByID(id.asInstanceOf[AnyRef]).map(_grater.asObject(_))
- /** @param t object to remove from the collection
- * @param wc write concern
- * @return (WriteResult) result of write operation
- */
- def remove(t: ObjectType, wc: WriteConcern) = {
- val dbo = decorateDBO(t)
- val wr = collection.remove(dbo, wc)
- val lastError = wr.getCachedLastError
- if (lastError != null && !lastError.ok()) {
- throw SalatRemoveError(description, collection, wc, wr, List(dbo))
- }
- wr
- }
- /** @param q the object that documents to be removed must match
- * @param wc write concern
- * @return (WriteResult) result of write operation
- */
- def remove[A <% DBObject](q: A, wc: WriteConcern) = {
- val wr = collection.remove(q, wc)
- val lastError = wr.getCachedLastError
- if (lastError != null && !lastError.ok()) {
- throw SalatRemoveQueryError(description, collection, q, wc, wr)
- }
- wr
- }
- /** @param id the ID of the document to be removed
- * @param wc write concern
- * @return (WriteResult) result of write operation
- */
- def removeById(id: ID, wc: WriteConcern = defaultWriteConcern) = {
- remove(MongoDBObject("_id" -> id), wc)
- }
- /** @param ids the list of IDs identifying the list of documents to be removed
- * @param wc wrote concern
- * @return (WriteResult) result of write operation
- */
- def removeByIds(ids: List[ID], wc: WriteConcern) = {
- remove(MongoDBObject("_id" -> MongoDBObject("$in" -> MongoDBList(ids: _*))), wc)
- }
- /** @param t object to save
- * @param wc write concern
- * @return (WriteResult) result of write operation
- */
- def save(t: ObjectType, wc: WriteConcern) = {
- val dbo = decorateDBO(t)
- val wr = collection.save(dbo, wc)
- val lastError = wr.getCachedLastError
- if (lastError != null && !lastError.ok()) {
- throw SalatSaveError(description, collection, wc, wr, List(dbo))
- }
- wr
- }
- /** @param q search query for old object to update
- * @param o object with which to update <tt>q</tt>
- * @param upsert if the database should create the element if it does not exist
- * @param multi if the update should be applied to all objects matching
- * @param wc write concern
- * @return (WriteResult) result of write operation
- */
- def update(q: DBObject, o: DBObject, upsert: Boolean = false, multi: Boolean = false, wc: WriteConcern = defaultWriteConcern): WriteResult = {
- val wr = collection.update(decorateQuery(q), o, upsert, multi, wc)
- val lastError = wr.getCachedLastError
- if (lastError != null && !lastError.ok()) {
- throw SalatDAOUpdateError(description, collection, q, o, wc, wr, upsert, multi)
- }
- wr
- }
- /** @param ref object for which to search
- * @param keys fields to return
- * @tparam A type view bound to DBObject
- * @tparam B type view bound to DBObject
- * @return a typed cursor to iterate over results
- */
- def find[A <% DBObject, B <% DBObject](ref: A, keys: B) = SalatMongoCursor[ObjectType](_grater,
- collection.find(decorateQuery(ref), keys).asInstanceOf[MongoCursorBase].underlying)
- /** @param query object for which to search
- * @param field field to project on
- * @param m implicit manifest typed to `P`
- * @param ctx implicit [[com.novus.salat.Context]]
- * @tparam P type of projected field
- * @return (Option[P]) Some() of the object found, or <code>None</code> if no such object exists
- */
- def projection[P <: CaseClass](query: DBObject, field: String)(implicit m: Manifest[P], ctx: Context): Option[P] = {
- collection.findOne(decorateQuery(query), MongoDBObject(field -> 1)).flatMap {
- dbo =>
- dbo.expand[DBObject](field).map(grater[P].asObject(_))
- }
- }
- /** @param query object for which to search
- * @param field field to project on
- * @param m implicit manifest typed to `P`
- * @param ctx implicit [[com.novus.salat.Context]]
- * @tparam P type of projected field
- * @return (Option[P]) Some() of the object found, or <code>None</code> if no such object exists
- */
- def primitiveProjection[P <: Any](query: DBObject, field: String)(implicit m: Manifest[P], ctx: Context): Option[P] = {
- collection.findOne(decorateQuery(query), MongoDBObject(field -> 1)).flatMap {
- dbo =>
- dbo.expand[P](field)
- }
- }
- /** @param query object for which to search
- * @param field field to project on
- * @param m implicit manifest typed to `P`
- * @param ctx implicit [[com.novus.salat.Context]]
- * @tparam P type of projected field
- * @return (List[P]) of the objects found
- */
- def projections[P <: CaseClass](query: DBObject, field: String)(implicit m: Manifest[P], ctx: Context): List[P] =
- collection.find(decorateQuery(query), MongoDBObject(field -> 1)).toList.flatMap {
- r =>
- r.expand[DBObject](field).map(grater[P].asObject(_))
- }
- /** @param query object for which to search
- * @param field field to project on
- * @param m implicit manifest typed to `P`
- * @param ctx implicit [[com.novus.salat.Context]]
- * @tparam P type of projected field
- * @return (List[P]) of the objects found
- */
- def primitiveProjections[P <: Any](query: DBObject, field: String)(implicit m: Manifest[P], ctx: Context): List[P] = {
- collection.find(query, MongoDBObject(field -> 1)).toList.flatMap(_.expand[P](field))
- }
- /** @param q object for which to search
- * @param fieldsThatMustExist list of field keys that must exist
- * @param fieldsThatMustNotExist list of field keys that must not exist
- * @return count of documents matching the search criteria
- */
- def count(q: DBObject = MongoDBObject.empty, fieldsThatMustExist: List[String] = Nil, fieldsThatMustNotExist: List[String] = Nil): Long = {
- val query = {
- val builder = MongoDBObject.newBuilder
- builder ++= q
- for (field <- fieldsThatMustExist) {
- builder += field -> MongoDBObject("$exists" -> true)
- }
- for (field <- fieldsThatMustNotExist) {
- builder += field -> MongoDBObject("$exists" -> false)
- }
- builder.result()
- }
- collection.count(decorateQuery(query))
- }
- }
- /** When you use a single collection to contain an entire type hierarchy, then use this trait to make sure that type hints
- * are appended to find, count and update queries. (Please note you need to make sure your indexes on this shared collection
- * take your type hint fields into account!)
- *
- * In addition, when you use the concrete subclass DAO to insert, update and save objects, a type hint will be appended to
- * the serialized object.
- *
- */
- trait ConcreteSubclassDAO {
- self: SalatDAO[_, _] =>
- override val forceTypeHints = true
- override val appendTypeHintToQueries = true
- require(_grater.ctx.typeHintStrategy.when != TypeHintFrequency.Never, "Concrete subclass DAO must support type hinting!")
- }