這篇文章將說說,我使用 MongoDB & Cashbah 的一些心得,這程式的主架構是 我從 simplistic 學來的。

The Abstract DAO

底下是一個很簡單的 CRUD 的 MongoDB DAO. 在這邊,我們建立一個 DAO 時,我們會把這 個 DAO 用的儲存空間 MongoCollection 傳進來,另外要注意的一點是,這個 MongoCollection 的 WriteConcert 必需是 Strict 的。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
 *  Base class for all DAO classes.
 */
abstract class AbstractDAO[T, ID <: Serializable](protected[locadz] val mongoCol: MongoCollection) {

  /** builder to turn mongodb object into model object */
  implicit protected val builder: DBObject => Either[DataAccessException, T]

  /** builder to turn model object into mongodb object */
  implicit protected val extractor: T => DBObject

  /** this dao expects the underlying mongoCol would throw exception when error occurs. */
  require(mongoCol.getWriteConcern != null)
  require(mongoCol.getWriteConcern.getW >= 1, "Write must be >= 1, was %d".format(mongoCol.getWriteConcern.getW))

  /** insert object into the underly*/
  def insert(obj: T): Either[DataAccessException, ID] = {
    try {
      val dbObj: DBObject = extractor(obj)
      val res: WriteResult = mongoCol.insert(dbObj)
      Right(dbObj.getAs[ID]("_id").get)
    } catch {
      case e: MongoException      => Left(new DataAccessException(e))
      case e: DataAccessException => Left(e)
    }
  }

  def save(obj: T): Option[DataAccessException] = {
    try {
      val res = mongoCol.save[T](obj)
      None
    } catch {
      case e: MongoException      => Some(new DataAccessException(e))
      case e: DataAccessException => Some(e)
    }
  }

  def findById(id: ID): Either[DataAccessException, Option[T]] = {
    try {
      mongoCol.findOneByID(id) match {
        case None        => Right(None)
        case Some(dbObj) => builder.apply(dbObj).right.map(Option(_))
      }
    } catch {
      case e: DataAccessException => Left(e)
    }
  }

  def delete(id: ID): Option[DataAccessException] = {
    try {
      mongoCol.remove(MongoDBObject("_id" -> id))
      None
    } catch {
      case e: MongoException => Some(new DataAccessException(e))
    }
  }

  def find[T](dbObj: DBObject)(implicit builder: (DBObject) => Either[DataAccessException, T]): Either[DataAccessException, Iterable[T]] = {

    try {
      Right(mongoCol.find(dbObj)
        .flatMap(found => {
          builder.apply(found).fold(
            e => throw e,
            r => Some(r))
        }).toStream)

    } catch {
      case e: DataAccessException => Left(e)
    }
  }
}

這邊有兩個需要被實作的東西

  • val builder: DBObject => Either[DataAccessException, T]
  • val extractor: T => DBObject

而怎麼建立這兩個東西,就是全文的重點了。

How to use AbstractDAO

先來看一個範例;實作上,在 UserDAO object 上,我們先把欄位的名稱及資料型態,一個個的定好

  • 'val Name = attribute("name", Conversion.String)`
  • 'val Email = attribute("email", Conversion.String)'
  • 'val CreateDate = attribute("ctime", Conversion.JodaDate)'

而這個 attribute method 做的是,把資料名稱存起來,然後當這個 Attribute instance

  • 收到一個 DBObject 時,把對應的欄位取出來,並將值給轉成定義的格式
  • 收到一個 property value 時,把他轉成 (name -> value) pair

第一個用法的這個方式,用在從 builder 上面,一個個的把值從 DBObject 抓出來再來建立物件, 第二個用法用在,把物件轉成 DBObject 時,及用在建立 query 時。

UserDAO 實作

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class UserDAO(mongoCol: MongoCollection) extends AbstractDAO[User, String](mongoCol) {

  import UserDAO._

  implicit protected val builder: (DBObject) => Either[DataAccessException, User] = UserDAO.dbObjectToUser _

  implicit protected val extractor: (User) => DBObject = UserDAO.userToDBObject _

  def this(dbCol: DBCollection) = this(dbCol.asScala)

}

object UserDAO {

  import Attributes._

  val Name = attribute("name", Conversion.String)

  val Email = attribute("email", Conversion.String)

  val CreateDate = attribute("ctime", Conversion.JodaDate)

  def dbObjectToUser(dbObj: DBObject): Either[DataAccessException, User] = {

    try {
      val id = dbObj.getAs[String]("_id").get

      Right(
        new User(id,
          Name(dbObj),
          Email(dbObj),
          CreateDate(dbObj)
        )
      )
    } catch {
      case e: DataAccessException => Left(e)
    }
  }

  implicit def userToDBObject(user: User): DBObject = {

    val ret = MongoDBObject("_id" -> user.id)
    ret += Name(user.externalId)
    ret += Email(user.verifiedId)
    ret += CreateDate(user.createDate)
    return ret
  }
}

