Как мне обрабатывать записи json для обновления существующих объектов в ReactiveMongo?

В моем текущем проекте Play2 я реализовал ReactiveMongo для хранения своих пользовательских объектов.

Пользователь:

case class User(override var _id: Option[BSONObjectID] = None,
                override var created: Option[DateTime] = None,
                override var updated: Option[DateTime] = None,
                firstName: String,
                lastName: String,
                email: String,
                emailValidated: Boolean,
                phoneNumber: String,
                lastLogin: DateTime,
                linkedProviders: Seq[LinkedProvider],
                userRoles: Seq[UserRole.UserRole]) extends TemporalModel {
}

import EnumUtils.enumReads
implicit val userRoleReads = enumReads(UserRole)
import mongo_models.LinkedProvider.linkedProviderReads
implicit val userReads: Reads[User] = (
    (__ \ "_id").read[Option[BSONObjectID]] and
      (__ \ "created").read[Option[DateTime]] and
      (__ \ "updated").read[Option[DateTime]] and
      (__ \ "firstName").read[String] and
      (__ \ "lastName").read[String] and
      (__ \ "email").read[String] and
      (__ \ "emailValidated").read[Boolean] and
      (__ \ "phoneNumber").read[String] and
      (__ \ "lastLogin").read[DateTime] and
      (__ \ "linkedProviders").read(list[LinkedProvider](linkedProviderReads)) and
      (__ \ "userRoles").read(list[UserRole.UserRole])
    )(User.apply _)


  import EnumUtils.enumWrites
  import mongo_models.LinkedProvider.linkedProviderWrites
  import play.api.libs.json.Writes._

  implicit val userWrites: Writes[User] = (
    (__ \ "_id").write[Option[BSONObjectID]] and
      (__ \ "created").write[Option[DateTime]] and
      (__ \ "updated").write[Option[DateTime]] and
      (__ \ "firstName").write[String] and
      (__ \ "lastName").write[String] and
      (__ \ "email").write[String] and
      (__ \ "emailValidated").write[Boolean] and
      (__ \ "phoneNumber").write[String] and
      (__ \ "lastLogin").write[DateTime] and
      (__ \ "linkedProviders").write(Writes.traversableWrites[LinkedProvider](linkedProviderWrites)) and
      (__ \ "userRoles").write(Writes.traversableWrites[UserRole.UserRole])
    )(unlift(User.unapply))

LinkedProvider:

case class LinkedProvider(userId: String,
                          providerId: String,
                          authMethod: String,
                          avatarUrl: String,
                          oAuth1Info: Option[OAuth1Info] = None,
                          oAuth2Info: Option[OAuth2Info] = None,
                          passwordInfo: Option[PasswordInfo] = None) {

}

object LinkedProvider {

  implicit val o1 = Json.format[OAuth1Info]
  implicit val o2 = Json.format[OAuth2Info]
  implicit val p = Json.format[PasswordInfo]

  implicit val linkedProviderReads: Reads[LinkedProvider] = (
    (__ \ "userId").read[String] and
      (__ \ "providerId").read[String] and
      (__ \ "authMethod").read[String] and
      (__ \ "avatarUrl").read[String] and
        (__ \ "oAuth1Info").read[Option[OAuth1Info]] and
        (__ \ "oAuth2Info").read[Option[OAuth2Info]] and
          (__ \ "passwordInfo").read[Option[PasswordInfo]]
    )(LinkedProvider.apply _)

  implicit val linkedProviderWrites: Writes[LinkedProvider] = (
    (__ \ "userId").write[String] and
      (__ \ "providerId").write[String] and
      (__ \ "authMethod").write[String] and
      (__ \ "avatarUrl").write[String] and
      (__ \ "oAuth1Info").write[Option[OAuth1Info]] and
      (__ \ "oAuth2Info").write[Option[OAuth2Info]] and
      (__ \ "passwordInfo").write[Option[PasswordInfo]]
    )(unlift(LinkedProvider.unapply))
}

Как видите, я реализовал операции чтения и записи, чтобы объекты правильно сохранялись в экземпляре mongoDB. При вставке с новым объектом все работает как часы, и структура объекта правильно сохраняется и извлекается из mongoDB.

