Русский
Русский
English
Статистика
Реклама

Generator

Заметка о перебираемых объектах

04.11.2020 16:22:09 | Автор: admin


Доброго времени суток, друзья!

Данная заметка не имеет особой практической ценности. С другой стороны, в ней исследуется некоторые пограничные возможности JavaScript, которые могут показаться вам интересными.

Руководство по стилю JavaScript от Goggle советует отдавать предпочтение циклу for-of там, где это возможно.

Руководство по стилю JavaScript от Airbnb не рекомендует использовать итераторы. Вместо циклов for-in и for-of следует использовать функции высшего порядка, такие как map(), every(), filter(), find(), findIndex(), reduce(), some() для итерации по массивам и Object.keys(), Object.values(), Object.entries() для итерации по массивам из объектов. Об этом позже.

Вернемся к Google. Что означает там, где это возможно?

Рассмотрим парочку примеров.

Допустим, у нас есть такой массив:

const users = ["John", "Jane", "Bob", "Alice"];

И мы хотим вывести в консоль значения его элементов. Как нам это сделать?

// вспомогательная функцияlog = (value) => console.log(value);// forfor (let i = 0; i < users.length; i++) {  log(users[i]); // John Jane Bob Alice}// for-infor (const item in users) {  log(users[item]);}// for-offor (const item of users) {  log(item);}// forEach()users.forEach((item) => log(item));// map()// побочный эффект - возвращает новый массив// поэтому в данном случае лучше использовать forEach()users.map((item) => log(item));

Все прекрасно работает без лишних усилий с нашей стороны.

Теперь предположим, что у нас есть такой объект:

const person = {  name: "John",  age: 30,  job: "developer",};

И мы хотим сделать тоже самое.

