Статьи

Как сохранить экземпляры в Кассандре, используя Гектор и Скала

Я собираюсь показать вам, как я решил решить, как сохранить экземпляры в Кассандре , используя Гектор , в функции insert(instance)в Scala. Чтобы понять, как эти экземпляры будут сохраняться, я буду использовать классы типов. Больше, чем просто код, я объясню и покажу каждый шаг моего дизайна.

Ускоренный курс на Кассандре

Cassandra — это база данных без схемы; Чтобы понять это, вот наиболее важные концепции и их свободное сопоставление с аналогами реляционных баз данных:

  • пространство клавиш — схема; база данных
  • семейство столбцов — таблица, с ключом и строками
  • ключ — первичный ключ
  • row — коллекция столбцов; строки в семействе столбцов могут иметь совершенно разные столбцы
  • столбец — столбец

Вставляя данные в Cassandra, мы должны иметь возможность сериализовать данные для вставки. Для этого мы должны знать тип ключа, а также имена и типы всех столбцов.

Вернуться к Скала

Вернемся к нашей insert(instance)функции. Интуитивно понятно, что мы должны понимать, что произойдет, если мы вставим простой класс case:

case class User(username: String,
                password: String,
                firstName: String,
                lastName: String,
                id: UUID)

При вызове insert(User("janm", "yeah right, like I'd tell ya!", "Jan", "Machacek", UUID.randomUUID))мы ожидаем новую строку с ключом, равным значению id; с четырьмя колоннами ( username -> janm, password -> ..., …) в семье колонке user .

Кажется, мне нужно уметь (игнорируя установку значений столбцов, которые я оставлю в качестве упражнения для читателей):

  • Извлечь семейство столбцов, учитывая экземпляр
  • извлечь ключ, учитывая экземпляр
  • получить сериализатор для извлеченного ключа
  • установить столбцы, учитывая извлеченный ключ, семейство столбцов и Mutator

Давайте превратим это в Scala-код

package object hector {
  type KeySerializer[K] = () => Serializer[K]
  type KeyExtractor[A, K] = A => K
  type ColumnFamilyExtractor[A] = A => String
  type ColumnExtractor[A, K] = A => (K, String, Mutator[K]) => Unit
}

Теперь, когда я создал эти типы, давайте неявно передадим ихinsert функции :

trait Hector {

  def keyspace: Keyspace

  def insert[A, Key](instance: A)
               (implicit keySerializer: KeySerializer[Key],
                keyExtractor: KeyExtractor[A, Key],
                columnFamilyExtractor: ColumnFamilyExtractor[A],
                columnExtractor: ColumnExtractor[A, Key]) {
    val mutator = HFactory.createMutator(keyspace, keySerializer())
    val key = keyExtractor(instance)
    val columnFamily = columnFamilyExtractor(instance)
    columnExtractor(instance)(key, columnFamily, mutator)

    mutator.execute()
  }

}

insertФункция в Hectorчерте использует API Hector Java для Кассандры; Keyspaceэкземпляр является ссылкой на ключевое пространство в Кассандре.
В первой строке insertфункции мы создаем Mutator[K]для пространства ключей, предоставляя Serializer[K]мы получили из keySerializer. Далее мы используем keyExtractorдля извлечения значения ключа Kиз instance; затем мы извлекаем имя семейства столбцов из того же instance. Наконец, мы получаем функцию, которая вызывает addInsertionиз Mutator[K]; и мы завершаем тело, выполняя вставки в очереди.

Simples!

Типы классов

Чтобы использовать Hectorпризнак , чтобы вставить некоторые экземпляры, мне нужны экземпляры классов типов KeySerializer[K], KeyExtractor[A, K], ColumnFamilyExtractor[A]и ColumnExtractor[A, K]для типов , которые я вставляя. Э-что?

case class User(username: String,
                password: String,
                firstName: String,
                lastName: String,
                id: UUID) 

object Main extends App with Hector {
  def keyspace = // connect to the keyspace

  insert(User("janm", 
              "yeah right, like I'd tell ya!", 
              "Jan", 
              "Machacek", 
              UUID.randomUUID))
}

Без экземпляров классов типов код не будет компилироваться: нет неявных значений, которые можно назначить неявным параметрам insertфункции.

Дом для занятий типа

Прежде чем мы перейдем к реализации экземпляров классов типов, мы должны решить, где они «живут». Поскольку мы хотим, чтобы наш код был гибким, хорошее место для экземпляров классов типов имеют черты; черты, которые вы можете смешать, где бы вы ни использовали Hectorчерту. Все потому, что в одном случае вы вставляете поле case classwith id: UUID; в другом случае вы вставляете с classпомощью @Id key: Stringgetter. Я хотел бы иметь возможность написать:

object Main extends App 
            with Hector 
            with UUIDKeySerializer 
            with UUIDIdKeyExtractor 
            with ... {
}

Или во втором случае

object Main extends App 
            with Hector 
            with StringKeySerializer 
            with StringAnnotatedKeyExtractor 
            with ... {
}

Итак, давайте реализуем экземпляры этих классов типов.

Экземпляр KeySerializer [K]

Мы реализуем KeySerializer[K]для UUIDкак K:

trait UUIDKeySerializer {

  implicit object UUIDKeySerializer extends KeySerializer[UUID] {
    def apply() = UUIDSerializer.get()
  }

}