У меня проблемы с выяснением того, как обрабатывать обновления. Допустим, я хочу обновить некоторые значения моего пользовательского объекта следующим образом:

val userForUpdate = user.copy(
       firstName = identity.firstName,
       lastName = identity.lastName,
       email = identity.email.getOrElse(""),
       lastLogin = DateTime.now()
       )

А затем вызовите мой метод обновления:

UserDAO.update(user._id.get.stringify, userForUpdate, false)

Метод обновления:

def update(id: String, document: T, upsert: Boolean)(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
    }
  }

Метод DBQueryBuilder.set():

def set[T](data: T)(implicit writer: Writes[T]): JsObject = Json.obj("$set" -> data)

Это вызовет эту ошибку:

[error] application - DB operation failed: [message=DatabaseException['Mod on _id not allowed' (code = 10148)]]
[error] application - DatabaseException: [code=10148, isNotAPrimaryError=false]

так как в записи (__ \ "_id").write[Option[BSONObjectID]] указано, что поле _id также должно быть сериализовано при вызове метода DBQueryBuilder.set(). Обновление _id, как мы знаем, не разрешено и определенно не должно выполняться в этом случае.

Итак, мой вопрос: как мне справиться с этим? Я предполагаю, что есть какой-то умный способ Scala, который не требует написания всего "$set" -> запроса? Может быть, создать лучший DBQueryBuilder? Может быть, определить другое определение записи?
Пожалуйста, дайте свой лучший шанс и помните, что я новичок в Scala, так что будьте осторожны =)!


person jakob    schedule 15.08.2013    source источник
comment
требуется ли использование реактивного монго? в противном случае вы можете взглянуть на play-salat, ORM для официально поддерживаемого драйвера casbah mongodb. . он думает, что поставляется со всем, что вам нужно, без необходимости писать, писать и читать.   -  person pichsenmeister    schedule 16.08.2013
comment
Привет! Спасибо за ваш ответ. Да, требуется использование ReactiveMongo.   -  person jakob    schedule 16.08.2013


Ответы (2)


Вместо

def set[T](data: T)(implicit writer: Writes[T]): JsObject = Json.obj("$set" -> data)

Вы могли бы использовать что-то вроде этого

def set[T](data: T)(implicit writer: Writes[T]): JsObject = {
    val data = Json.obj(data)
    val dataWithoutId = data.remove("_id")
    Json.obj("$set" -> dataWithoutId)
}

В библиотеке Play Json должно быть что-то, чтобы удалить поле _id (но это может быть не "удалить (...)"...)


Поскольку вы используете опцию, вы, вероятно, напишете что-то вроде:

val userForUpdate = user.copy(
       id = None,
       firstName = identity.firstName,
       lastName = identity.lastName,
       email = identity.email.getOrElse(""),
       lastLogin = DateTime.now()
       )

Вероятно, лучший способ — использовать общий трейт для всех классов, представляющих документы Mongo (может быть, T, который вы используете?) и добавить к трейту возможность удалить идентификатор (установить для него значение none).

Таким образом, вы можете позвонить:

def set[T](data: T)(implicit writer: Writes[T]): JsObject = Json.obj("$set" -> data.copy(_id = None))
person Sebastien Lorber    schedule 16.08.2013
comment
Всем привет! Спасибо за ваш ответ! Установка для поля _id значения None не будет работать, поскольку поле id по-прежнему будет существовать в обновленном запросе. Но ваш способ удаления поля может сработать. Посмотрите мой ответ, он немного откровенен и может быть сокращен, что вы думаете? - person jakob; 16.08.2013

Как насчет этого?

def set[T](data: T)(implicit writer: Writes[T]): JsObject = {
    val jsonTransformer = (__ \ '$set \ '_id).json.prune
    val dataWithId = Json.obj("$set" -> data)
    val withoutId = dataWithId.transform(jsonTransformer).get
    withoutId
}
person jakob    schedule 16.08.2013
comment
это кажется хорошим, обрезка кажется способом сделать remove() моего ответа - person Sebastien Lorber; 16.08.2013