// forfor (let i = 0; i < Object.keys(person).length; i++) {  log(Object.values(person)[i]); // John 30 developer}// for-infor (const i in person) {  log(person[i]);}// for-of & Object.values()for (const i of Object.values(person)) {  log(i);}// Object.keys() & forEach()Object.keys(person).forEach((i) => log(person[i]));// Object.values() & forEach()Object.values(person).forEach((i) => log(i));// Object.entries() & forEach()Object.entries(person).forEach((i) => log(i[1]));

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

  for (const value of person) {    log(value); // TypeError: person is not iterable  }

О чем нам говорит это исключение? Оно говорит о том, что объект person, впрочем, как и любой другой объект, не является итерируемым или, как еще говорят, итерируемой (перебираемой) сущностью.

О том, что такое перебираемые сущности и итераторы, очень хорошо написано в этом разделе Современного учебника по JavaScript. С вашего позволения, я не буду заниматься копипастой. Однако, настоятельно рекомендую потратить 20 минут на его прочтение. В противном случае, дальнейшее изложение не будет иметь для вас особого смысла.

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

Вот пример, приводимый Ильей Кантором:

// имеется такой объектconst range = {  from: 1,  to: 5,};// добавляем ему свойство Symbol.iteratorrange[Symbol.iterator] = function () {  return {    // текущее значение    current: this.from,    // последнее значение    last: this.to,    // обязательный для итератора метод    next() {      // если текущее значение меньше последнего      if (this.current <= this.last) {        // возвращаем такой объект, увеличивая значение текущего значения        return { done: false, value: this.current++ };      } else {        // иначе сообщаем о том, что значений для перебора больше нет        return { done: true };      }    },  };};for (const num of range) log(num); // 1 2 3 4 5// работает!

По сути, приведенный пример это генератор, созданный с помощью итератора. Но вернемся к нашему объекту. Функция для превращения обычного объекта в итерируемый может выглядеть следующим образом:

const makeIterator = (obj) => {  // добавляем неперечисляемое свойство "size", аналогичное свойству "length" массива  Object.defineProperty(obj, "size", {    value: Object.keys(obj).length,  });  obj[Symbol.iterator] = (    i = 0,    values = Object.values(obj)  ) => ({    next: () => (      i < obj.size        ? { done: false, value: values[i++] }        : { done: true }    ),  });};

Проверяем:

makeIterator(person);for (const value of person) {  log(value); // John 30 developer}

Получилось! Теперь мы легко можем преобразовать такой объект в массив, а также получить количество его элементов через свойство size:

const arr = Array.from(person);log(arr); // ["John", 30, "developer"]log(arr.size); // 3

Мы можем упростить код нашей функции, если вместо итератора воспользуемся генератором:

const makeGenerator = (obj) => {  // другое неперечисляемое свойство  // возвращающее логическое значение  Object.defineProperty(obj, "isAdult", {    value: obj["age"] > 18,  });  obj[Symbol.iterator] = function* () {    for (const i in this) {      yield this[i];    }  };};makeGenerator(person);for (const value of person) {  log(value); // John 30 developer}const arr = [...person];log(arr); // ["John", 30, "developer"]log(person.isAdult); // true

Можем ли мы использовать метод next сразу после создания итерируемого объекта?

log(person.next().value); // TypeError: person.next is not a function

Для того, чтобы у нас появилась такая возможность, необходимо сначала вызвать Symbol.iterator объекта:

const iterablePerson = person[Symbol.iterator]();log(iterablePerson.next()); // { value: "John", done: false }log(iterablePerson.next().value); // 30log(iterablePerson.next().value); // developerlog(iterablePerson.next().done); // true

Стоит отметить, что при наобходимости создания итерируемого объекта, лучше сразу определить в нем Symbol.iterator. На примере нашего объекта:

const person = {  name: "John",  age: 30,  job: "developer",  [Symbol.iterator]: function* () {    for (const i in this) {      yield this[i];    }  },};

Двигаемся дальше. Куда дальше? В метапрограммирование. Что если мы хотим получать значения свойств объекта по индексу, как в массивах? И что если мы хотим, чтобы определенные свойства объекта были иммутабельными. Реализуем это поведение с помощью прокси. Почему с помощью прокси? Ну, хотя бы потому, что можем:

const makeProxy = (obj, values = Object.values(obj)) =>  new Proxy(obj, {    get(target, key) {      // преобразуем ключ в целое число      key = parseInt(key, 10);      // если ключ является числом, если он больше или равен 0 и меньше длины объекта      if (key !== NaN && key >= 0 && key < target.size) {        // возвращаем соответствующее свойство        return values[key];      } else {        // иначе сообщаем, что такого свойства нет        throw new Error("no such property");      }    },    set(target, prop, value) {      // при попытке перезаписать свойство "name" или свойство "age"      if (prop === "name" || prop === "age") {        // выбрасываем исключение        throw new Error(`this property can't be changed`);      } else {        // иначе добавляем свойство в объект        target[prop] = value;        return true;      }    },  });const proxyPerson = makeProxy(person);// получаем свойствоlog(proxyPerson[0]); // John// пытаемся получить несуществующее свойствоlog(proxyPerson[2]); // Error: no such property// добавляем новое свойствоlog((proxyPerson[2] = "coding")); // true// пытаемся перезаписать иммутабельное свойствоlog((proxyPerson.name = "Bob")); // Error: this property can't be changed

Какие выводы мы можем сделать из всего этого? Создать итерируемый объект своими силами, конечно, можно (это JavaScript, детка), но вопрос в том, зачем это делать. Следует согласиться с Руководством от Airbnb в том, что нативных методов более чем достаточно для решения всего спектра задач, связанных с перебором ключей и значений объектов, нет необходимости изобретать велосипед. Руководство же от Google можно уточнить тем, что цикл for-of следует предпочитать для массивов и массивов из объектов, для объектов же как таковых можно использовать цикл for-in, но лучше встроенные функции.

Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание.
Подробнее..

Kotlite и Kotgres генераторы SQL и JDBC кода на Kotlin для Sqlite и Postgresql

01.04.2021 22:22:46 | Автор: admin

Eсть таблица:

CREATE TABLE person(    id         uuid primary key,    name       text,    birth_date date)

и соотвтетствующий ей дата-класс:

data class Person(    val id: UUID,    val name: String,    val birthDate: LocalDate,)

Что если для того чтобы выполнить базовые CRUD операции:

  • сохранить список Person-ов

  • вычитать всё из таблицы

  • удалить все записи в таблице

  • найти по ID

  • удалить по имени

будет достаточно создать интерфейс:

@SqliteRepositoryinterface PersonRepository : Repository<People> {    fun saveAll(people: List<Person>)    fun selectAll(): List<Person>    fun deleteAll()    fun selectBy(id: UUID): Person?    fun deleteBy(name: String)}

а имплеметнация будет сгенерирована автоматически.

Напоминает Spring Data? Но это не Spring, не Hibernate и даже не JPA.

TL;DR

  • Kotlin-центричная библиотека (не фреймворк)

  • Не ORM (не содержит JPA)

  • Генерирует SQL и JDBC до этапа компиляции (Kotlin Annotation Precessing)

  • Нет магии в рантайме

  • Сгенерированный код отформатирован, можно дебажить, работает навигация, можноскопировать в проект и модифицировать

  • Удобный DSL для работы с базой

  • Есть 2 имплементации: под Postgres и Sqlite

Конфигурация

На данный момент есть 2 реализации этой библиотеки: для Postgresql и Sqlite. В данной статье примеры будут для Sqlite.

Для начала нужно сконфигурировать Gradle (да простят меня пользователи Maven):

build.gradle.kts

plugins {    kotlin("kapt") version "1.4.31" //(1)    kotlin("plugin.serialization") version "1.4.31"}dependencies {    implementation("com.github.mfarsikov:kotlite-core:0.5.0") //(2)    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0") //(3)    implementation("org.xerial:sqlite-jdbc:3.34.0") //(4)    kapt("com.github.mfarsikov:kotlite-kapt:0.5.0") //(5)    }kapt {    arguments {        arg("kotlite.db.qualifiedName", "my.pkg.DB") //(6)    }}
Пояснения по build.gradle.kts
  1. Добавить плагин для обработки аннотаций и генерации кода (`kapt`).

  2. Добавить зависимость на core-часть библиотеки. Она содержит необходимые аннотации, и некоторый обвязочный код.

  3. Сериализация в/из JSON используется для вложенных коллекций.

  4. Непосредственно драйвер Sqlite базы.

  5. Плагин создаст kapt конфигурацию, в которую нужно включить зависимость на `kapt`-часть библиотеки. Именно она занимается генерацией SQL запросов и кода JDBC.

  6. Необходимо указать полное имя класса (включая пакет), через который мы будем работать с базой данных (этот класс также будет сгенерирован).

Репозиторий

import kotlite.annotations.SqliteRepository@SqliteRepositoryinterface PersonRepository

От такого репозитория пользы немного, но уже для него Kotlite может сгенерировать имплементацию.

Команда ./gradlew kaptKotlin сгенерирует:

build/generated/source/kapt/PersonRepositoryImpl.kt
@Generatedinternal class PersonRepositoryImpl(    private val connection: Connection) : PersonRepository

Первый запрос

import kotlite.annotations.Queryimport kotlite.annotations.SqliteRepository@SqliteRepositoryinterface PersonRepository {    @Query("SELECT id, name, birth_date FROM person")    fun findPeople(): List<Person>}

Kotlite знает достаточно, чтобы сгенерировать код для этого запроса:

  • Из возвращаемого типа List следует, что записей может быть от 0 до N

  • Из возвращаемого типа Person следует, что каждый кортеж будет содержать три поля: id, name и birth_date.

  • По конвенции, для поля в классе birthDate ожидается значение в кортеже birth_date

В результате сгенерируется метод:

build/generated/source/kapt/PersonRepositoryImpl.kt
public override fun findPeople(): List<Person> {    val query = "SELECT id, name, birth_date FROM person"    return connection.prepareStatement(query).use {        it.executeQuery().use {            val acc = mutableListOf<Person>()            while (it.next()) {                acc +=                    Person(                        birthDate = it.getObject("birth_date", LocalDate::class.java),                        id = it.getObject("id", java.util.UUID::class.java),                        name = it.getString("name"),                    )            }            acc        }    }}

Как выполнить этот запрос?

В конфигурации (build.gradle.kts) мы указывали, что нужно сгенерировать класс my.pkg.DB. Это главный объект, через который осуществляется доступ ко всем сгенерированным репозиториям. Для его создания нужен DataSource. Все объявленные нами репозитории доступны внутри транзакции:

main.kt

import my.pkg.DBimport org.sqlite.SQLiteDataSourcefun main() {    val datasource = SQLiteDataSource().apply {        url = "jdbc:sqlite:path/to/my/test.db"    }    val db = DB(datasource)    val people: List<Person> = db.transaction {        personRepository.findPeople()    }    println(people)}

Запрос с параметрами

@Query("SELECT id, name, birth_date FROM person WHERE name = :firstName")fun findPeopleBy(firstName: String): List<Person>

Параметры метода могут быть использованы в запросе. Перед именем параметра должно быть двоеточие.

сгенерированный метод
public override fun findPeopleBy(firstName: String): List<Person> {    val query = "SELECT id, name, birth_date FROM person WHERE name = ?"    return connection.prepareStatement(query).use {        it.setString(1, firstName)        it.executeQuery().use {            val acc = mutableListOf<Person>()            while (it.next()) {                acc +=                    Person(                        birthDate = LocalDate.parse(it.getString("birth_date")),                        id = UUID.fromString(it.getString("id")),                        name = it.getString("name"),                    )            }            acc        }    }}

Возвращаемые типы

В зависимости от возвращаемого типа Kotlite генерирует различное поведение.

Список (List)

Cамый обычный тип. Полностью соответствует тому, что возвращает база от 0 до N элементов. Другие коллекции не поддерживаются.

Сущность (Entity)

На первый взгляд ничего особенного, но есть несколько нюансов:

  • что если запрос не вернет ни одного значения

  • что если запрос вернет больше одного значения

В обоих случаях сгенерированный код выбросит исключение. Для второго случая предусмотрена небольшая оптимизация в виде добавления LIMIT 2.

@Query("SELECT id, name, birth_date FROM person WHERE name = :name")fun findPersonBy(name: String): Person
Сгенерированный код
public override fun findPersonBy(name: String): Person {    val query = """     |SELECT id, name, birth_date FROM person WHERE name = ?     |LIMIT 2     """.trimMargin()    return connection.prepareStatement(query).use {        it.setString(1, name)        it.executeQuery().use {            if (it.next()) {                val result =                    Person(                        birthDate = LocalDate.parse(it.getString("birth_date")),                        id = UUID.fromString(it.getString("id")),                        name = it.getString("name"),                    )                if (it.next()) {                    throw IllegalStateException("Query has returned more than one element")                }                result            } else {                throw NoSuchElementException()            }        }    }}

Для выбора первого значения можно пометить метод аннотацией kotlite.annotations.First

Скаляр

Возвращаемым типом может быть не только сущность, но и любое скалярное ("примитивное") значение. Например: Int, String, UUID LocalDateи т.п.

@Query("SELECT name FROM person WHERE id = :id")fun findPersonNameBy(id: UUID): String

Если запрос не вернул ни одного значения, или если вернул больше одного, то так-же как и для сущности будет выброшено исключение.

Сгенерированный метод
public override fun findPersonNameBy(id: UUID): String {    val query = """        |SELECT name FROM person WHERE id = ?        |LIMIT 2        """.trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, id)        it.executeQuery().use {            if (it.next()) {                val result =                    it.getString(1)                if (it.next()) {                    throw IllegalStateException("Query has returned more than one element")                }                result            } else {                throw NoSuchElementException()            }        }    }}

Для выбора первого значения можно пометить метод аннотацией kotlite.annotations.First

Nullable значения

Скаляр или сущность могут быть объявлены как Nullable. В таком случае вернется nullесли запрос не вернул ни одной записи.

@Query("SELECT name FROM person WHERE id = :id")fun findPersonNameBy(id: UUID): String?
Сгенерированный метод
public override fun findPersonNameBy(id: UUID): String? {    val query = """     |SELECT name FROM person WHERE id = ?     |LIMIT 2     """.trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, id)        it.executeQuery().use {            if (it.next()) {                val result =                    it.getString(1)                if (it.next()) {                    throw IllegalStateException("Query has returned more than one element")                }                result            } else {                null            }        }    }}

Постраничный вывод (Pagination)

Pageableопределяет сколько элементов размещается на странице, и какую страницу нужно выбрать

import kotlite.aux.page.Pageimport kotlite.aux.page.Pageable@SqliteRepositoryinterface PersonRepository : Repository<Person> {    @Query("SELECT name FROM person")    fun selectAll(pageable: Pageable): Page<String>}
Сгенерированный метод
public override fun selectAll(pageable: Pageable): Page<String> {    val query = """        |SELECT name FROM person        |LIMIT ? OFFSET ?        """.trimMargin()    return connection.prepareStatement(query).use {        it.setInt(1, pageable.pageSize)        it.setInt(2, pageable.offset)        it.executeQuery().use {            val acc = mutableListOf<String>()            while (it.next()) {                acc +=                    it.getString(1)            }            Page(pageable, acc)        }    }}

Генерация SQL

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

Для этого нужно дать библиотеке немного информации о том, с какой сущностью мы работаем. Делается это через переменную типа, интерфейса kotlite.aux.Repository

import kotlite.annotations.SqliteRepositoryimport kotlite.aux.Repository@SqliteRepositoryinterface PersonRepository : Repository<Person> 

Теперь библиотека знает достаточно о нашей сущности, чтобы можно было сгенерировать SQL автоматически.

Известно название таблицы. По конвенции это имя клaсса, сконвертированное из UpperCamelCaseв snake_case. Название таблицы может быть явно указано в аннотацииkotlite.annotations.Table.

Также известно количество, названия и типы колонок таблицы. Названия колонок конвертируются из camelCaseв snake_case Альтернативно, название может быть указано в аннотации kotlite.annotations.Column

Что это нам дает?

Сохранение и обновление

Для любого метода, имя которого начинается на save(либо который помечен аннотацией kotlite.annotations.Save) будет сгенерирован INSERT . Такой метод должен принимать в качестве параметро либо саму сущность, либо список сущностей. Возвращаемый тип должен быть Unit

fun save(person: Person)
Сгенерированный метод
public override fun save(person: Person): Unit {    val query = """        |INSERT INTO person        |("birth_date", "id", "name")        |VALUES (?, ?, ?)        """.trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, person.birthDate)        it.setObject(2, person.id)        it.setString(3, person.name)        it.executeUpdate()    }}

Если сущность имеет первичный ключ (как минимум одно из полей помечено аннотацией kotlite.annotations.ID) будет сгенерирован INSERT/UPDATE

Сгенерированный метод
public override fun save(person: Person): Unit {    val query = """    |INSERT INTO person    |("birth_date", "id", "name")    |VALUES (?, ?, ?)    |ON CONFLICT (id) DO     |UPDATE SET "birth_date" = EXCLUDED."birth_date", "id" = EXCLUDED."id", "name" = EXCLUDED."name"    |""".trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, person.birthDate)        it.setObject(2, person.id)        it.setString(3, person.name)        it.executeUpdate()    }}

