Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Личная боль при работе с БД в Go

Личная боль при работе с БД в Go

Iskander (Alex) Sharipov

December 02, 2018
Tweet

More Decks by Iskander (Alex) Sharipov

Other Decks in Technology

Transcript

  1. Robert Egorov
    Личная боль при
    работе с БД в Go

    View Slide

  2. Тернистый карьерный путь
    - Кодер
    - Программист
    - Архитектор
    - Продуктовый менеджер
    - Директор продукта
    - CTO
    - Мама! Я стартапер!

    View Slide

  3. О компании «АБИЭС эскос»
    Умное управление уличным освещением
    - Стартап
    - Разрабатываем облачное ПО на
    Go+PostgreSQL+ReactJS
    - Разрабатываем ПО контроллеров группы
    светильников на Go
    - Разрабатываем собственную MESH радио сеть для
    передачи данных в диапазоне 868 MHz
    - Делаем дизайн и производим собственное железо
    в Китае
    - Зарабатываем на аутсорсе, всё заработанное
    вкладываем в стартап
    Lighting
    Management
    System

    View Slide

  4. Выбор языка
    программирования
    Java, Питон 2.х-3.х, D, Ruby, Node, .NET, Go
    - Компилируемый и межплатформенный x86, AMD64, ARM
    - Windows и Linux
    - Статическая типизация
    - Автоматическое управление памятью
    - Богатая стандартная библиотека
    - Простой язык
    - Принудительная обработка ошибок
    - Быстрая компиляция
    - Встроенный тулинг
    - Статические зависимости
    Отсутствие дженериков
    Отсутствие дженериков

    View Slide

  5. package main
    import “fmt”
    func main() {
    fmt.Println("hello world")
    }
    $ go run hello-world.go
    hello world
    $ go build hello-world.go
    $ ls
    hello-world hello-world.go
    $ ./hello-world
    hello world

    View Slide

  6. package main
    import (
    "fmt"
    "net/http"
    )
    func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path)
    })
    http.ListenAndServe(":8081", nil)
    }
    $curl http://localhost:8081/
    Hello, you've requested: /

    View Slide

  7. И понеслось
    - Монолит или микросервисы? Ложиться под
    полноценный фреймворк или создавать свое?
    Beego или go-kit?
    - Как организовать AAA
    - Какой выбрать router?
    - Что передавать в контекстах запросов
    - Где начинаются и заканчиваются принципы 12 fapp
    - Какой логгер выбрать?
    - Стандартного типа error не достаточно
    - Вебсокеты
    - Graceful shutdown
    - Generics
    - И куча других вопросов

    View Slide

  8. А работа с БД?
    - Есть пакет database/sql в стандартной библиотеке!

    View Slide

  9. import (
    "database/sql"
    “github.com/lib/pq"
    )
    //...
    db, err := sql.Open("postgres", "user=robert password=12345678 dbname=pg sslmode=verify-full")
    if err != nil {
    panic(err)
    }

    .
    defer db.Close()

    View Slide

  10. type Person struct {
    ID int
    Name string
    LastName string
    }
    var person Person
    sqlStatement := `SELECT id, name, last_name FROM persons WHERE id=$1`
    personID := 1
    row := db.QueryRow(sqlStatement, personID)
    err := row.Scan(&peron.ID, &person.Name, &person.LastName)
    if err != nil {
    if err == sql.ErrNoRows {
    fmt.Println("Zero rows found")
    } else {
    panic(err)
    }
    }

    View Slide

  11. type Person struct {
    ID int
    Name string
    LastName string
    }
    var (
    person Person
    arr []Person
    )
    rows, err := db.Query("SELECT id, name, last_name FROM persons")
    defer rows.Close()
    for rows.Next() {
    err = rows.Scan(&person.ID, &person.Name, &person.LastName)
    if err != nil {
    // обработка ошибки при чтении данных в переменные
    }
    arr = append(arr, row)
    }
    err = rows.Err()
    if err != nil {
    // обработка ошибки которая возникла в результате итерации по записям
    }

    View Slide

  12. Многовато писанины?

    View Slide

  13. Ищем ORM

    View Slide

  14. УРА! 11 тысяч разрабов не могут
    ошибаться!

    View Slide

  15. Библиотека - огонь!
    - Есть все!
    - Отличная документация
    - Если чего то нет, то может вам это не надо 
    - Одна строчка заменяет «портянку» при
    использовании прямого database/sql!
    - БЕРЕМ!

    View Slide

  16. type Person struct {
    ID int
    Name string
    LastName string
    }
    var (
    person Person
    arr []Person
    )
    db, err := gorm.Open("postgres", "user=robert password=12345678 dbname=pg")
    personID := 1
    db.Where(“id = $1", personID).First(&person)
    db.Find(&arr)
    defer db.Close()

    View Slide

  17. Кажется мы нашли счастье!
    - Лаконично
    - Расширенные тэги для обозначения значений по
    умолчанию
    - Поддержка миграций
    - JOIN-ы
    - Хуки перед CRUD операциями
    - Поддержка особенностей PG. Например ON
    CONFLICT при INSERT
    - Chaining: db.Where("name LIKE $1", “Rob
    %").Find(&persons, "id IN ($2)", []int{1, 2,
    3}).Count(&count)

    View Slide

  18. Отрезвление
    - Если нужно считать все поля таблицы кроме
    одного
    - Если нужно изменить только некоторые поля
    таблицы
    - Много разных способов добиться одинакового
    результата
    - Сложные SQL запросы сначала пишутся и
    отлаживаются в SQL Manager, потом приходится
    разбивать на цепочки функций gorm
    - Админы жалуются на «мусорность» генерируемых
    SQL запросов
    - Нагрузка на Garbage collector

    View Slide

  19. GORM – огонь? Да. Сжечь!
    - Ищем альтернативы
    - Не смотрим на гитхаб звезды
    - Рыщем в интернетах как голодные …

    View Slide

  20. Да их здесь сотни!
    - Классические ORM: gorp, beego orm, go-pg, xorm,
    qbs*, hood*, dbr, buffalo pop**
    - ORM с кодогенерацией : reform, kallax
    - Микро ORM: sqlx, dat*, genmai*, scaneo*
    - REST2SQL конвертер: prest

    View Slide

  21. Пишем тесты бенчмарков на SELECT
    - gorm
    - database/sql
    - sqlx
    - reform
    - dbr
    - В таблице ~160 тысяч записей
    - Выбираем два полня ID, NAME из таблицы с 20 полями

    View Slide

  22. func BenchmarkGormSelectAll(b *testing.B) {
    db, err := gorm.Open("postgres", "host=127.0.0.1 port=5432 dbname=etsoft sslmode='disable’)
    if err != nil {
    b.Fatal(err)
    }
    cnt := 0
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
    var arr []Patient
    err := db.Model(&Patient{}).Find(&arr).Error
    if err != nil {
    b.Fatal(err)
    }
    cnt += len(arr)
    }
    b.StopTimer()
    }
    $ go test -bench . --benchmem

    View Slide

  23. $ go test -bench . –benchmem
    BenchmarkGormSelectAll-8 1 1306587788 ns/op 430774992 B/op 10007 590 allocs/op
    BenchmarkSQLSelectAll-8 10 124044727 ns/op 26426735 B/op 754 311 allocs/op
    BenchmarkSqlXSelectAll-8 10 172360602 ns/op 38596978 B/op 1056 045 allocs/op
    BenchmarkReformSelectAll-8 10 141932390 ns/op 32363486 B/op 1056 070 allocs/op
    BenchmarkDBRSelectAll-8 5 260183127 ns/op 38598811 B/op 1056 075 allocs/op

    View Slide

  24. Готовим требования и кальян
    - Database model first
    - Миграции не нужны
    - Не писать SQL для типовых CRUD операций
    - Минимизировать нагрузку на GC
    - Уметь возможность выборочно игнорировать поля в
    разных запросах без создания промежуточных
    структур
    - Регистрировать время получения первой записи от
    СУБД и длительность чтения всех записей
    - Для CRU* операций отправлять в базу данных
    запросы содержащие только требуемые поля
    - В журнале помечать запросы выполняющиеся в
    рамках одной транзакции

    View Slide

  25. Принцип Database Model First
    - Описываем базу данных в
    дизайнере
    - Отрисовываем все Foreign
    Keys!
    - Группируем таблицы по
    функциональным группам
    (микросервисам) для
    наглядности
    - Генерируем SQL для создания
    таблиц и других объектов БД
    - Автоматически заполняем
    собственную таблицу с
    метаданными
    - Генерируем описание типов
    данных Go из метаданных
    таблиц

    View Slide

  26. Тэги
    Как способ внесения управляемой декларативности и магии

    View Slide

  27. -- create_objects.sql
    CREATE TABLE persons (
    id int8 not null,
    name varchar(32),
    last_name text not null,
    blob bytea,
    CONSTRAINT persons_pk PRIMARY KEY (id)
    comment on column persons.name is ‘Имя. Правила: maxlen:32, trim';
    // domain.go
    type Person struct {
    ID int `json:”id”`
    Name string `json:”name” dbw:”maxlen:32,trim”`
    LastName string `json:”last_name”`
    Blob []byte `json:”blob” dbw:”nocache,blob”`
    }
    ..
    .
    type PersonBalance struct {
    ID int `json:”id” dbw:”noseq”` // не нашли sequence который имеет имя person_balances_seq
    Balance int `json:”balance”`
    }
    type PersonWallet struct {
    ID string `json:”id”`
    Name string `json:”name” dbw:”maxlen:20”`
    Reserved int `json:”reserved” dbw:”noins”`
    Available int `json:”available” dbw:”min:0”`
    }

    View Slide

  28. Дорисовываем оставшуюся часть совы!
    - Обертка вокруг стандартного пакета database/sql
    - Говно и палки!
    - Сразу вкатываем в реальные проекты, что бы библиотека
    быстрее приобрела форму
    - Небольшие неудобства в использовании без ущерба
    производительности допустимо!

    View Slide

  29. type Person struct {
    ID int `json:”id”`
    Name string `json:”name”`
    LastName string `json:”last_name”`
    Blob []byte `json:”blob” dbw:”nocache,blob”`
    }
    var (
    row Patient
    arr []Patient
    )
    tbl := table.NewSimple(db, "patients", &Patient{})
    // чтение всех полей таблицы
    err := tbl.DoSelect (func() error { arr = append(arr, row); return nil }, &row);
    // обработка ошибки
    // чтение всех полей, кроме полей имеющих тэг “blob”
    err := tbl.DoSelectEx (“blob”, dbw.Exclude, func() error { arr = append(arr, row); return nil }, &row);
    // чтение только полей имеющих тэг blob
    err := tbl.DoSelectEx (“blob”, dbw.Include, func() error { arr = append(arr, row); return nil }, &row);
    // чтени полей, которые имеют тэг “blob” и обработка в процессе выборки записей
    err := tbl.DoSelectEx (“blob”, dbw.Include, func() error {
    // делаем что то, с полем row.Blob без сохранения
    return nil }, &row);

    View Slide

  30. func BenchmarkDBWSelectAll(b *testing.B) {
    db, err := dbw.Open("postgres", "host=127.0.0.1 port=5432 dbname=etsoft sslmode='disable')
    if err != nil {
    b.Fatal(err)
    }
    cnt := 0
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
    var (
    row Patient
    arr []Patient
    )
    tbl := table.NewSimple(db, "patients", &Patient{})
    if err := tbl.DoSelect(func() error {
    arr = append(arr, row);
    return nil },
    &row); err != nil {
    b.Fatal(err)
    }
    cnt += len(arr)
    }
    b.StopTimer()
    }
    //func (t *Table) DoSelect(f func() error, row interface{}) error {
    // cols := t.fieldAddrsSelect(row, dbw.TagNoCache, dbw.Exclude)
    // return t.db.QueryContext(t.ctx, t.SQL.SelectCache).Fetch(f, cols...).Err()
    //}

    View Slide

  31. $ go test -bench . –benchmem
    BenchmarkGormSelectAll-8 1 1306587788 ns/op 430774992 B/op 10007 590 allocs/op
    BenchmarkSQLSelectAll-8 10 124044727 ns/op 26426735 B/op 754 311 allocs/op
    BenchmarkSqlXSelectAll-8 10 172360602 ns/op 38596978 B/op 1056 045 allocs/op
    BenchmarkReformSelectAll-8 10 141932390 ns/op 32363486 B/op 1056 070 allocs/op
    BenchmarkDBRSelectAll-8 5 260183127 ns/op 38598811 B/op 1056 075 allocs/op
    BenchmarkDBWSelectAll-8 10 122501868 ns/op 25435496 B/op 603 512 allocs/op

    View Slide

  32. Присоединяйтесь, будет интересно
    - Ищем единомышленников
    - Палок уже меньше, другое же еще остается
    - Будем отдавать в open source
    - Студенты в поисках open source проекта?
    - Испытали похожие боли при выборе ORM?

    View Slide

  33. Егоров Роберт, ABIES escos
    Twitter: @robert_egorov
    Telegram: @regorov
    Спасибо

    View Slide