Статьи

Параметры CRUD в Scala

 Вот запись в блоге, в которой рассматриваются различные подходы CRUD в вашем коде Scala. Я покажу вам три разных подхода, подчеркнув достоинства и недостатки каждого из них. Основной мотивацией стали наши эксперименты с проектом Scala 2.10 без каски.

Подход 1

Первый подход состоит в том, чтобы определить Crud[T]для некоторого типа Tи требовать, чтобы MongoSerializerи IdentityQueryBuilderдля этого Tбыли смешаны.

trait Crud[T] {
  this: MongoSerializer[T] with IdentityQueryBuilder[T] =>
 
  def collection: DBCollection  

  def updateFirst(entity: T): Option[T] = {
    val id = createIdQuery(entity)
    if (collection.findAndModify(id, serialize(entity)) != null) Some(entity)
    else None
  }

}

Это позволяет нам создавать тривиальные компоненты CRUD:

val userCrud = new Crud[User] 
   with MongoSerializer[User] with IdentityQueryBuilder[User] {

   def collection: DBCollection = 
     // set the collection where the Users will end up
   def serialize: ...
   def deserialize: ...
   def createIdQuery: ...
}

Мы хотели бы сохранить наши печатать и иметь MongoSerializerи IdentityQueryBuilderкомпоненты заранее подготовленные и , возможно , обобщаются быть применимы к различным типам. Это IdentityQueryBuilderбыло бы легко определить, давая нам:

trait IdUuidIdentityQueryBuilder[A <: {def id: UUID}] 
  extends IdentityQueryBuilder[A] {
  ...
}

Это MongoSerializerдаст нам больше головной боли. Мы хотели бы использовать существующие сериализаторы JSON, но они неизбежно требуют больше информации о типах, чем мы можем предоставить MongoSerializer[A]. (Самый простой способ думать об этом — это то, что параметр типа Aдолжен быть изменен **.)

Подход 2

Во втором подходе нам нужен доступ к полным параметрам типа. Это означает, что наша Crud[A]больше не может быть чертой. Это должно стать абстрактным классом:

abstract class Crud[A : MongoSerializer : IdentityQueryBuilder] {
  val serializer = implicitly[MongoSerializer[T]]
  val identityQueryBuilder = implicitly[IdentityQueryBuilder[T]]

  def collection: DBCollection  

  def updateFirst(entity: T): Option[T] = {
    val id = identityQueryBuilder.createIdQuery(entity)
    if (collection.findAndModify(id, 
      serializer.serialize(entity)) != null) Some(entity)
    else None
  }

}

Этот подход не отбрасывает методы из наследования собственного типа в методы в Crud[A]. Однако требуется, чтобы уникальный экземпляр типа MongoSerializerи IdentityQueryBuilderдля типа Aбыл доступен в области, в которой создается экземпляр Crud[A].

implicit def productMongoSerializer[A <: Product] = new MongoSerializer[A] {
  def serialize(entity: A) = ???

  def deserialize(dbObject: DBObject) = ???
}

implicit def UUIDIdIdentityFieldExtractor[A <: {def id: Long}] = 
  new IdentityQueryBuilder[A] { def createIdQuery(entity: A) = ??? }

val crud = new ImplicitCrud[SomeEntity] {
  def collection = db.getCollection("some")
}

Причина этого в том, что Scala генерирует конструктор для вышеуказанного класса как

abstract class Crud[A](
  implicit ev1: MongoSerializer[A], ev2: IdentityQueryBuilder[A])

Таким образом, по сути, уточняются параметры типа. Хорошей новостью является то, что вам могут потребоваться дополнительные классы типов. Например, было бы вполне разумно требовать, чтобы экземпляр JsonWriterбыл доступен для нашего MongoSerializerиз A:

implicit def productMongoSerializer[A <: Product : JsonWriter] = 
  new MongoSerializer[A] {
  
  val writer = implicitly[JsonWriter[A]]

  def serialize(entity: A) = ??? 

  def deserialize(dbObject: DBObject) = ???
}

Это даст нам больше свободы, но это означает, что сериализаторы / конструкторы запросов будут доступны в области, в которой мы создаем экземпляры CRUD. Это также означает, что CRUD — это одна единица, и что отделение C от R от U от D означало бы ломать Crud[A]черту.

Подход 3

Что приводит нас к последнему подходу, где мы оставляем Crudчерту как черту, но делаем ее методы полиморфными, требуя наличия классов типов. Это освобождает Crudот типов объектов, которые он обрабатывает. Это, однако, означает, что место, в DBCollectionкотором находятся объекты, должно быть указано дополнительно. Как обычно, у нас есть несколько вариантов:

3,1

Первый выбор состоит в том, чтобы просто потребовать, DBCollectionчтобы он был указан как параметр.

trait Crud {
  def updateFirst[T : MongoSerializer : IdentityQueryBuilder]
    (collection: DBCollection, entity: T): Option[T] = {
    val serializer = implicitly[MongoSerializer[T]]

    val id = implicitly[IdentityQueryBuilder[T]].createIdQuery(entity)
    if (collection.findAndModify(id, 
      serializer.serialize(entity)) != null) Some(entity)
    else None
  }
}

3,2

Во втором варианте мы карри все функции Crud:

trait Crud {
  def updateFirst[T : MongoSerializer : IdentityQueryBuilder]
    (collection: DBCollection)(entity: T): Option[T] = {
    val serializer = implicitly[MongoSerializer[T]]

    val id = implicitly[IdentityQueryBuilder[T]].createIdQuery(entity)
    if (collection.findAndModify(id, 
      serializer.serialize(entity)) != null) Some(entity)
    else None
  }
}

3,3

И третий выбор вводит CollectionProvier[A], который дает в DBCollectionзависимости от типа A.

trait Crud {
  def updateFirst[T : MongoSerializer : IdentityQueryBuilder : 
                      CollectionProvider](entity: T): Option[T] = {
    val collection = implicitly[CollectionProvider[T]].getCollection
    val serializer = implicitly[MongoSerializer[T]]

    val id = implicitly[IdentityQueryBuilder[T]].createIdQuery(entity)
    if (collection.findAndModify(id, 
      serializer.serialize(entity)) != null) Some(entity)
    else None
  }
}

Различия в коде очень тонкие, но последствия довольно велики.

// assuming the required typecalss instances are in scope

val crud31 = new Crud {}
val crud32 = new Crud {}
val crud33 = new Crud {}
val users: DBCollection = ...
val userUpdateFirst = crud32.updateFirst[User](users)

crud31.updateFirst(users, User(...))
crud32.updateFirst(users)(User(...)) 
/* or */ 
userUpdateFirst(User(...))
crud33.updateFirst(User(...))

С помощью crud31нам нужно перепрыгивать через обручи и указывать коллекцию при каждом вызове. Это может быть подвержено ошибкам. С помощью crud31мы можем применить updateFirstфункцию только к первому списку параметров, предоставив нам функцию, которая выполняет операцию обновления usersколлекции. Это все еще оставляет нас с некоторой вероятностью ошибок. Наконец, с crud33типом сущности управляет коллекцией, в которой он собирается оказаться. (Это именно то, что нам нужно, за исключением случаев, когда мы хотели бы иметь возможность изменить коллекцию.)


*** F # делает различие намного яснее: если вы используете
'a, вы определяете общий элемент, если вы используете
^a, вы определяете полиморфный элемент по отношению к типу
a. И, поскольку код заканчивается как код CLR, параметры уровня типа могут быть только общими. Так же, как Скала.