Это поведение можно переопределить аннотацией:

import kotlite.annotations.OnConflictFail@OnConflictFailfun save(person: Person)

Оптимистическая блокировка

Если числовое поле класса помечено аннотацией kotlite.annotations.Versionдля такой сущности запросы обновления и удаления будут содержать проверку текущей версии

Сгенерированные методы
public override fun save(person: Person): Unit {    val query = """        |INSERT INTO person        |("birth_date", "id", "name", "version")        |VALUES (?, ?, ?, ? + 1)        |ON CONFLICT (id) DO         |UPDATE SET "birth_date" = EXCLUDED."birth_date", "id" = EXCLUDED."id", "name" = EXCLUDED."name", "version" = EXCLUDED."version"        |WHERE person.version = EXCLUDED.version - 1        """.trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, person.birthDate)        it.setObject(2, person.id)        it.setString(3, person.name)        it.setInt(4, person.version)        val rows = it.executeUpdate()        if (rows != 1) {            throw OptimisticLockFailException()        }    }}public override fun delete(person: Person): Unit {    val query = """        |DELETE         |FROM person        |WHERE "id" = ? AND "version" = ?        """.trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, person.id)        it.setInt(2, person.version)        val rows = it.executeUpdate()        if (rows != 1) {            throw OptimisticLockFailException()        }    }}

