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. Тернистый карьерный путь - Кодер - Программист - Архитектор -

    Продуктовый менеджер - Директор продукта - CTO - Мама! Я стартапер!
  2. О компании «АБИЭС эскос» Умное управление уличным освещением - Стартап

    - Разрабатываем облачное ПО на Go+PostgreSQL+ReactJS - Разрабатываем ПО контроллеров группы светильников на Go - Разрабатываем собственную MESH радио сеть для передачи данных в диапазоне 868 MHz - Делаем дизайн и производим собственное железо в Китае - Зарабатываем на аутсорсе, всё заработанное вкладываем в стартап Lighting Management System
  3. Выбор языка программирования Java, Питон 2.х-3.х, D, Ruby, Node, .NET,

    Go - Компилируемый и межплатформенный x86, AMD64, ARM - Windows и Linux - Статическая типизация - Автоматическое управление памятью - Богатая стандартная библиотека - Простой язык - Принудительная обработка ошибок - Быстрая компиляция - Встроенный тулинг - Статические зависимости Отсутствие дженериков Отсутствие дженериков
  4. 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
  5. 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: /
  6. И понеслось - Монолит или микросервисы? Ложиться под полноценный фреймворк

    или создавать свое? Beego или go-kit? - Как организовать AAA - Какой выбрать router? - Что передавать в контекстах запросов - Где начинаются и заканчиваются принципы 12 fapp - Какой логгер выбрать? - Стандартного типа error не достаточно - Вебсокеты - Graceful shutdown - Generics - И куча других вопросов
  7. 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()
  8. 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) } }
  9. 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 { // обработка ошибки которая возникла в результате итерации по записям }
  10. Библиотека - огонь! - Есть все! - Отличная документация -

    Если чего то нет, то может вам это не надо  - Одна строчка заменяет «портянку» при использовании прямого database/sql! - БЕРЕМ!
  11. 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()
  12. Кажется мы нашли счастье! - Лаконично - Расширенные тэги для

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

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

    смотрим на гитхаб звезды - Рыщем в интернетах как голодные …
  15. Да их здесь сотни! - Классические ORM: gorp, beego orm,

    go-pg, xorm, qbs*, hood*, dbr, buffalo pop** - ORM с кодогенерацией : reform, kallax - Микро ORM: sqlx, dat*, genmai*, scaneo* - REST2SQL конвертер: prest
  16. Пишем тесты бенчмарков на SELECT - gorm - database/sql -

    sqlx - reform - dbr - В таблице ~160 тысяч записей - Выбираем два полня ID, NAME из таблицы с 20 полями
  17. 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
  18. $ 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
  19. Готовим требования и кальян - Database model first - Миграции

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

    - Отрисовываем все Foreign Keys! - Группируем таблицы по функциональным группам (микросервисам) для наглядности - Генерируем SQL для создания таблиц и других объектов БД - Автоматически заполняем собственную таблицу с метаданными - Генерируем описание типов данных Go из метаданных таблиц
  21. -- 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”` }
  22. Дорисовываем оставшуюся часть совы! - Обертка вокруг стандартного пакета database/sql

    - Говно и палки! - Сразу вкатываем в реальные проекты, что бы библиотека быстрее приобрела форму - Небольшие неудобства в использовании без ущерба производительности допустимо!
  23. 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);
  24. 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() //}
  25. $ 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
  26. Присоединяйтесь, будет интересно - Ищем единомышленников - Палок уже меньше,

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