Attribute & Conversion 部份實作

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
34
35
36
37
38
39
40
41
42
43
44
45
46
trait Attribute[A, B <: Any] {
  val name: String
  val conversion: Conversion[A, B]

  protected implicit val manifestA: Manifest[A]
  protected implicit val manifestB: Manifest[B]
}

trait RequiredAttribute[A, B <: Any] extends Attribute[A, B] {

  def apply(value: A): Tuple2[String, B] = {
    name -> conversion.apply(value)
  }

  def apply(value: DBObject): A = {
    value.getAs[B](name).map(b => conversion.unapply(b)).getOrElse(throw new MissingRequiredAttributeException(name))
  }
}

def attribute[A, B <: Any](name: String, dataType: Conversion[A, B])(implicit manifestA: Manifest[A], manifestB: Manifest[B]) = {
  new SingleValueRequiredAttribute[A, B](name, dataType)
}


trait Conversion[A, B] {
  def apply(source: A): B
  def unapply(value: B): A
}

object Conversion {
  case object String extends Conversion[String, String] {
    override def apply(source: String) = source
    override def unapply(value: String) = value
  }

  /**
   * Conversion for DateTime to DateTime.
   */
  object JodaDate extends Conversion[DateTime, DateTime] {

    RegisterJodaTimeConversionHelpers()

    override def apply(source: DateTime): DateTime = source
    override def unapply(value: DateTime): DateTime = value
  }
}

How to Use Attributes to Build Query

前面的 Attribute(s) 不只是用來產做 builder 及 extractor 用的,他們最好用的地方在於 建立 query 時,如下

1
2
3
def findByDateRange(start: DateTime, end: DateTime): Either[DataAccessException, User] = {
  return find(CreateDate.name $gth CreateDate(start)._2  $lt CreateDate(range)._2)
}

這邊,我們可以看到用 Conversion 來描述資料型態的好處是,當你儲存的資料型態有變時,你不用去更 改你的 MongoDB query ,欄位的名稱、資料型別的轉換,都被封裝起來,使用者在寫 Query 時,不用去 記欄位的名稱,也不用去記這個欄位的型態是什麼, MongoDB 支不支援這個型態。

第二個好處是, Casbah 本來只支援 java primitive type & java.util.Date ,透過 Conversion , 我們可以支援任意的資料型態於 query 中,只要透過新的 Conversion 類別,我們就可以增加 MongoDB 可以 支援的資料型態。

例如,如果我們想增加對 java.net.URL 的支援,只要訂義以下的 Conversion 即可

1
2
3
4
5
6
case object Url extends Conversion[URL, String] {

  override def apply(source: URL) = source.toExternalForm

  override def unapply(value: String) = new URL(value)
}

Appendix:

Attribute.scala

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.locadz.model.mongodb

import com.mongodb.DBObject
import com.mongodb.casbah._
import com.locadz.model.exception.MissingRequiredAttributeException

/**
 * Date: 2/20/12
 */
object Attributes {

  trait Attribute[A, B <: Any] {
    val name: String
    val conversion: Conversion[A, B]

    protected implicit val manifestA: Manifest[A]

    protected implicit val manifestB: Manifest[B]

  }

  trait OptionalAttribute[A, B <: Any] extends Attribute[A, B] {

    def apply(value: Option[A]): Tuple2[String, Any] = {
      val v = value.map(conversion.apply(_)).getOrElse(null)
      name -> v
    }

    def apply(value: DBObject): Option[A] = value.getAs[B](name).map(b => conversion.unapply(b))

  }

  trait RequiredAttribute[A, B <: Any] extends Attribute[A, B] {

    def apply(value: A): Tuple2[String, B] = {
      name -> conversion.apply(value)
    }

    def apply(value: DBObject): A = {
      value.getAs[B](name).map(b => conversion.unapply(b)).getOrElse(throw new MissingRequiredAttributeException(name))
    }
  }

  case class SingleValueRequiredAttribute[A, B <: Any](name: String, conversion: Conversion[A, B])(implicit val manifestA: Manifest[A], implicit val manifestB: Manifest[B]) extends RequiredAttribute[A, B]

  case class SingleValueOptionalAttribute[A, B <: Any](name: String, conversion: Conversion[A, B])(implicit val manifestA: Manifest[A], implicit val manifestB: Manifest[B])
    extends OptionalAttribute[A, B]

  def attribute[A, B <: Any](name: String, dataType: Conversion[A, B])(implicit manifestA: Manifest[A], manifestB: Manifest[B]) = {
    new SingleValueRequiredAttribute[A, B](name, dataType)
  }

  def optionalAttribute[A, B <: Any](name: String, dataType: Conversion[A, B])(implicit manifestA: Manifest[A], manifestB: Manifest[B]) = {
    new SingleValueOptionalAttribute[A, B](name, dataType)
  }

}