Сге

Удаление

Для любого метода, имя которого начинается на delete (или который помечен аннотацией kotlite.annotations.Delete) будет сгенерирован DELETE

fun deleteAll()
Сгенерированный метод
public override fun deleteAll(): Unit {    val query = """    |DELETE     |FROM person    """.trimMargin()    return connection.prepareStatement(query).use {        it.executeUpdate()    }}

Такой метод может принимать сущность в качестве параметра:

fun delete(person: Person)

Удаление будет происходить по всем полям сущности

Сгенерированный метод
public override fun delete(person: Person): Unit {    val query = """        |DELETE         |FROM person        |WHERE "birth_date" = ? AND "id" = ? AND "name" = ?        """.trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, person.birthDate)        it.setObject(2, person.id)        it.setString(3, person.name)        it.executeUpdate()    }}

Если сущность имеет первичный ключ (хотя бы одно поле помечено kotlite.annotations.Id) удаление будет по первичному ключу:

Сгенерированный метод
public override fun delete(person: Person): Unit {    val query = """    |DELETE     |FROM person    |WHERE "id" = ?    """.trimMargin()    return connection.prepareStatement(query).use {        it.setObject(1, person.id)        it.executeUpdate()    }}

Кроме этого метод удаления может так-же принимать и другие параметры, см. разделы "Метод с параметрами" и "Сложные условия" ниже.

