videos. Теперь вы начнёте работать с этой базой видеороликов на Go. Но для этого нужно обучиться языку SQL-запросов. Их условно делят на две группы:SELECT-запросы;CREATE, UPDATE, DROP, DELETE и так далее.SELECT-запросы (англ. select — выбирать). Вы научитесь отправлять запросы и обрабатывать ответы на Go. Без этих знаний невозможно работать с базами данных.SELECT-запрос возвращает данные из таблицы в соответствии с указанными в нём параметрами. Рассмотрим базовый синтаксис SELECT-запросов. Простейший запрос для получения всех данных из таблицы выглядит так:-- получаем все записи со всеми полями из таблицы videos
SELECT * FROM videos videos. В таком случае указываем только нужные поля:-- все записи со значениями полей video_id,title,views из таблицы videos
SELECT video_id,title,views FROM videos WHERE с требуемыми условиями. Вы можете использовать:=, <, >, <=, >=, !=;AND и OR;IN и NOT IN для фильтрации по нескольким значениям;BETWEEN ... AND для указания диапазона.-- записи с количеством просмотров от 1000 до 2000
SELECT * FROM videos WHERE views >= 1000 AND views <= 2000
-- эквивалент
SELECT * FROM videos WHERE views BETWEEN 1000 AND 2000
-- записи с количеством лайков - 0, 5 или 10
SELECT title, likes FROM videos WHERE likes IN (0,5,10) GROUP BY и агрегирующие функции:COUNT() — количество записей;SUM() — сумма числовых значений;MAX(), MIN() — максимальное и минимальное значение;AVG() — среднее значение;GROUP BY нужна фильтрация по агрегированным данным, то вместо WHERE используется HAVING.-- группируем записи по лайкам от 0 до 9 и получаем для каждой группы количество
-- записей и среднее количество просмотров в группе
SELECT COUNT(*) AS count_likes, AVG(views) AS avg_views, likes
FROM videos GROUP BY likes HAVING likes < 10 AS определяет алиас для столбца данных. В примере выше у колонок будут имена count_likes и avg_views, а не COUNT(*) и AVG(views).ORDER BY column. Она сортирует возвращаемые записи по возрастанию значений указанного столбца. Если требуется сортировка по убыванию, то после имени поля нужно добавить слово DESC.-- записи с просмотрами меньше 1000 отсортированы по возрастанию
SELECT * FROM videos WHERE views < 1000 ORDER BY views
-- записи отсортированы по убыванию лайков
SELECT * FROM videos ORDER BY likes DESC LIMIT:-- первые десять роликов с самым большим количеством просмотров
SELECT * FROM videos ORDER BY views DESC LIMIT 10 database/sql. Для отправки запросов в нём есть три основных метода:(db *DB) ExecContext(ctx context.Context, query string, args ...any) (Result, error)
(db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
(db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *Row Exec(...), Query(...) и QueryRow(...). Какие методы выбрать, решает сам разработчик. Но в код лучше сразу заложить использование контекста.Query, используются для отправки SELECT запросов:QueryContext() и Query() возвращают все полученные записи;QueryRowContext() и QueryRow() возвращают только одну запись.ExecContext() и Exec() используются для запросов без возвращения данных: INSERT, DELETE, UPDATE и так далее. Как с ними работать, подробно расскажем в следующем уроке. Query() или Exec() под капотом используют context.Background(). Если вы пользуетесь инструментарием context.Context, нужно применять методы QueryContext() и ExecContext(), позволяющие установить контекст явно.Query() функции возвращают значения типа *Rows или *Row. Чтобы перевести полученные данные в Go-типы, нужно использовать методы Scan(). Они последовательно копируют колонки текущей записи в переменные или области, на которые указывают передаваемые параметры.func (r *Row) Scan(dest ...any) error
func (rs *Rows) Scan(dest ...any) error package main
import (
"context"
"database/sql"
"fmt"
_ "modernc.org/sqlite"
)
func main() {
db, err := sql.Open("sqlite", "video.db")
if err != nil {
panic(err)
}
defer db.Close()
// делаем запрос
row := db.QueryRowContext(context.Background(),
"SELECT COUNT(*) as count FROM videos")
// готовим переменную для чтения результата
var id int64
err = row.Scan(&id) // разбираем результат
if err != nil {
panic(err)
}
fmt.Println(id)
} QueryContext(), а QueryRowContext(). Это позволит сократить объём кода.Scan(), который записывает результат в переменную. В случае нескольких значений указываем их последовательно. Так же, как выводим в SQL.context.Background(). Контекст позволяет ограничить по времени или прервать слишком долгие или уже не нужные операции с базой данных. Назначить для них дедлайн или тайм-аут. Вот так выглядит установка трёхсекундного тайм-аута, после которого затянувшаяся операция с БД будет прервана:ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
// не забываем освободить ресурс
defer cancel()
// обращаемся к БД
row := db.QueryRowContext(ctx, "SELECT COUNT(*) as count FROM videos") http.Handler) HTTP-сервера, то наследуйте контекст HTTP-запроса:func MyHandler(w http.ResponseWriter, r *http.Request) {
// наследуем контекст запроса r *http.Request, оснащая его Timeout
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
// делаем обращение к db в рамках полученного контекста
row := db.QueryRowContext(ctx, "SELECT COUNT(*) as count FROM videos")
// ...
} Scan(). Все значения, полученные в результате SQL-запроса в *sql.Rows и *sql.Row, имеют тип interface{}. Работать с этим типом не совсем удобно, поэтому и используется метод Scan(). Он преобразует значения в тот тип переменных, который нужен для дальнейших действий.Scan() поддерживает: int64, float64, bool, []byte, string, time.Time, nil.database/sql, например *sql.NullString.sql.Scanner и driver.Valuer (пакет database/driver).videos есть колонка comments_disabled типа BOOLEAN со значениями TRUE или FALSE. Но это могла бы быть и колонка типа TEXT со значениями: true, T, TRUE, 1 или 0, false, F. Если в Scan() для этого поля передать ссылку на строку, то после, чтобы получить логическое значение, нужно использовать strconv.ParseBool(). Если же сразу передать ссылку на переменную типа bool, Scan() сам сделает преобразования и запишет логическое значение по указанному адресу или вернёт ошибку.videos. Попробуйте запустить эту программу и посмотрите на результат.package main
import (
"context"
"database/sql"
"fmt"
_ "modernc.org/sqlite"
)
func main() {
db, err := sql.Open("sqlite", "video.db")
if err != nil {
panic(err)
}
defer db.Close()
row := db.QueryRowContext(context.Background(),
"SELECT title, likes, comments_disabled "+
"FROM videos ORDER BY likes DESC LIMIT 1")
var (
title string
likes int
comdis bool
)
// порядок переменных должен соответствовать порядку колонок в запросе
err = row.Scan(&title, &likes, &comdis)
if err != nil {
panic(err)
}
fmt.Printf("%s | %d | %t \r\n", title, likes, comdis)
} fmt.Sprintf или объединение строк. // если в id окажется "ooops; DROP TABLE video",
// то вам придётся создавать таблицу video заново
query := fmt.Sprintf(`SELECT title FROM video WHERE video_id = %s`, id) row := db.QueryRowContext(ctx, "SELECT title FROM videos WHERE video_id = ?", id) ?, $N, :param, которые зависят от базы данных.// запрос для SQLite и MySQL
db.Query("SELECT * FROM video WHERE likes BETWEEN ? AND ?", minVal, maxVal)
// запрос для PostgreSQL
db.Query("SELECT * FROM video WHERE likes BETWEEN $1 AND $2", minVal, maxVal) ? будет разным для разных типов баз данных. Для MySQL, SQLite3 и MS SQL это будет ?. Для PostgreSQL — $N, где N - число. Для Oracle — :param.NULL, которое может быть записано в поле любого типа. Оно означает, что данное поле не содержит значения. Например, NULL нужен, чтобы отличить пустую строку в текстовом поле от отсутствия значения (значение не определено). Чтобы отслеживать такие ситуации, пакет database/sql содержит типы начинающиеся с Null: sql.NullString, sql.NullInt64, sql.NullFloat64, sql.NullByte и так далее. sql.NullString. Он представляет строку, которая в SQL-таблице может быть не установлена, то есть иметь значение NULL. Этот тип, как и другие подобные, описывается структурой с полем Valid bool. Оно равно true, если у поля в SQL-таблице есть значение, и false — если значение — NULL.type NullString struct {
String string
Valid bool
} video_id ищет ролик и возвращает его описание в поле description. Если значение поля не определено, то нужно возвратить -----. В этом случае пригодится тип NullString. В колонке description таблицы videos нет значений NULL, поэтому не получится проверить работу функции. С другой стороны, тут есть записи с пустыми строками в description. В этом случае будет возвращена именно пустая строка, а не -----.func getDesc(ctx context.Context, db *sql.DB, id string) (string, error) {
row := db.QueryRowContext(ctx,
"SELECT description FROM videos WHERE video_id = ?", id)
var desc sql.NullString
err = row.Scan(&desc)
if err != nil {
return "", err
}
if desc.Valid {
return desc.String, nil
}
return "-----", nil
} 0EbFotkXOiA (должна возвратить пустую строку). После этого, выполните UPDATE videos SET description = NULL WHERE video_id = '0EbFotkXOiA'; в программе sqlite3 или другом клиенте БД и снова вызовите getDesc для этого же идентификатора. Если вы всё сделали правильно, то функция возвратит -----.videos надо выбрать первые X самых популярных роликов, где X — количество.SELECT video_id, title, views from videos ORDER BY views LIMIT X; Video, в которую функция Scan будет записывать значения.// Video — структура видео.
type Video struct {
Id string
Title string
Views int64
}
// limit — максимальное количество записей.
const limit = 20 func QueryVideos(ctx context.Context, db *sql.DB, limit int) ([]Video, error) {
videos := make([]Video, 0, limit)
rows, err := db.QueryContext(ctx, "SELECT video_id, title, views from videos ORDER BY views LIMIT ?", limit)
if err != nil {
return nil, err
}
// обязательно закрываем перед возвратом функции
defer rows.Close()
// пробегаем по всем записям
for rows.Next() {
var v Video
err = rows.Scan(&v.Id, &v.Title, &v.Views)
if err != nil {
return nil, err
}
videos = append(videos, v)
}
// проверяем на ошибки
err = rows.Err()
if err != nil {
return nil, err
}
return videos, nil
} defer rows.Close() — это обязательно. Пока доступны результаты, соединение активно и не закроется ещё долгое время. Если не закрыть, соединение будет «грязным». За ним будет закреплён курсор с данными, и драйверу придётся закрыть соединение и создать новое, что требует ресурсов.rows.Next() до тех пор, пока не пройдём все доступные результаты.rows.Err(). Метод возвращает ошибку, которая могла произойти в момент получения новой порции данных (новой итерации цикла rows.Next()). Ошибка заставит итерацию прекратиться, и алгоритм не сможет проверить её другим способом. Пример такой ошибки — разрыв сетевого соединения с сервером базы данных в процессе получения результатов запроса.db.QueryContext(ctx, "SELECT video_id, title, views from videos ORDER BY views LIMIT ?", 20) — запрос выполняется с помощью специального встроенного форматирования, с помощью символа ?. Никогда не используйте в запросе fmt.Sprintf. Этот способ небезопасен — он приводит к SQL-инъекциям.Scan поддерживает ограниченный набор типов переменных: строки, числа, булевы значения и байты. А что, если нужно использовать массивы или собственные структуры?videos есть колонка tags, которая содержит теги в таком формате: Robots|"Boston Dynamics"|"SpotMini"|"Legged Locomotion"|"Dynamic robot". Попробуем получить их в виде слайса строк.type Tags []string
type Video struct {
Id string
Title string
Tags Tags
} database/driver.Valuer и database/sql.Scanner:// приводит сложные типы и структуры к простому типу
type Valuer interface {
Value() (Value, error)
}
// приводит простой тип к сложным типам и структурам Go
type Scanner interface {
Scan(src any) error
} Value() для приведения Tags к строке:// Value — функция реализующая интерфейс driver.Valuer
func (tags Tags) Value() (driver.Value, error) {
// преобразуем []string в string
if len(tags) == 0 {
return "", nil
}
return strings.Join(tags, "|"), nil
} int64, float64, bool, []byte, string, time.Time, nil.Scan() для реализации интерфейса sql.Scanner.func (tags *Tags) Scan(value interface{}) error {
// если `value` равен `nil`, будет возвращён пустой массив
if value == nil {
*tags = Tags{}
return nil
}
sv, err := driver.String.ConvertValue(value)
if err != nil {
return fmt.Errorf("cannot scan value. %w", err)
}
v, ok := sv.(string)
if !ok {
return errors.New("cannot scan value. cannot convert value to string")
}
*tags = strings.Split(v, "|")
// удаляем кавычки у тегов
for i, v := range *tags {
(*tags)[i] = strings.Trim(v, `"`)
}
return nil
} driver.String.ConvertValue(value) пытаемся конвертировать значение в строку. После этого преобразуем значение в слайс строк. Напишем функцию, которая будет получать идентификатор, наименование и теги у роликов.func QueryTagVideos(ctx context.Context, db *sql.DB, limit int) ([]Video, error) {
videos := make([]Video, 0, limit)
rows, err := db.QueryContext(ctx, "SELECT video_id, title, tags from videos "+
"GROUP BY video_id ORDER BY views LIMIT ?", limit)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var v Video
// все теги должны автоматически преобразоваться в слайс v.Tags
err = rows.Scan(&v.Id, &v.Title, &v.Tags)
if err != nil {
return nil, err
}
videos = append(videos, v)
}
err = rows.Err()
if err != nil {
return nil, err
}
return videos, nil
}
func main() {
db, err := sql.Open("sqlite", "video.db")
if err != nil {
panic(err)
}
defer db.Close()
list, err := QueryTagVideos(context.Background(), db, 5)
if err != nil {
panic(err)
}
// для теста проверим, какие строки содержит v.Tags
// выведем по 4 первых тега
for _, v := range list {
length := 4
if len(v.Tags) < length {
length = len(v.Tags)
}
fmt.Println(strings.Join(v.Tags[:length], " # "))
}
} shooting # kentucky # breaking news # u.s. news
[none]
Oregon # Ducks # college athletics # college football
news # associated press # ap # ap online
90s commercials # Huffy Query() в виде строки. Вы изучили, что метод Rows.Next() перебирает полученные в результате запроса строки, а разбор строки в целевую структуру Go делает метод Row.Scan(). database/sql. А ещё — что пакет предоставляет интерфейс для конвертации пользовательских типов.SELECT-запросы к SQL базам и как обрабатывать полученные в ответе данные. Этот полезный навык пригодится, например, при разработке веб-сервисов.