PageRenderTime 52ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/salat-core/src/main/scala/com/novus/salat/dao/SalatDAO.scala

https://github.com/kodemaniak/salat
Scala | 523 lines | 206 code | 47 blank | 270 comment | 33 complexity | da0c112500cb44a9b663f96a611708f7 MD5 | raw file
Possible License(s): Apache-2.0
  1. /*
  2. * Copyright (c) 2010 - 2012 Novus Partners, Inc. (http://www.novus.com)
  3. *
  4. * Module: salat-core
  5. * Class: SalatDAO.scala
  6. * Last modified: 2012-12-05 12:24:48 EST
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. *
  20. * Project: http://github.com/novus/salat
  21. * Wiki: http://github.com/novus/salat/wiki
  22. * Mailing list: http://groups.google.com/group/scala-salat
  23. * StackOverflow: http://stackoverflow.com/questions/tagged/salat
  24. */
  25. package com.novus.salat.dao
  26. import com.mongodb.casbah.Imports._
  27. import com.mongodb.casbah.MongoCursorBase
  28. import com.novus.salat._
  29. import com.mongodb.casbah.commons.{ MongoDBObject, Logging }
  30. import com.mongodb.{ WriteConcern, DBObject }
  31. /** Sample DAO implementation.
  32. * @param collection MongoDB collection
  33. * @param mot implicit manifest for ObjectType
  34. * @param mid implicit manifest for ID
  35. * @param ctx implicit [[com.novus.salat.Context]]
  36. * @tparam ObjectType class to be persisted
  37. * @tparam ID _id type
  38. */
  39. abstract class SalatDAO[ObjectType <: AnyRef, ID <: Any](val collection: MongoCollection)(implicit mot: Manifest[ObjectType],
  40. mid: Manifest[ID], ctx: Context)
  41. extends com.novus.salat.dao.DAO[ObjectType, ID] with Logging {
  42. dao =>
  43. /** Supplies the [[com.novus.salat.Grater]] from the implicit [[com.novus.salat.Context]] and `ObjectType` manifest */
  44. val _grater = grater[ObjectType](ctx, mot)
  45. /** Force type hints when objects are persisted. Used to support a DAO typed to an abstract superclass or trait.
  46. * Should be overriden and forced to true when you want to select
  47. */
  48. val forceTypeHints = {
  49. val isProxy = _grater.isInstanceOf[ProxyGrater[_]]
  50. // safety check - if you never type hint, then deserializing using a proxy grater is impossible
  51. require(!isProxy || ctx.typeHintStrategy.when != TypeHintFrequency.Never,
  52. "Abstract class hierarchies cannot be deserialized when the context '%s' type hint strategy is NeverTypeHint".format(ctx.name))
  53. isProxy
  54. }
  55. /** If you are mixing and matching abstract and concrete DAOs, turn this on in the concrete DAOs to ensure that querying on a
  56. * mixed collection will only yield results in the child collection.
  57. */
  58. val appendTypeHintToQueries = false
  59. /** A central place to modify find, count and update queries before executing them.
  60. * @param query query to decorate
  61. * @return decorated query for execution
  62. */
  63. def decorateQuery(query: DBObject) = {
  64. if (appendTypeHintToQueries) {
  65. query(ctx.typeHintStrategy.typeHint) = ctx.typeHintStrategy.encode(_grater.clazz.getName).asInstanceOf[AnyRef]
  66. }
  67. query
  68. }
  69. /** A central place to modify DBOs before inserting, saving, or updating.
  70. * @param toPersist object to be serialized
  71. * @return decorated DBO for persisting
  72. */
  73. def decorateDBO(toPersist: ObjectType) = {
  74. val dbo = _grater.asDBObject(toPersist)
  75. if (forceTypeHints) {
  76. // take advantage of the mutability of DBObject by cramming in a type hint
  77. dbo(ctx.typeHintStrategy.typeHint) = ctx.typeHintStrategy.encode(toPersist.getClass.getName).asInstanceOf[AnyRef]
  78. }
  79. dbo
  80. }
  81. /** Inner abstract class to facilitate working with child collections using a typed parent id -
  82. * no cascading support will be offered, but you can override saves and deletes in the parent DAO
  83. * to manually cascade children as you like.
  84. *
  85. * Given parent class `Foo` and child class `Bar`:
  86. * {{{
  87. * case class Foo(_id: ObjectId, // etc )
  88. * case class Bar(_id: ObjectId,
  89. * parentId: ObjectId, // this refers back to a parent in Foo collection
  90. * // etc )
  91. *
  92. * object FooDAO extends SalatDAO[Foo, ObjectId](collection = MongoConnection()("db")("fooCollection")) {
  93. *
  94. * // and here is a child DAO you can use within FooDAO to work with children of type Bar whose parentId field matches
  95. * // the supplied parent id of an instance of Foo
  96. * val bar = new ChildCollection[Bar, ObjectId](collection = MongoConnection()("db")("barCollection"),
  97. * parentIdField = "parentId") { }
  98. *
  99. * }
  100. * }}}
  101. *
  102. * @param collection MongoDB collection
  103. * @param parentIdField parent id field key
  104. * @param mct implicit manifest for `ChildType`
  105. * @param mcid implicit manifest for `ChildID`
  106. * @param ctx implicit [[com.novus.salat.Context]]
  107. * @tparam ChildType type of child object
  108. * @tparam ChildID type of child _id field
  109. */
  110. abstract class ChildCollection[ChildType <: AnyRef, ChildID <: Any](override val collection: MongoCollection,
  111. val parentIdField: String)(implicit mct: Manifest[ChildType],
  112. mcid: Manifest[ChildID], ctx: Context)
  113. extends SalatDAO[ChildType, ChildID](collection) {
  114. childDao =>
  115. override lazy val description = "SalatDAO[%s,%s](%s) -> ChildCollection[%s,%s](%s)".format(
  116. mot.erasure.getSimpleName, mid.erasure.getSimpleName, dao.collection.name,
  117. mct.erasure.getSimpleName, mcid.erasure.getSimpleName, childDao.collection.name)
  118. /** @param parentId parent id
  119. * @return base query object for a single parent id
  120. */
  121. def parentIdQuery(parentId: ID): DBObject = {
  122. decorateQuery(MongoDBObject(parentIdField -> parentId))
  123. }
  124. /** @param parentIds list of parent ids
  125. * @return base query object for a list of parent ids
  126. * TODO - replace list with traversable
  127. */
  128. def parentIdsQuery(parentIds: List[ID]): DBObject = {
  129. MongoDBObject(parentIdField -> MongoDBObject("$in" -> parentIds))
  130. }
  131. /** Count the number of documents matching the parent id.
  132. * @param parentId parent id
  133. * @param query object for which to search
  134. * @param fieldsThatMustExist list of field keys that must exist
  135. * @param fieldsThatMustNotExist list of field keys that must not exist
  136. * @return count of documents matching the search criteria
  137. */
  138. def countByParentId(parentId: ID, query: DBObject = MongoDBObject.empty, fieldsThatMustExist: List[String] = Nil, fieldsThatMustNotExist: List[String] = Nil): Long = {
  139. childDao.count(parentIdQuery(parentId) ++ query, fieldsThatMustExist, fieldsThatMustNotExist)
  140. }
  141. /** @param parentId parent id
  142. * @param query object for which to search
  143. * @return list of child ids matching parent id and search criteria
  144. */
  145. def idsForParentId(parentId: ID, query: DBObject = MongoDBObject.empty): List[ChildID] = {
  146. childDao.collection.find(parentIdQuery(parentId) ++ query, MongoDBObject("_id" -> 1)).map(_.expand[ChildID]("_id").get).toList
  147. }
  148. /** @param parentIds list of parent ids
  149. * @param query object for which to search
  150. * @return list of child ids matching parent ids and search criteria
  151. */
  152. def idsForParentIds(parentIds: List[ID], query: DBObject = MongoDBObject.empty): List[ChildID] = {
  153. childDao.collection.find(parentIdsQuery(parentIds) ++ query, MongoDBObject("_id" -> 1)).map(_.expand[ChildID]("_id").get).toList
  154. }
  155. /** @param parentId parent id
  156. * @param query object for which to search
  157. * @return list of child objects matching parent id and search criteria
  158. */
  159. def findByParentId(parentId: ID, query: DBObject = MongoDBObject.empty): SalatMongoCursor[ChildType] = {
  160. childDao.find(parentIdQuery(parentId) ++ query)
  161. }
  162. /** @param parentIds list of parent ids
  163. * @param query object for which to search
  164. * @return list of child objects matching parent ids and search criteria
  165. */
  166. def findByParentIds(parentIds: List[ID], query: DBObject = MongoDBObject.empty): SalatMongoCursor[ChildType] = {
  167. childDao.find(parentIdsQuery(parentIds) ++ query)
  168. }
  169. /** @param parentId parent id
  170. * @param query object for which to search
  171. * @return list of child objects matching parent id and search criteria
  172. */
  173. def findByParentId(parentId: ID, query: DBObject, keys: DBObject): SalatMongoCursor[ChildType] = {
  174. childDao.find(parentIdQuery(parentId) ++ query, keys)
  175. }
  176. /** @param parentIds parent ids
  177. * @param query object for which to search
  178. * @return list of child objects matching parent ids and search criteria
  179. */
  180. def findByParentIds(parentIds: List[ID], query: DBObject, keys: DBObject): SalatMongoCursor[ChildType] = {
  181. childDao.find(parentIdsQuery(parentIds) ++ query, keys)
  182. }
  183. /** @param parentId parent id
  184. * @param o object with which to update the document(s) matching `parentId`
  185. * @param upsert if the database should create the element if it does not exist
  186. * @param multi if the update should be applied to all objects matching
  187. * @param wc write concern
  188. * @tparam A type view bound to DBObject
  189. */
  190. def updateByParentId[A <% DBObject](parentId: ID, o: A, upsert: Boolean, multi: Boolean, wc: WriteConcern = collection.writeConcern) {
  191. childDao.update(parentIdQuery(parentId), o, upsert, multi, wc)
  192. }
  193. /** @param parentIds parent ids
  194. * @param o object with which to update the document(s) matching `parentIds`
  195. * @param upsert if the database should create the element if it does not exist
  196. * @param multi if the update should be applied to all objects matching
  197. * @param wc write concern
  198. * @tparam A type view bound to DBObject
  199. */
  200. def updateByParentIds[A <% DBObject](parentIds: List[ID], o: A, upsert: Boolean, multi: Boolean, wc: WriteConcern = collection.writeConcern) {
  201. childDao.update(parentIdsQuery(parentIds), o, upsert, multi, wc)
  202. }
  203. /** Remove documents matching parent id
  204. * @param parentId parent id
  205. * @param wc write concern
  206. */
  207. def removeByParentId(parentId: ID, wc: WriteConcern = collection.writeConcern) {
  208. childDao.remove(parentIdQuery(parentId), wc)
  209. }
  210. /** Remove documents matching parent ids
  211. * @param parentIds parent ids
  212. * @param wc write concern
  213. */
  214. def removeByParentIds(parentIds: List[ID], wc: WriteConcern = collection.writeConcern) {
  215. childDao.remove(parentIdsQuery(parentIds), wc)
  216. }
  217. /** Projection typed to a case class, trait or abstract superclass.
  218. * @param parentId parent id
  219. * @param field field to project on
  220. * @param query (optional) object for which to search
  221. * @param mr implicit manifest typed to `R`
  222. * @param ctx implicit [[com.novus.salat.Context]]
  223. * @tparam R type of projected field
  224. * @return (List[R]) of the objects found
  225. */
  226. def projectionsByParentId[R <: CaseClass](parentId: ID, field: String, query: DBObject = MongoDBObject.empty)(implicit mr: Manifest[R], ctx: Context): List[R] = {
  227. childDao.projections(parentIdQuery(parentId) ++ query, field)(mr, ctx)
  228. }
  229. /** Projection typed to a case class, trait or abstract superclass.
  230. * @param parentIds parent ids
  231. * @param field field to project on
  232. * @param query (optional) object for which to search
  233. * @param mr implicit manifest typed to `R`
  234. * @param ctx implicit [[com.novus.salat.Context]]
  235. * @tparam R type of projected field
  236. * @return (List[R]) of the objects found
  237. */
  238. def projectionsByParentIds[R <: CaseClass](parentIds: List[ID], field: String, query: DBObject = MongoDBObject.empty)(implicit mr: Manifest[R], ctx: Context): List[R] = {
  239. childDao.projections(parentIdsQuery(parentIds) ++ query, field)(mr, ctx)
  240. }
  241. /** Projection typed to a type for which Casbah or mongo-java-driver handles conversion
  242. * @param parentId parent id
  243. * @param field field to project on
  244. * @param query (optional) object for which to search
  245. * @param mr implicit manifest typed to `R`
  246. * @param ctx implicit [[com.novus.salat.Context]]
  247. * @tparam R type of projected field
  248. * @return (List[R]) of the objects found
  249. */
  250. def primitiveProjectionsByParentId[R <: Any](parentId: ID, field: String, query: DBObject = MongoDBObject.empty)(implicit mr: Manifest[R], ctx: Context): List[R] = {
  251. childDao.primitiveProjections(parentIdQuery(parentId) ++ query, field)(mr, ctx)
  252. }
  253. /** Projection typed to a type for which Casbah or mongo-java-driver handles conversion
  254. * @param parentIds parent ids
  255. * @param field field to project on
  256. * @param query (optional) object for which to search
  257. * @param mr implicit manifest typed to `R`
  258. * @param ctx implicit [[com.novus.salat.Context]]
  259. * @tparam R type of projected field
  260. * @return (List[R]) of the objects found
  261. */
  262. def primitiveProjectionsByParentIds[R <: Any](parentIds: List[ID], field: String, query: DBObject = MongoDBObject.empty)(implicit mr: Manifest[R], ctx: Context): List[R] = {
  263. childDao.primitiveProjections(parentIdsQuery(parentIds) ++ query, field)(mr, ctx)
  264. }
  265. }
  266. /** Default description is the case class simple name and the collection.
  267. */
  268. override lazy val description = "SalatDAO[%s,%s](%s)".format(mot.erasure.getSimpleName, mid.erasure.getSimpleName, collection.name)
  269. /** @param t instance of ObjectType
  270. * @param wc write concern
  271. * @return if insert succeeds, ID of inserted object
  272. */
  273. def insert(t: ObjectType, wc: WriteConcern) = {
  274. val dbo = decorateDBO(t)
  275. val wr = collection.insert(dbo, wc)
  276. val error = wr.getCachedLastError
  277. if (error == null || (error != null && error.ok())) {
  278. dbo.getAs[ID]("_id")
  279. }
  280. else {
  281. throw SalatInsertError(description, collection, wc, wr, List(dbo))
  282. }
  283. }
  284. /** @param docs collection of `ObjectType` instances to insert
  285. * @param wc write concern
  286. * @return list of object ids
  287. * TODO: flatten list of IDs - why on earth didn't I do that in the first place?
  288. */
  289. def insert(docs: Traversable[ObjectType], wc: WriteConcern = defaultWriteConcern) = if (docs.nonEmpty) {
  290. val dbos = docs.map(decorateDBO(_)).toList
  291. val wr = collection.insert(dbos: _*)
  292. val lastError = wr.getCachedLastError
  293. if (lastError == null || (lastError != null && lastError.ok())) {
  294. dbos.map {
  295. dbo =>
  296. dbo.getAs[ID]("_id") orElse collection.findOne(dbo).flatMap(_.getAs[ID]("_id"))
  297. }
  298. }
  299. else {
  300. throw SalatInsertError(description, collection, wc, wr, dbos)
  301. }
  302. }
  303. else Nil
  304. /** @param query query
  305. * @tparam A type view bound to DBObject
  306. * @return list of IDs
  307. */
  308. def ids[A <% DBObject](query: A): List[ID] = {
  309. collection.find(decorateQuery(query), MongoDBObject("_id" -> 1)).map(_.expand[ID]("_id").get).toList
  310. }
  311. /** @param t object for which to search
  312. * @tparam A type view bound to DBObject
  313. * @return (Option[ObjectType]) Some() of the object found, or <code>None</code> if no such object exists
  314. */
  315. def findOne[A <% DBObject](t: A) = collection.findOne(decorateQuery(t)).map(_grater.asObject(_))
  316. /** @param id identifier
  317. * @return (Option[ObjectType]) Some() of the object found, or <code>None</code> if no such object exists
  318. */
  319. def findOneById(id: ID) = collection.findOneByID(id.asInstanceOf[AnyRef]).map(_grater.asObject(_))
  320. /** @param t object to remove from the collection
  321. * @param wc write concern
  322. * @return (WriteResult) result of write operation
  323. */
  324. def remove(t: ObjectType, wc: WriteConcern) = {
  325. val dbo = decorateDBO(t)
  326. val wr = collection.remove(dbo, wc)
  327. val lastError = wr.getCachedLastError
  328. if (lastError != null && !lastError.ok()) {
  329. throw SalatRemoveError(description, collection, wc, wr, List(dbo))
  330. }
  331. wr
  332. }
  333. /** @param q the object that documents to be removed must match
  334. * @param wc write concern
  335. * @return (WriteResult) result of write operation
  336. */
  337. def remove[A <% DBObject](q: A, wc: WriteConcern) = {
  338. val wr = collection.remove(q, wc)
  339. val lastError = wr.getCachedLastError
  340. if (lastError != null && !lastError.ok()) {
  341. throw SalatRemoveQueryError(description, collection, q, wc, wr)
  342. }
  343. wr
  344. }
  345. /** @param id the ID of the document to be removed
  346. * @param wc write concern
  347. * @return (WriteResult) result of write operation
  348. */
  349. def removeById(id: ID, wc: WriteConcern = defaultWriteConcern) = {
  350. remove(MongoDBObject("_id" -> id), wc)
  351. }
  352. /** @param ids the list of IDs identifying the list of documents to be removed
  353. * @param wc wrote concern
  354. * @return (WriteResult) result of write operation
  355. */
  356. def removeByIds(ids: List[ID], wc: WriteConcern) = {
  357. remove(MongoDBObject("_id" -> MongoDBObject("$in" -> MongoDBList(ids: _*))), wc)
  358. }
  359. /** @param t object to save
  360. * @param wc write concern
  361. * @return (WriteResult) result of write operation
  362. */
  363. def save(t: ObjectType, wc: WriteConcern) = {
  364. val dbo = decorateDBO(t)
  365. val wr = collection.save(dbo, wc)
  366. val lastError = wr.getCachedLastError
  367. if (lastError != null && !lastError.ok()) {
  368. throw SalatSaveError(description, collection, wc, wr, List(dbo))
  369. }
  370. wr
  371. }
  372. /** @param q search query for old object to update
  373. * @param o object with which to update <tt>q</tt>
  374. * @param upsert if the database should create the element if it does not exist
  375. * @param multi if the update should be applied to all objects matching
  376. * @param wc write concern
  377. * @return (WriteResult) result of write operation
  378. */
  379. def update(q: DBObject, o: DBObject, upsert: Boolean = false, multi: Boolean = false, wc: WriteConcern = defaultWriteConcern): WriteResult = {
  380. val wr = collection.update(decorateQuery(q), o, upsert, multi, wc)
  381. val lastError = wr.getCachedLastError
  382. if (lastError != null && !lastError.ok()) {
  383. throw SalatDAOUpdateError(description, collection, q, o, wc, wr, upsert, multi)
  384. }
  385. wr
  386. }
  387. /** @param ref object for which to search
  388. * @param keys fields to return
  389. * @tparam A type view bound to DBObject
  390. * @tparam B type view bound to DBObject
  391. * @return a typed cursor to iterate over results
  392. */
  393. def find[A <% DBObject, B <% DBObject](ref: A, keys: B) = SalatMongoCursor[ObjectType](_grater,
  394. collection.find(decorateQuery(ref), keys).asInstanceOf[MongoCursorBase].underlying)
  395. /** @param query object for which to search
  396. * @param field field to project on
  397. * @param m implicit manifest typed to `P`
  398. * @param ctx implicit [[com.novus.salat.Context]]
  399. * @tparam P type of projected field
  400. * @return (Option[P]) Some() of the object found, or <code>None</code> if no such object exists
  401. */
  402. def projection[P <: CaseClass](query: DBObject, field: String)(implicit m: Manifest[P], ctx: Context): Option[P] = {
  403. collection.findOne(decorateQuery(query), MongoDBObject(field -> 1)).flatMap {
  404. dbo =>
  405. dbo.expand[DBObject](field).map(grater[P].asObject(_))
  406. }
  407. }
  408. /** @param query object for which to search
  409. * @param field field to project on
  410. * @param m implicit manifest typed to `P`
  411. * @param ctx implicit [[com.novus.salat.Context]]
  412. * @tparam P type of projected field
  413. * @return (Option[P]) Some() of the object found, or <code>None</code> if no such object exists
  414. */
  415. def primitiveProjection[P <: Any](query: DBObject, field: String)(implicit m: Manifest[P], ctx: Context): Option[P] = {
  416. collection.findOne(decorateQuery(query), MongoDBObject(field -> 1)).flatMap {
  417. dbo =>
  418. dbo.expand[P](field)
  419. }
  420. }
  421. /** @param query object for which to search
  422. * @param field field to project on
  423. * @param m implicit manifest typed to `P`
  424. * @param ctx implicit [[com.novus.salat.Context]]
  425. * @tparam P type of projected field
  426. * @return (List[P]) of the objects found
  427. */
  428. def projections[P <: CaseClass](query: DBObject, field: String)(implicit m: Manifest[P], ctx: Context): List[P] =
  429. collection.find(decorateQuery(query), MongoDBObject(field -> 1)).toList.flatMap {
  430. r =>
  431. r.expand[DBObject](field).map(grater[P].asObject(_))
  432. }
  433. /** @param query object for which to search
  434. * @param field field to project on
  435. * @param m implicit manifest typed to `P`
  436. * @param ctx implicit [[com.novus.salat.Context]]
  437. * @tparam P type of projected field
  438. * @return (List[P]) of the objects found
  439. */
  440. def primitiveProjections[P <: Any](query: DBObject, field: String)(implicit m: Manifest[P], ctx: Context): List[P] = {
  441. collection.find(query, MongoDBObject(field -> 1)).toList.flatMap(_.expand[P](field))
  442. }
  443. /** @param q object for which to search
  444. * @param fieldsThatMustExist list of field keys that must exist
  445. * @param fieldsThatMustNotExist list of field keys that must not exist
  446. * @return count of documents matching the search criteria
  447. */
  448. def count(q: DBObject = MongoDBObject.empty, fieldsThatMustExist: List[String] = Nil, fieldsThatMustNotExist: List[String] = Nil): Long = {
  449. val query = {
  450. val builder = MongoDBObject.newBuilder
  451. builder ++= q
  452. for (field <- fieldsThatMustExist) {
  453. builder += field -> MongoDBObject("$exists" -> true)
  454. }
  455. for (field <- fieldsThatMustNotExist) {
  456. builder += field -> MongoDBObject("$exists" -> false)
  457. }
  458. builder.result()
  459. }
  460. collection.count(decorateQuery(query))
  461. }
  462. }
  463. /** When you use a single collection to contain an entire type hierarchy, then use this trait to make sure that type hints
  464. * are appended to find, count and update queries. (Please note you need to make sure your indexes on this shared collection
  465. * take your type hint fields into account!)
  466. *
  467. * In addition, when you use the concrete subclass DAO to insert, update and save objects, a type hint will be appended to
  468. * the serialized object.
  469. *
  470. */
  471. trait ConcreteSubclassDAO {
  472. self: SalatDAO[_, _] =>
  473. override val forceTypeHints = true
  474. override val appendTypeHintToQueries = true
  475. require(_grater.ctx.typeHintStrategy.when != TypeHintFrequency.Never, "Concrete subclass DAO must support type hinting!")
  476. }