Метод без параметров

Любой метод, объявленный в репозитории, считается запросом типа SELECT(кроме методов, названия которых начинаются со слов saveи delete).

fun selectAll(): List<Person>
Сгенерированный метод
public override fun selectAll(): List<Person> {    val query = """     |SELECT "birth_date", "id", "name"     |FROM person     """.trimMargin()    return connection.prepareStatement(query).use {        it.executeQuery().use {            val acc = mutableListOf<Person>()            while (it.next()) {                acc +=                    Person(                        birthDate = LocalDate.parse(it.getString("birth_date")),                        id = UUID.fromString(it.getString("id")),                        name = it.getString("name"),                    )            }            acc        }    }}

Функции fun selectAll(): List<Person>и fun blaBlaBla(): List<Person> ничем не отличаются друг от друга и для них будет сгенерирован абсолютно одинаковый код.

Метод с параметрами

Все параметры метода должны совпадать по названию с полями класса. Они будут использованы как условия равенства во WHEREи объединены через AND.

fun selectBy(name: String, birthDate: LocalDate): Person?
Сгенерированный метод
public override fun selectBy(name: String, birthDate: LocalDate): Person? {    val query = """     |SELECT "birth_date", "id", "name"     |FROM person     |WHERE "name" = ? AND "birth_date" = ?     |LIMIT 2     """.trimMargin()    return connection.prepareStatement(query).use {        it.setString(1, name)        it.setObject(2, birthDate)        it.executeQuery().use {            if (it.next()) {                val result =                    Person(                        birthDate = java.time.LocalDate.parse(it.getString("birth_date")),                        id = UUID.fromString(it.getString("id")),                        name = it.getString("name"),                    )                if (it.next()) {                    throw IllegalStateException("Query has returned more than one element")                }                result            } else {                null            }        }    }}

