Skip to content

Commit c15fa3d

Browse files
author
Leonid Moguchev
committed
sql, sqlx
1 parent 6a8c11d commit c15fa3d

File tree

8 files changed

+634
-74
lines changed

8 files changed

+634
-74
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ up-db:
55

66
.PHONY: down-db
77
down-db:
8-
docker-compose down postgres
8+
docker-compose down

database-sql/main.go

+387
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"database/sql" // https://go.dev/src/database/sql/doc.txt
6+
"database/sql/driver"
7+
"encoding/json"
8+
"errors"
9+
"fmt"
10+
"log"
11+
"time"
12+
13+
// database/sql — это набор интерфейсов для работы с базой
14+
// Чтобы эти интерфейсы работали, для них нужна реализация. Именно за реализацию и отвечают драйверы.
15+
_ "github.com/lib/pq" // импортируем драйвер для postgres
16+
// Обратите внимание, что мы загружаем драйвер анонимно, присвоив его квалификатору пакета псевдоним, _ ,
17+
// чтобы ни одно из его экспортированных имен не было видно нашему коду.
18+
// Под капотом драйвер регистрирует себя как доступный для пакета database/sql
19+
)
20+
21+
const (
22+
23+
// название регистрируемоего драйвера github.com/lib/pq
24+
stdPostgresDriverName = "postgres"
25+
/*
26+
PostgreSQL:
27+
* github.com/lib/pq -> postgres
28+
* github.com/jackc/pgx -> pgx
29+
MySQL:
30+
* github.com/go-sql-driver/mysql -> mysql
31+
SQLite3:
32+
* github.com/mattn/go-sqlite3 -> sqlite3
33+
Oracle:
34+
* github.com/godror/godror -> godror
35+
MS SQL:
36+
* github.com/denisenkom/go-mssqldb -> sqlserver
37+
38+
See more drivers: https://zchee.github.io/golang-wiki/SQLDrivers/
39+
*/
40+
)
41+
42+
const (
43+
host = "localhost"
44+
port = 5432
45+
user = "user"
46+
password = "password"
47+
dbname = "playground"
48+
)
49+
50+
func main() {
51+
52+
// connection string
53+
psqlConn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", host, port, user, password, dbname)
54+
55+
// open database
56+
db, err := sql.Open(stdPostgresDriverName, psqlConn) // returns *sql.DB, error
57+
if err != nil {
58+
log.Fatal(err)
59+
}
60+
defer db.Close() // Обязательно при завершении работы приложения мы должны освободить все ресурсы, иначе соединения к базе останутся висеть.
61+
62+
/*
63+
sql.DB - не является соединением с базой данных! Это абстракция интерфейса.
64+
65+
sql.DB выполняет некоторые важные задачи для вас за кулисами:
66+
* открывает и закрывает соединения с фактической базовой базой данных через драйвер.
67+
* управляет пулом соединений по мере необходимости.
68+
69+
Абстракция sql.DB предназначена для того, чтобы вы не беспокоились о том, как управлять одновременным
70+
доступом к базовому хранилищу данных. Соединение помечается как используемое, когда вы используете
71+
его для выполнения задачи, а затем возвращается в доступный пул, когда оно больше не используется.
72+
*/
73+
74+
// После установления соединеия пингуем базу. Проверяем, что она отвечает нашему приложению.
75+
if err := db.Ping(); err != nil {
76+
log.Fatal(err)
77+
}
78+
79+
fmt.Println("Connection with database successfully established!")
80+
81+
/* Настройка пула соединений */
82+
db.SetConnMaxIdleTime(time.Minute) // время, в течение которого соединение может быть бездействующим.
83+
db.SetConnMaxLifetime(time.Hour) // время, в течение которого соединение может быть повторно использовано.
84+
db.SetMaxIdleConns(2) // максимум 2 простаивающих соединения
85+
db.SetMaxOpenConns(4) // максимум 4 открытых соединений с БД
86+
87+
/* статистика пула соединений */
88+
statistics := db.Stats()
89+
bytes, err := json.Marshal(statistics)
90+
if err != nil {
91+
log.Fatal(err)
92+
}
93+
fmt.Printf("db connection statistics: %s\n", string(bytes))
94+
95+
/* примеры работы c БД */
96+
exampleQueryRow(db)
97+
exampleQuery(db)
98+
exampleExec(db)
99+
100+
/* примеры работы c БД c контекстом */
101+
// Совет: используйте запросы с контекстом
102+
ctx := context.Background()
103+
exampleQueryRowContext(ctx, db)
104+
exampleQueryContext(ctx, db)
105+
exampleExecContext(ctx, db)
106+
107+
/* примеры работы c транзакциями */
108+
exampleTransaction(ctx, db)
109+
110+
/* пример работы с nullable полями*/
111+
exampleWithNullableFields(ctx, db)
112+
113+
// Более подробный туториал (правда там с MySQL, но суть та же)
114+
// Go database/sql tutorial: http://go-database-sql.org/
115+
}
116+
117+
func exampleQueryRow(db *sql.DB) {
118+
// Ex. 1
119+
row := db.QueryRow("SELECT count(*) FROM students")
120+
121+
var totalStudents uint
122+
if err := row.Scan(&totalStudents); err != nil { // Обязательно передаем адрес переменной, куда будем сканировать значение.
123+
log.Fatal(err)
124+
}
125+
126+
fmt.Printf("total students: %d\n", totalStudents)
127+
128+
// Ex. 2
129+
var studentID int64
130+
row = db.QueryRow("SELECT id FROM students WHERE age > 10000") // такого "долгожителя" в нашей таблице может не быть
131+
if err := row.Scan(&studentID); err != nil { // мы тут получим ошубку, так как нам ничего не вернулось из БД
132+
fmt.Println("db.QueryRow.Scan():", err) // нам вернется ошибка sql.ErrNoRows
133+
if errors.Is(err, sql.ErrNoRows) { // при использовании QueryRow не забывайте обрабатывать ошибку на sql.ErrNoRows, так как отстуствие результата может быть стандартным кейсом
134+
fmt.Println("Не найден в БД студент с age > 10000")
135+
}
136+
}
137+
}
138+
139+
func exampleQuery(db *sql.DB) {
140+
const minAge = 18
141+
142+
rows, err := db.Query("SELECT first_name, last_name, age FROM students WHERE age >= $1", minAge)
143+
if err != nil {
144+
log.Fatal(err)
145+
}
146+
defer rows.Close() // Обязательно закрываем иначе соединение с БД повиснет
147+
148+
type Student struct {
149+
FirstName string
150+
LastName string
151+
Age uint
152+
}
153+
154+
var students []Student
155+
for rows.Next() {
156+
var st Student
157+
if err := rows.Scan(&st.FirstName, &st.LastName, &st.Age); err != nil {
158+
log.Fatal(err)
159+
}
160+
students = append(students, st)
161+
}
162+
163+
if err = rows.Err(); err != nil {
164+
// handle the error here
165+
log.Fatal(err)
166+
}
167+
168+
fmt.Printf("students: %v\n", students)
169+
}
170+
171+
func exampleExec(db *sql.DB) {
172+
// Ex. 1:
173+
const notExistedStudentID = 1234567
174+
result, err := db.Exec("UPDATE students SET age = age+1 WHERE id = $1", notExistedStudentID)
175+
if err != nil {
176+
log.Fatal(err)
177+
}
178+
179+
var (
180+
rowsAffected, lastInsertId int64
181+
)
182+
183+
rowsAffected, err = result.RowsAffected()
184+
if err != nil {
185+
fmt.Println("sql.Result.RowsAffected():", err) // ok
186+
}
187+
lastInsertId, err = result.LastInsertId()
188+
if err != nil {
189+
fmt.Println("sql.Result.LastInsertId():", err) // LastInsertId is not supported by "postgres" driver
190+
}
191+
fmt.Printf("rows affected: %d, last insert id: %d\n", rowsAffected, lastInsertId)
192+
193+
// Ex. 2:
194+
const studentID = 1
195+
result, err = db.Exec("UPDATE students SET age = age+1 WHERE id = $1", studentID)
196+
if err != nil {
197+
log.Fatal(err)
198+
}
199+
200+
rowsAffected, err = result.RowsAffected()
201+
if err != nil {
202+
fmt.Println("sql.Result.RowsAffected():", err)
203+
}
204+
fmt.Printf("rows affected: %d, last insert id: %d\n", rowsAffected, lastInsertId)
205+
}
206+
207+
func exampleQueryRowContext(ctx context.Context, db *sql.DB) {
208+
row := db.QueryRowContext(ctx, "SELECT count(*) FROM students")
209+
210+
var totalStudents uint
211+
if err := row.Scan(&totalStudents); err != nil { // Обязательно передаем адрес переменной, куда будем сканировать значение.
212+
log.Fatal(err)
213+
}
214+
215+
fmt.Printf("total students: %d\n", totalStudents)
216+
}
217+
218+
func exampleQueryContext(ctx context.Context, db *sql.DB) {
219+
const minAge = 18
220+
221+
rows, err := db.QueryContext(ctx, "SELECT first_name, last_name, age FROM students WHERE age >= $1", minAge)
222+
if err != nil {
223+
log.Fatal(err)
224+
}
225+
defer rows.Close() // Обязательно закрываем иначе соединение с БД повиснет
226+
227+
type Student struct {
228+
FirstName string
229+
LastName string
230+
Age uint
231+
}
232+
233+
var students []Student
234+
for rows.Next() {
235+
var st Student
236+
if err := rows.Scan(&st.FirstName, &st.LastName, &st.Age); err != nil {
237+
log.Fatal(err)
238+
}
239+
students = append(students, st)
240+
}
241+
// Внутри драйвера мы получаем данные, накапливая их в буфер размером 4KB.
242+
// rows.Next() порождает поход в сеть и наполняет буфер. Если буфера не хватает,
243+
// то мы идём в сеть за оставшимися данными. Больше походов в сеть – меньше скорость обработки.
244+
245+
fmt.Printf("students: %v\n", students)
246+
}
247+
248+
func exampleExecContext(ctx context.Context, db *sql.DB) {
249+
const studentID = 1
250+
result, err := db.ExecContext(ctx, "UPDATE students SET age = age+1 WHERE id = $1", studentID)
251+
if err != nil {
252+
log.Fatal(err)
253+
}
254+
255+
rowsAffected, err := result.RowsAffected()
256+
if err != nil {
257+
fmt.Println("sql.Result.RowsAffected():", err)
258+
}
259+
fmt.Printf("rows affected: %d\n", rowsAffected)
260+
}
261+
262+
func exampleTransaction(ctx context.Context, db *sql.DB) {
263+
// создаем транзакцию
264+
tx, err := db.BeginTx(ctx, &sql.TxOptions{
265+
Isolation: sql.LevelRepeatableRead, // указываем в опциях уровень изоляции
266+
ReadOnly: false, // можем указать, что транзакции только для чтения
267+
})
268+
if err != nil {
269+
log.Fatal(err)
270+
}
271+
defer tx.Rollback() // если на любом из этапов произойдет ошибка, то мы откатим изменения при выходе из функции.
272+
// После вызова tx.Commit() вызов tx.Rollback() ничего уже не откатит, а просто вернет ошибку sql.ErrTxDone
273+
274+
rows, err := tx.QueryContext(ctx, "SELECT 1") // у sql.Tx все те же селекторы что и у sql.DB
275+
if err != nil {
276+
log.Fatal(err)
277+
}
278+
rows.Close()
279+
280+
_, err = tx.ExecContext(ctx, "SELECT 1")
281+
if err != nil {
282+
log.Fatal(err)
283+
}
284+
285+
if err = tx.Commit(); err != nil {
286+
log.Fatal(err)
287+
}
288+
289+
fmt.Println("transaction is commited")
290+
}
291+
292+
func exampleWithNullableFields(ctx context.Context, db *sql.DB) {
293+
var number int
294+
if err := db.QueryRowContext(ctx, "SELECT null").Scan(&number); err != nil {
295+
fmt.Println("error scan:", err) // вернет ошибку, так как нельзя NULL сложить в нессылочные типы
296+
}
297+
// Как быть?
298+
299+
// Вариант 1: COALESCE(field, <default_value>)
300+
if err := db.QueryRowContext(ctx, "SELECT COALESCE(null, -1) AS some_field").Scan(&number); err != nil {
301+
log.Fatal(err)
302+
}
303+
fmt.Println("number =", number) // number = -1
304+
// Преимущества:
305+
// - ничего не меням в коде
306+
// Недостатски:
307+
// - можно забыть в запросе использовать COALESCE, и тогда запросы будут валиться с ошибкой
308+
// - иногда нам важно отличать NULL от значения по умолчанию
309+
310+
// Вариант 2: давайте сделаем из int ссылочный тип - указатель
311+
var ptrNumber *int // теперь у нас не int, а указатель на int
312+
if err := db.QueryRowContext(ctx, "SELECT null").Scan(&ptrNumber); err != nil {
313+
log.Fatal(err)
314+
}
315+
fmt.Println("ptrNumber =", ptrNumber) // тут мы увидем, что numberCanStoreNul == nil
316+
if ptrNumber != nil { // делаем постоянную проверку на nil
317+
fmt.Println("value of ptrNumber =", *ptrNumber) // разыменовываем указатель
318+
}
319+
320+
// Преимущества:
321+
// - легко из обычного типа сделать указатель
322+
// Недостатски:
323+
// - везде в приложении нам надо делать проверки на nil и разыменовывать указатель
324+
// - Наша переменная будет теперь аллоцировать в куче, что создает накладки для работы приложения на Go
325+
326+
// Вариант 3: за нас уже позаботились и сделали специальные типы в пакете database/sql:
327+
/*
328+
sql.NullInt16
329+
sql.NullInt32
330+
sql.NullInt64
331+
sql.NullByte
332+
sql.NullBool
333+
sql.NullFloat64
334+
sql.NullString
335+
sql.NullTime
336+
*/
337+
338+
var sqlNullNumber sql.NullInt32
339+
if err := db.QueryRowContext(ctx, "SELECT null").Scan(&sqlNullNumber); err != nil {
340+
log.Fatal(err)
341+
}
342+
fmt.Println("sqlNullNumber =", sqlNullNumber) // sqlNullNumber это структура
343+
if sqlNullNumber.Valid { // поле Valid = true сообщает нам, что поле не Null и можно брать занчение
344+
fmt.Println("value of ptrNumber =", sqlNullNumber.Int32) // получаем значение
345+
}
346+
// Преимущества:
347+
// - выделяется на стеке переменная
348+
// - все так же имеем возможность отличить NULL от 0
349+
// Недостатки:
350+
// - А что если мы хоти такое поведение для нашего кастомного типа? Нам не хватает стандартного набора.
351+
352+
// Нам необходмо, чтобы наш тип удовлетоварял sql.Scanner интерфейсу
353+
}
354+
355+
type MyCustomType struct {
356+
Valid bool
357+
Number int
358+
}
359+
360+
// Scan implements the Scanner interface.
361+
func (n *MyCustomType) Scan(value interface{}) error {
362+
if value == nil {
363+
n.Number, n.Valid = 0, false
364+
return nil
365+
}
366+
n.Valid = true
367+
368+
// some fantastic logic here
369+
fmt.Printf("%#v\n", value)
370+
371+
return nil
372+
373+
}
374+
375+
var _ sql.Scanner = (*MyCustomType)(nil) // наш тип MyCustomType удовлетовряет интерфейсу sql.Scanner
376+
377+
// Если мы хоти наш тип как-то мапить в null/valu, то реализуем Value()
378+
379+
// Value implements the driver Valuer interface.
380+
func (n MyCustomType) Value() (driver.Value, error) {
381+
if !n.Valid {
382+
return nil, nil
383+
}
384+
return int64(n.Number), nil
385+
}
386+
387+
var _ driver.Valuer = (*MyCustomType)(nil) // наш тип MyCustomType удовлетовряет интерфейсу driver.Valuer

0 commit comments

Comments
 (0)