Я собираюсь показать вам, как я решил решить, как сохранить экземпляры в Кассандре , используя Гектор , в функции 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 class
with id: UUID
; в другом случае вы вставляете с class
помощью @Id key: String
getter. Я хотел бы иметь возможность написать:
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 с именем id
returning 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
черта, которую я смешиваю со своими актерами … Но это для другого поста в блоге!