Сложные условия

Если вместо стандартного равенства нужно использовать >, <=, != и т.д., или условия должны быть объединены с помощьюOR с расстановкой скобок, для этого подойдет аннотация kotlite.annotations.Where:

@Where("name = :name OR birth_date < :birthDate")fun selectBy(name: String, birthDate: LocalDate): Person?

Её содержимое будет подставлено в запрос почти без изменений.

Сгенерированный метод
public override fun selectBy(name: String, birthDate: LocalDate): Person? {    val query = """        |SELECT "birth_date", "id", "name"        |FROM person        |WHERE name = ? OR birth_date < ?        |LIMIT 2        """.trimMargin()    return connection.prepareStatement(query).use {        it.setString(1, name)        it.setObject(2, birthDate)        it.executeQuery().use {            if (it.next()) {                val result =                    Person(                        birthDate = java.time.LocalDate.parse(it.getString("birth_date")),                        id = UUID.fromString(it.getString("id")),                        name = it.getString("name"),                    )                if (it.next()) {                    throw IllegalStateException("Query has returned more than one element")                }                result            } else {                null            }        }    }}

Сортировка

Часто вместе с постраничным выводом необходимо задать порядок:

@OrderBy("name DESC, birth_date")fun selectAll(): List<Person>
Сгенерированный метод
public override fun selectAll(): List<Person> {    val query = """    |SELECT "birth_date", "id", "name"    |FROM person    |ORDER BY name DESC, birth_date    """.trimMargin()    return connection.prepareStatement(query).use {        it.executeQuery().use {            val acc = mutableListOf<Person>()            while (it.next()) {                acc +=                    Person(                        birthDate = LocalDate.parse(it.getString("birth_date")),                        id = UUID.fromString(it.getString("id")),                        name = it.getString("name"),                    )            }            acc        }    }}

Вложенные объекты

Вложенные объекты не могут быть представлены как связь один-к-одному. Поля вложенных объектов должны быть представлены колонками в этой же таблице. Т.е. быть @Embeddableв терминах JPA.

data class Person(    val name: Name,)data class Name(    val firstName: String,    val lastName: String,)
CREATE TABLE person(    first_name text,    last_name text)

Альтернативно вложенные объекты могут быть сериализованы в JSON. Предмет для добавления в ближайшие версии.

Вложенные коллекции

Вложенные коллекции не могут быть представлены как связь один-ко-многим.Вместо этого они автоматически сериализуются в JSON.

data class Person(    val habits: List<String>)@SqliteRepositoryinterface PersonRepository: Repository<Person> {    fun save(person: Person)    fun select(): List<Person>}
Сгенерированные методы
public override fun select(): List<Person> {    val query = """    |SELECT "habits"    |FROM person    """.trimMargin()    return connection.prepareStatement(query).use {        it.executeQuery().use {            val acc = mutableListOf<Person>()            while (it.next()) {                acc +=                    Person(                        habits = Json.decodeFromString(it.getString("habits")),                    )            }            acc        }    }}public override fun save(person: Person): Unit {    val query = """    |INSERT INTO person    |("habits")    |VALUES (?)    """.trimMargin()    return connection.prepareStatement(query).use {        it.setString(1, Json.encodeToString(person.habits))        it.executeUpdate()    }}

