Я собираюсь показать вам, как я решил решить, как сохранить экземпляры в Кассандре , используя Гектор , в функции 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черта, которую я смешиваю со своими актерами … Но это для другого поста в блоге!