Вы могли заметить, что пакет database/sql
по сути предоставляет стандартный интерфейс между нашим приложением на Go и миром баз данных SQL.
Содержание статьи
- Пакет database/sql в Go
- NULL значения в SQL и Golang
- Работа с SQL транзакциями в Go
- Управление SQL соединениями
Пакет database/sql в Go
Пока вы используете пакет database/sql
, ваш код будет работать с любым типом SQL базы данных — будь то MySQL, PostgreSQL, SQLite или что-то еще. Это означает, что ваше приложение не зависит от определённой базы данных, которую вы используете в настоящее время. Следовательно, вы можете менять базу данных в будущем, не переписывая весь код (за исключением специфических особенностей драйвера и SQL запросов).
Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании! 😎
Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.
Важно отметить, что, хотя пакет database/sql
обычно хорошо справляется предоставляя стандартный интерфейс для работы с SQL базами данных, существуют некоторые особенности в работе различных драйверов и баз данных. Всегда нужно ознакомиться с документацией к новому драйверу, чтобы понять его особенности перед началом его использования.
NULL значения в SQL и Golang
Одним из недостатков Go является тот факт, что данный язык не очень хорошо справляется с управлением NULL
значениями в записях базы данных.
Предположим, что столбец title
в таблице snippets
содержит значением NULL
в определенном ряду. Если выполнить запрос к данному ряду, то метод rows.Scan()
вернет ошибку, потому что он не может конвертировать NULL
в строку.
1 2 |
sql: Scan error on column index 1: unsupported Scan, storing driver.Value type <nil> into type *string |
Грубо говоря, исправить это можно, изменив сканируемое поле из string
на тип sql.NullString
. В качестве примера можете ознакомиться с этим gist.
Как правило, проще всего вообще избегать значений NULL
. Поставьте ограничения NOT NULL
для всех столбцов базы данных, как мы это сделали в этом курсе.
Работа с SQL транзакциями в Go
Важно понимать, что вызовы Exec()
, Query()
и QueryRow()
могут использовать любое соединение из пула sql.DB
. Даже если в вашем коде есть два вызова Exec()
рядом друг с другом, нет гарантии, что они будут использовать одно и то же соединение с базой данных.
Иногда это неприемлемо. Например, если заблокировать таблицу с помощью MySQL команды LOCK TABLES
, нужно вызвать UNLOCK TABLES
точно для того же соединения, чтобы избежать блокировки всего процесса работы с данной таблицей из других соединений.
Для гарантии использования одного и того же соединения можно заключить в транзакцию несколько SQL-операторов.
Вот основной шаблон метода в нашей тестовой модели:
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 |
type ExampleModel struct { DB *sql.DB } func (m *ExampleModel) ExampleTransaction() error { // Вызов метода Begin() в пуле соединений создает новый объект sql.Tx // который представляет текущую транзакцию к базы данных. tx, err := m.DB.Begin() if err != nil { return err } // Вызываем Exec() для транзакции, передавая оператор и любые другие // параметры. Важно отметить, что tx.Exec() вызывается для // только что созданного объекта транзакции, а НЕ для пула соединений. Хотя мы // здесь используем tx.Exec(), вы также можете использовать tx.Query() и tx.QueryRow() // таким же образом _, err = tx.Exec("INSERT INTO ...") if err != nil { // Если возникла ошибка, вызываем метод tx.Rollback() для // транзакции. Он прервет транзакцию и // в базу данных не будут внесены изменения. tx.Rollback() return err } // Точно так же выполняется другая транзакция. _, err = tx.Exec("UPDATE ...") if err != nil { tx.Rollback() return err } // Если ошибок нет, то запрос транзакции может быть // выполнен в базе данных с помощью метода tx.Commit(). Очень важно ВСЕГДА // вызывать Rollback() или Commit() в конце функции перед "return". Если вы // этого не сделаете, соединение останется открытым и не будет возвращено // в пул соединений. Это может привести к достижению максимального лимита соединений и исчерпанию ресурсов. err = tx.Commit() return err } |
Транзакции очень полезны, если нужно выполнять несколько SQL запросов как одно единое действие. Даже если возникнет ошибка, но пока вы используете метод tx.Rollback()
, транзакция гарантирует, что:
- Все запросы выполняются успешно… или
- Никакие запросы не выполняются, и база данных остается неизменной.
Управление SQL соединениями в Go
Пул подключений sql.DB
состоит из соединений, которые либо простаивают, либо используются. По умолчанию нет ограничения для максимального количества открытых подключений (простаивающих + используемых) одновременно, но максимальное количество неактивных подключений в пуле по умолчанию равно 2.
Эти значения можно изменить с помощью методов SetMaxOpenConns()
и SetMaxIdleConns()
. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
db, err := sql.Open("mysql", *dsn) if err != nil { log.Fatal(err) } // Устанавливаем максимальное количество одновременно открытых (простаивающих + используемых) соединений. // Указание этого значения меньше или равного 0 будет означать, что ограничений нет. Если максимальное // количество открытых подключений достигнуто и все они используются, но нам требуется новое подключение, // тогда Go будет ждать, пока одно из подключений не освободится. С точки зрения // пользователя это означает, что HTTP запрос будет загружаться до тех пор, пока // соединение с базой данных не станет доступным. По сути, у пользователя зависнет сайт. db.SetMaxOpenConns(100) // Устанавливаем максимальное количество неактивных соединений в пуле. Указание этого // значения меньше или равного 0 будет означать, что неактивные соединения не будут создаваться. // Каждый запрос создаст новое подключение и сразу начнет с ней работу. db.SetMaxIdleConns(5) |
Использование данных методов сопровождается предупреждением: у базы данных скорее всего есть ограничение на максимальное количество подключений.
Например, ограничение по умолчанию для MySQL равно 151. Таким образом, если настроить SetMaxOpenConns()
полностью неограниченным или установить значение больше 151 подключений, это может привести к ошибке "too many connections"
при высокой нагрузке. Чтобы эта ошибка не возникла, нужно установить максимальное значение открытых подключений ниже 151.
Однако, так появляется другая проблема. Когда достигается предел SetMaxOpenConns()
, любые новые задачи к базы данных, которые приложению необходимо выполнить, будут ждать, пока соединение не станет свободным и продолжить работу.
Для некоторых приложений такое поведение может быть нормальным, но веб-приложению лучше сразу записать ошибку в журнал как "too many connections"
и отправлять пользователю ответ 500 Internal Server Error
вместо того, чтобы их HTTP запрос завис и ожидал освобождения нового подключения к базе данных.
По этой причине мы не использовали методы SetMaxOpenConns()
и SetMaxIdleConns()
в нашем приложении все настройки по умолчанию. Такие моменты лучше настраивать в конфигурационном файле самого MySQL.
Администрирую данный сайт с целью распространения как можно большего объема обучающего материала для языка программирования Go. В IT с 2008 года, с тех пор изучаю и применяю интересующие меня технологии. Проявляю огромный интерес к машинному обучению и анализу данных.
E-mail: vasile.buldumac@ati.utm.md
Образование
Технический Университет Молдовы (utm.md), Факультет Вычислительной Техники, Информатики и Микроэлектроники
- 2014 — 2018 Universitatea Tehnică a Moldovei, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Universitatea Tehnică a Moldovei, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»