Особенности (сравнительно с JPA/Hibernate)

  • Из-за использования SQL, рефакторинг (например, переименование поля сущности) может потребовать изменения тех запросов, которые были написаны вручную.

  • Поскольку во главу угла поставлена простота, нет возможности создавать связи `один-к-одному`, `один-ко-многим` (и нет N+1 проблемы).

  • Нет ленивых загрузок (и нет `SessionClosedException`).

  • Нет встроенного механизма конвертеров типов (не переусложнен API, библиотека решает только одну задачу).

  • Нет возможности сохранения иерархий наследования (в основном из-за личной неприязни автора к наследованию. Возможно будет добавлено в будущем).

  • Не питает иллюзий относительно легкой миграции на другую базу данных.

На этом наши полномочия всё

Спасибо за уделенное внимание.

Sqlite

Posgresql

Подробнее..
Категории: Kotlin , Postgresql , Sql , Sqlite , Generator , Jdbc , Kapt

А вы все еще генерируете данные руками? Тогда GenRocket идет к вам

06.01.2021 20:05:30 | Автор: admin

На днях я наконец-то получила свой долгожданный сертификат по работе с сервисом для генерации тестовых данных GenRocket. И теперь как сертифицированный специалист готова рассказать об этом сервисе.

НО изначально необходимо очертить проблему, которую можно решить при помощи этого сервиса.

Проблема генерации тестовых данных

Генерация тестовых данных в достаточном количестве для покрытия минимально возможных необходимых вариантов сценариев становится проблемой для многих проектов.

Почему "минимально возможных"? Потому что тут вспоминаются "Классы эквивалентности", которые сокращают количество тестовых данных от бесконечности до полезного набора.

Почему "необходимых"? А тут вспоминаются "Тестовое покрытие", которое говорит, что тестовые данные должны вести к покрытию максимально возможных сценариев.

Но, даже учитывая "Классы эквивалентности", генерация тестовых данных сейчас стала отдельной задачей для таких сфер, где работа идет непосредственно с данными, их обработкой и/или трансформацией. Вариаций таких данных огромное количество, а еще необходимо тестировать не только конечный результат, но и промежуточные значения.

Функциональность идеального сервиса генерации данных

Идеальный сервис по генерации тестовых данных должен иметь возможность:

  • генерировать данные в разных форматах (JSON, XML, CSV и т.д.)

  • генерировать данные с зависимостями (parent, child)

  • генерировать сложные зависиммые данные (if a then 1 or 2 else 3 or 5)

  • генерировать большие обьемы данный за небольшой промежуток времени

Хотелось бы иметь возможность:

  • загрузки данные в прямо в БД

  • интерграции в CI/CD

  • создавать модель данных автоматом из схем

GenRocket университет

Сервис GenRocket предоставяет тренинг-университет, пройдя который вы ознакомитесь с основным базовым функционалом, установкой и настройкой.

Изучить его я крайне рекомендую, первая половина - это чистая теория и немножно нудная, но вы узнаете все основные понятия, которыми оперирует сервис. Вторая же половина - это уже практические занятия с детализированным описание куда нажимать. Этот тернинг подойдет для любого уровня подготовки тестировщика, бизнес-аналитика или разработчика.

GenRocket сервис

GenRocket - это сервис для генерации данных, созданный в 2011 году Hycel Taylor и Garth Rose для решения проблемы создания реалистичных тестовых данных для любой модели данных. Сервис обладает функциональностью генерировать данные для автоматического тестирования, для тестирования нагрузки, тестирования безопасности и др.

Сервис состоит из двух частей: web часть и программная cli часть. В web части происходит создание сценария-инструкции для генерации данных, в программной cli части на самой машине происходит генерация данных.

Что бы начать работу с GenRocket пользователь должен быть авторизирован, затем что бы начать работу с GenRocket необходимо скачать архив для Runtime* для cli части, распаковать его и прописать в системных переменных путь к папке с GenRocket в переменную GEN_ROCKET_HOME и в переменно PATH прописать %GEN_ROCKET_HOME%\bin значение.

Затем открываем командную строку, набираем genrocket и видим картинку ниже.

GenRocket cli часть работает в двух режимах on-line и off-line, но для работы с off-line надо скачать сертификат, который будет валиден только 24 часа.

GenRocket домен и его атрибуты

Первые два из основных компонентов - это Домен и Атрибуты домена. Домен - это существительное, например, пользователь: адрес, кредитная карта и т.д. Каждый домен описывается атрибутами, например: имя, фамилия, e-mail, пароль и день рождения. На картинке ниже вы видите домент User (1), описанный атрибутами (2) и пример сгенерированных данных (3).

