Запрос для получения одной записи из базы данных через SELECT
может показаться немного сложным. Давайте разберемся, как это можно выполнить используя обновленный метод SnippetModel.Get()
, чтобы он возвращал определенную заметку на основе её ID.
Содержание статьи
- Конвертирование типов из MySQL в Go
- Модели базы данных в обработчиках
- Обработка ошибок используя errors.Is()
- Метод для получения данных записи
Для этого требуется выполнить SQL запрос к базе данных:
Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании! 😎
Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.
1 2 |
SELECT id, title, content, created, expires FROM snippets WHERE expires > UTC_TIMESTAMP() AND id = ? |
Поскольку таблица snippets
использует столбец id
в качестве первичного ключа, этот запрос всегда будет возвращать только одну запись из базы данных (или вообще ни одной). Запрос также включает проверку срока годности заметки, чтобы не возвращались заметки с истекшим сроком.
Обратите внимание, что мы снова используем плейсхолдер для id
, это сделано в целях безопасности. Ведь, в будущем ID заметки мы получим от пользователя из URL. Это не безопасный источник для получения данных, потому мы передаём данные через плейсхолдер при выполнении SQL запроса.
Откройте файл pkg/models/mysql/snippets.go
и добавьте следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
package mysql import ( "database/sql" "errors" // новый импорт "golangify.com/snippetbox/pkg/models" ) type SnippetModel struct { DB *sql.DB } ... // Get - Метод для возвращения данных заметки по её идентификатору ID. func (m *SnippetModel) Get(id int) (*models.Snippet, error) { // SQL запрос для получения данных одной записи. stmt := `SELECT id, title, content, created, expires FROM snippets WHERE expires > UTC_TIMESTAMP() AND id = ?` // Используем метод QueryRow() для выполнения SQL запроса, // передавая ненадежную переменную id в качестве значения для плейсхолдера // Возвращается указатель на объект sql.Row, который содержит данные записи. row := m.DB.QueryRow(stmt, id) // Инициализируем указатель на новую структуру Snippet. s := &models.Snippet{} // Используйте row.Scan(), чтобы скопировать значения из каждого поля от sql.Row в // соответствующее поле в структуре Snippet. Обратите внимание, что аргументы // для row.Scan - это указатели на место, куда требуется скопировать данные // и количество аргументов должно быть точно таким же, как количество // столбцов в таблице базы данных. err := row.Scan(&s.ID, &s.Title, &s.Content, &s.Created, &s.Expires) if err != nil { // Специально для этого случая, мы проверим при помощи функции errors.Is() // если запрос был выполнен с ошибкой. Если ошибка обнаружена, то // возвращаем нашу ошибку из модели models.ErrNoRecord. if errors.Is(err, sql.ErrNoRows) { return nil, models.ErrNoRecord } else { return nil, err } } // Если все хорошо, возвращается объект Snippet. return s, nil } ... |
На заметку: Вам может быть интересно, почему мы возвращаем ошибку
models.ErrNoRecord
вместоsql.ErrNoRows
. Причина в том, что требуется полностью инкапсулировать модель, чтобы приложение не было связано с базовым хранилищем данных или зависело от ошибок базы данных.
Конвертирование типов из MySQL в Go
Под капотом метода rows.Scan()
, драйвер базы данных автоматически преобразует MySQL типы в типы языка программирования Go:
CHAR
,VARCHAR
иTEXT
соответствуют типу string;BOOLEAN
соответствует bool;INT
соответствует int;BIGINT
соответствует int64;DECIMAL
иNUMERIC
соответствуют float;TIME
,DATE
иTIMESTAMP
соответствуют time.Time.
Особенность нашего MySQL драйвера заключается в том, нам нужно использовать параметр parseTime=true
в нашей строке подключения к MySQL, чтобы заставить его преобразовывать поля TIME
и DATE
в time.Time
. В противном случае, он вернёт их как объекты []byte
. Это один из многих предлагаемых параметров для предварительной настройки драйвера базы данных при подключении к нему.
Модели базы данных в обработчиках
Отлично, давайте воспользуемся методом SnippetModel.Get()
на практике.
Откройте файл cmd/web/handlers.go
и обновите обработчик showSnippet
, чтобы он возвращал данные для определенной записи:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
package main import ( "errors" // новый импорт "fmt" "html/template" "net/http" "strconv" "golangify.com/snippetbox/pkg/models" // новый импорт ) ... func (app *application) showSnippet(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(r.URL.Query().Get("id")) if err != nil || id < 1 { app.notFound(w) // Страница не найдена. return } // Вызываем метода Get из модели Snipping для извлечения данных для // конкретной записи на основе её ID. Если подходящей записи не найдено, // то возвращается ответ 404 Not Found (Страница не найдена). s, err := app.snippets.Get(id) if err != nil { if errors.Is(err, models.ErrNoRecord) { app.notFound(w) } else { app.serverError(w, err) } return } // Отображаем весь вывод на странице. fmt.Fprintf(w, "%v", s) } ... |
Запускаем наше веб-приложение из терминала:
1 |
go run ./cmd/web |
Перейдите в браузере на страницу http://127.0.0.1:4000/snippet?id=1. Вы должны увидеть HTTP ответ, похожий на следующий:
Также, можете попробовать сделать несколько запросов для других заметок, срок действия которых истек или которых вообще не существуют (например, id=99
), чтобы убедиться, что они возвращают ответ 404 Not Found
:
Обработка ошибок используя errors.Is()
В коде выше мы использовали функцию errors.Is(), которая была введена в Go 1.13, чтобы проверить, нужная ли нам ошибка выскочила (в нашем случае проверяется, если нам попалась именно ошибка sql.ErrNoRows
).
Обсудим данный вопрос немного подробнее.
Во-первых, sql.ErrNoRows является примером так называемых предопределённых ошибок (по аналогии с Python, это будут «исключения» exceptions), которых мы можем приблизительно определить как объект error
, хранящийся в глобальной переменной. Обычно они создаются с помощью функции errors.New()
. Несколько примеров ошибок из стандартной библиотеки — io.ErrUnexpectedEOF и bytes.ErrTooLarge.
Только что созданная нами ошибка models.ErrNoRecord
является примером предопределённой ошибки.
У нас есть её «имя» и мы можем «ловить» её в случае если она возникнет при работе программы. Всё работает по принципу
try - catch
в Java, PHP или try — except в Python.
В старых версиях Go (до 1.13) лучший способ проверить, если ошибка совпадает с предопределённой ошибкой, выглядел бы так:
1 2 3 4 5 |
if err == sql.ErrNoRows { // Делает что-то } else { // Делает что-то другое } |
Однако после Go 1.13 лучше использовать функцию errors.Is()
:
1 2 3 4 5 |
if errors.Is(err, sql.ErrNoRows) { // Делает что-то } else { // Делает что-то другое } |
Причина в том, что в Go 1.13 появилась возможность оборачивать ошибки для добавления дополнительной информации. По сути, мы создаём новую ошибку на базе уже существующей ошибки.
Для строго стиля проверки на ошибку — предопределённая ошибка и созданная (обёрнутая) на основе неё другая новая ошибка — не будут совпадать, так как обернутая ошибка не равна оригинальной предопределённой ошибке.
Функция errors.Is()
наоборот работает путем распаковки ошибок — при необходимости — перед проверкой на совпадения.
Если у вас Go 1.13 или новее, лучше использовать error.Is()
. Это хороший способ защитить код в будущем и предотвратить проблемы, вызванные вами — или любыми пакетами, которые ваш код импортирует.
Существует также другая функция, errors.As(), которую вы можете использовать, чтобы проверить, есть ли (у потенциально обернутой — wrapped) ошибки определенный тип. Мы будем использовать эту функцию в будущих уроках.
Для получения дополнительной информации об изменениях в способах обработки ошибок в Go 1.13 можете прочитать официальный FAQ.
Метод для получения данных записи
Мы специально сделали код в SnippetModel.Get()
немного длиннее, чтобы было проще понять, что именно происходит за кулисами вашего кода.
На практике, можно сократить код (или хотя бы количество строк), воспользовавшись тем, что появление ошибки из DB.QueryRow()
откладываются до вызова Scan()
. Функциональной разницы нет, так что если вы хотите, можете переписать код следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
func (m *SnippetModel) Get(id int) (*models.Snippet, error) { s := &models.Snippet{} err := m.DB.QueryRow("SELECT ...", id).Scan(&s.ID, &s.Title, &s.Content, &s.Created, &s.Expires) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, models.ErrNoRecord } else { return nil, err } } return s, nil } |
Скачать исходный код урока
В конце каждой статьи мы предоставляем готовый исходный код на текущем этапе разработки.
Скачать: snippetbox-20.zip
Администрирую данный сайт с целью распространения как можно большего объема обучающего материала для языка программирования Go. В IT с 2008 года, с тех пор изучаю и применяю интересующие меня технологии. Проявляю огромный интерес к машинному обучению и анализу данных.
E-mail: vasile.buldumac@ati.utm.md
Образование
Технический Университет Молдовы (utm.md), Факультет Вычислительной Техники, Информатики и Микроэлектроники
- 2014 — 2018 Universitatea Tehnică a Moldovei, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Universitatea Tehnică a Moldovei, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»