Таким образом, для ключа типа UUIDкомпилятор может найти неявное значение KeyExtractor[UUID]и предоставить его для вызова insertфункции. Следующая задача состоит в том, чтобы иметь возможность извлечь значение ключа.

Экземпляр KeyExtractor [A, K]

Мы звоним insert(User(..., UUID.randomUUID)); и тип ключа UUID. Мы реализуем экземпляр класса KeyExtractor[A <: {def id: K}, K], который извлекает ключ типа Kиз некоторого типа A, который содержит метод get с именем idreturning K. Затем мы дополнительно специализируем экземпляр класса type на KeyExtractor[A <: {def id: UUID}, UUID]соответствие нашему классу case.

trait IdKeyExtractor {

  class IdKeyExtractor[A <: {def id: K}, K] 
    extends KeyExtractor[A, K] {

    def apply(value: A) = value.id
  }

}

trait UUIDIdKeyExtractor extends IdKeyExtractor {

  implicit def UUIDIdKeyExtractor[A <: {def id: UUID}] = 
    new IDKeyExtractor[A, UUID]

}

Отлично — когда я вызываю insert(User(..., UUID.randomUUID)), компилятор обнаружит, что единственный возможный экземпляр класса типа для KeyExtractor[A, K]— это UUIDIdKeyExtractor[User, UUID](означает, что тип Kявляется сейчас UUID), что означает, что единственный применимый экземпляр класса типа для KeySerializer[K]— это UUIDKeySerializer. Onwards!

Экземпляр ColumnFamilyExtractor [A]

Прежде чем мы сможем вставить строки (с ключами и столбцами), мы должны знать имя семейства столбцов. Для простоты давайте возьмем подход, аналогичный JPA, и используем имя типа [simple] экземпляров, которые мы вставляем, в качестве имени семейства столбцов. В нашем случае мы вставляем экземпляры User, поэтому семейство столбцов должно быть user. Таким образом, экземпляр ColumnFamilyExtractor[A]:

trait TypeNameColumnFamilyExtractor {

  implicit object TypeNameColumnFamilyExtractor 
    extends ColumnFamilyExtractor[AnyRef] {
    def apply(v1: AnyRef) = v1.getClass.getSimpleName.toLowerCase
  }

}

Таким образом, компилятор знает , как получить в свои руки экземпляр KeyExtractor[A, K], KeySerializer[K], ColumnFamilyExtractor[A]где Aесть Userи Kесть UUID. Теперь мы должны установить значения столбца.

Экземпляр ColumnExtractor [A, K]

Я просто обрисую в общих чертах реализацию и оставлю подробности любопытным читателям — не потому, что реализация сложна, а потому что этот пост в блоге становится слишком длинным. В любом случае, скелет экземпляра ColumnExtractor[A, K]для case-классов:

trait ProductColumnExtractor {

  implicit def ProductColumnExtractor[K] = new ColumnExtractor[Product, K] {
    def apply(value: Product) = { 
        (key: K, columnFamily: String, mutator: Mutator[K]) =>
        // TODO: extract the values and serialize them
        for-all-fields {
          val fieldValue = ///
          val fieldName = ///

          // as an example for String columns, you could call
          mutator.addInsertion(key, columnFamily, 
            HFactory.createStringColumn(fieldName, fieldValue))
        }
        ()
      }
  }

}

использование

Это завершает экземпляры классов типов, которые мне нужны для вставки Userэкземпляров; все, что мне нужно сделать, это смешать подходящие черты, которые содержат правильные экземпляры классов типов.

case class User(username: String,
                password: String,
                firstName: String,
                lastName: String,
                id: UUID) 

object Main extends App 
  with Hector 
  with UUIDIdKeyExtractor 
  with UUIDKeySerializer
  with TypeNameColumnFamilyExtractor 
  with ProductColumnExtractor {

  def keyspace = // connect to the keyspace

  insert(User("janm", 
              "yeah right, like I'd tell ya!", 
              "Jan", 
              "Machacek", 
              UUID.randomUUID))
}

Чего все это достигло? Ну, у меня есть проверка во время компиляции всех типов, которые я вставляю; и если я решу вставить значение, для которого у меня нет экземпляров классов типов, я получу ошибку компилятора! Это гораздо лучше, чем обнаруживать, что что-то не работает во время выполнения.

Прощальный подарок

Естественно, этот код появится в моей учетной записи GitHub по адресу https://github.com/janm399 в ближайшие несколько дней, но я приведу вам пример того, как я использовал этот самый код в актере Akka (с Конфигурация шаблона Akka ):

class UserActor extends Actor 
  with Configured 
  with Hector 
  with UUIDIdKeyExtractor 
  with UUIDKeySerializer
  with TypeNameColumnFamilyExtractor 
  with ProductColumnExtractor {

  def keyspace = configured[Keyspace]

  protected def receive = {
    case Register(user) =>
      // business logic left to readers' imagination!

      insert(user)
    
  }
}

Наконец, поскольку у меня много актеров, которым нужны одинаковые экземпляры классов типов, у меня есть DefaultHectorчерта:

trait DefaultHector extends Hector
  with UUIDIdKeyExtractor 
  with UUIDKeySerializer
  with TypeNameColumnFamilyExtractor 
  with ProductColumnExtractor

И это DefaultHectorчерта, которую я смешиваю со своими актерами … Но это для другого поста в блоге!