Атрибуты к домену могут добавляться вручную по одному (2), с помощью блокнота или импортируя DDL, CSV, JSON или другие форматы. Если мы говорим о табличных данных, то можно сказать, что домен - это таблица, а атрибуты - это клонки этой таблицы.

GenRocket генераторы

Следующий компонент - генератор (generator) - это функциональность, которая непосредственно отвечает за генерацию данных в различных форматах. Генераторов в GenRocket 150+ для различных типов данных. Например:

Каждому атрибуту домена GenRocket назначает свой генератор, опираясь на имя атрибута, автоматом. Например, для атрибута, который содержит слово Name, будет подобран генератор NameGen, а для атрибута с SSN будет подобран генератор SSNGen.

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

GenRocket получатель (receiver)

Следующий компонент - получатель (receiver) - это функциональность, которая отвечает за выгрузку данных в необходимом формате: XML, JSON, SQL, CSV, JDBC, REST, SOAP. В GenRocket 35+ подобных получателелей.

Так же получателем может быть база данных, для которой необходимо настроить JDBC соединение. Настройки для этого соединения настраивается в специальных properties файлах.

GenRocket сценарий (scenarios)

Следующий компонент - сценарий (scenarios) - это набор инструкций, которые определяют сколько и в каком порядке будут созданы данные. Сценарии бывают одиночные (2) и сценарии-цепочки (1), которые позволяют генерировать данные для несколько связанных доменов одновременно. За количество данных отвечает переменная loopCount в настройках домена. Причем у каждого домена значение этой переменной устаналивается отдельно, что позволяет генерировать разное количество данных для каждого домена в сценариях-цепочках.

Сценарий выгружается в виде grs файла (3) и должен быть исполнен на машине, где был установлен GenRocket. Открываем командную строку и выполняем сценарий при помощи команды genrocket -r UserInsertScenario.grs.

При выполнении сценария видим результат, в котором отображатеся время генерации данных. На изображении ниже 10 тыс записей были вставлены в таблицу за 26 сек:

Применение GenRocket на реальном проекте

Возьмем небольшую схему данных, в которой есть таблицы user, grantHistory и notificationSetting.

Используя импорт DDL создадим домен для user.

create table `user` ( id int(10) not null auto_increment,  external_id varchar(50) not null unique,  first_name varchar(25) not null,  last_name varchar(25) not null,  middle_initial char(1),  username varchar(100) not null,  ssn varchar(15) not null,  password varchar(255) not null,  activation_date date,  primary key (id));

После создания доменов GenRocket подбирает подходящие генераторы для каждого атрибута. При необходимости настраиваем специфичные генераторы или модифицируем существующие. Например, изменяем generationType на random и сохраняем изменения.

Аналогичные действия проделываем для grant_history и notification_setting. Сгенерированные данные будут сохраняться в базу данных, для которой настроено JBDC соединение.

driver=org.h2.Driveruser=sapassword=saurl=jdbc:h2:file:~/lms_course/lms_alpha;AUTO_SERVER=TRUE;batchCount=1000

И так же для этой базы настраивается специфичные получатели H2InsertV2Receiver для вставки и SQLUpdateV2Receiver для модификации.

После всех манипуляций с настройками получаем файлы сценарии InsertScenarioChain.grs для вставки и UpdateScenarioChain.grs для модификации, после выполнения которых получаем картинку ниже.

И вуаля, данные в таблицах:

Заключение

Проведя иследование существующих на рынке предложений по сервисам генерации данных, могу сказать, что можно встретить два варианта сервисов:

  • бесплатный сервис, ограниченный в количестве генерируемых данных для несложных моделей данных

  • платный сервис с неограниченным количеством генерируемых данных и подходящие для сложных моделей данных, который может предоставляет ограниченный бесплатный функционал

Если у вас небольшой короткий проект с несложными данными, то данные для него можно получить при помощи бесплатных сервисов или при помощи самописного генератора. Но если проект долгосрочный с постоянно расширяющейся моделью данных, но идея покупки платного сервиса становится все привлекательней и привлекательней, но конечно решать вам.

Ниже расценка на доступ для GenRocket

Для сравнения я приготовила несколько ссылок бесплатных сервисов:

https://www.datprof.com/solutions/test-data-generation/ - только 14 дней бесплатного использования, похоже что цена договорная

http://generatedata.com/ - бесплатно, но возможна генерация только 100 записей

https://www.mockaroo.com/ - бесплатна возможна генерация только 1000 записей, остальное платно - для самого дорогого доступа $5000/year

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru