ozzo-dbx это Go-пакет, который расширяет стандартный пакет database/sql
, путем предоставления мощных методов извлечения данных,
а также позволяет использовать DB-agnostic (независимый от БД) построитель запросов. Он имеет следующие особенности:
- Заполнение данных в структуры или NullString карты
- Именованные параметры связывания
- DB-agnostic методы построения запросов, включая SELECT, запросы на манипуляцию с данными и запросы на манипуляцию со схемой (структурой) БД
- Мощный построитель запросов с условиями
- Открытая архитектура, позволяющая с легкостью создавать поддержку для новых баз данных или кастомизировать текущую поддержку
- Логирование выполненных SQL запросов
- Предоставляет поддержку основных реляционных баз данных
Go 1.2 или выше.
Выполните данные команды для установки:
go get github.com/go-ozzo/ozzo-dbx
В дополнение, установите необходимый пакет драйвера базы данных для той базы, которая будет использоваться. Пожалуйста обратитесь к SQL database drivers для получения полного списка. Например, если вы используете MySQL, вы можете загрузить этот пакет:
go get github.com/go-sql-driver/mysql
и импортировать его в ваш основной код следующим образом:
import _ "github.com/go-sql-driver/mysql"
Представленные базы данных уже имеют поддержку из коробки:
- SQLite
- MySQL
- PostgreSQL
- MS SQL Server (2012 или выше)
- Oracle
Другие базы данных также могут работать. Если нет, вы можете создать для него свой построитель, как описано далее в этом документе.
Представленный ниже код показывает как вы можете использовать пакет для доступа к данным базы данных MySQL.
import (
"fmt"
"github.com/go-ozzo/ozzo-dbx"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, _ := dbx.Open("mysql", "user:pass@/example")
// создаем новый запрос
q := db.NewQuery("SELECT id, name FROM users LIMIT 10")
// извлекаем все строки в срез структур
var users []struct {
ID, Name string
}
q.All(&users)
// извлекаем одну строку в структуру
var user struct {
ID, Name string
}
q.One(&user)
// извлекаем строку в строковую карту
data := dbx.NullStringMap{}
q.One(data)
// извлечение строку за строкой
rows2, _ := q.Rows()
for rows2.Next() {
rows2.ScanStruct(&user)
// rows.ScanMap(data)
// rows.Scan(&id, &name)
}
}
Следующий пример показывает, как можно использовать возможности построителя запросов из этого пакета.
import (
"fmt"
"github.com/go-ozzo/ozzo-dbx"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, _ := dbx.Open("mysql", "user:pass@/example")
// строим SELECT запрос
// SELECT `id`, `name` FROM `users` WHERE `name` LIKE '%Charles%' ORDER BY `id`
q := db.Select("id", "name").
From("users").
Where(dbx.Like("name", "Charles")).
OrderBy("id")
// извлекаем все строки в срез структур
var users []struct {
ID, Name string
}
q.All(&users)
// строим INSERT запрос
// INSERT INTO `users` (`name`) VALUES ('James')
db.Insert("users", dbx.Params{
"name": "James",
}).Execute()
}
Для соединения с базой данных, используйте dbx.Open()
точно таким же образом, как вы могли бы сделать это при помощи Open()
метода в database/sql
.
db, err := dbx.Open("mysql", "user:pass@hostname/db_name")
Метод возвращает экземпляр dbx.DB
который можно использовать для создания и выполнения запросов к БД. Обратите внимание,
что метод на самом деле не устанавливает соединение до тех пор пока вы не выполните запрос с использованием экземпляра dbx.DB
. Он так же
не проверяет корректность имени источника данных. Выполните dbx.MustOpen()
для того чтобы убедиться, что имя базы данных правильное.
Для выполнения SQL запроса, сначала создайте экземпляр dbx.Query
, и далее создайте запрос при помощи DB.NewQuery()
с SQL выражением,
которое необходимо исполнить. И затем выполните Query.Execute()
для исполнения запроса, в случае если запрос не предназначен для извлечения данных.
Например,
q := db.NewQuery("UPDATE users SET status=1 WHERE id=100")
result, err := q.Execute()
Если SQL запрос должен вернуть данные (такой как SELECT), следует вызвать один из нижепреведенных методов, которые выполнят запрос и сохранят результат в указанной переменной/переменных.
Query.All()
: заполняет массив структур илиNullString
карты всеми строками результата.Query.One()
: сохраняет первую строку из результата в структуру или вNullString
карту.Query.Row()
: заполняет список переменных первой строкой результата, каждая перменная заполняется данными одной из возвращаемых колонок.Query.Rows()
: возвращает экземплярdbx.Rows
для обеспечения дальнейшей возможности извлечения данных методом строка за строкой.
Например,
type User struct {
ID int
Name string
}
var (
users []User
user User
row dbx.NullStringMap
id int
name string
err error
)
q := db.NewQuery("SELECT id, name FROM users LIMIT 10")
// заполняет срез User всеми строками из запроса
err = q.All(&users)
fmt.Println(users[0].ID, users[0].Name)
// заполняет структуру User данными из первой строки
err = q.One(&user)
fmt.Println(user.ID, user.Name)
// запоминает первую строку в карту NullString
err = q.One(&row)
fmt.Println(row["id"], row["name"])
// заполняет переменные id и name данными из первой строки
err = q.Row(&id, &name)
// заполнят данные методом строка за строкой
rows, _ := q.Rows()
for rows.Next() {
rows.ScanMap(&row)
}
При заполнении структуры, данные правила используются для определения какая колонка будет сохранена в какое поле структуры:
- Только экспортируемые поля структуры будут заполнены.
- Поле принимает данные, если его имя имеет отображение к столбцу в соответствии с функцией отображения
Query.FieldMapper
. Функция отображения поля по умолчанию разделяет слова в имени поля подчеркиванием и приводит их к нижнему регистру. Например, полеFirstName
будет отображено в имя столбцаfirst_name
, иMyID
вmy_id
. - Если поле имеет
db
тег, значение тега будет использовано для опредления соответствующего имени столбца. Еслиdb
тег имеет-
, это обозначает, что поле НЕ будет заполнено. - Анонимные поля типа структуры будут расширены и будут заполнены в соответствии с правилами описанными выше.
- Именованные поля типа структуры также будут раширяться. Но их составные поля будут иметь префикс с именем структуры при заполнении.
Представленный ниже пример показывает, как поля будут заполняться в соответствии с указанными выше правилами:
type User struct {
id int
Type int `db:"-"`
MyName string `db:"name"`
Prof Profile
}
type Profile struct {
Age int
}
User.id
: не будет заполнено в следствие того, что поле id не экспортируется (т.к. оно записано с маленькой буквы);User.Type
: не будет заполнено потому что поле имеетdb
тег-
;User.MyName
: будет заполнено из столбцаname
, в соответствии с тегомdb
;Profile.Age
: будет заполнено из столбцаprof.age
, т.к.Prof
имя поля типа структуры и оно получит префиксprof.
.
Обратите внимание, что если столбец не имеет соответствующего поля в структуре, он будет проигнорирован. По аналогии, если поле структуры не имееет соответствующего столбца в результате, то оно не будет заполнено.
SQL запросы, как правило, имеют димамические значения. Например, вы можете выбрать запись пользоателя в соответствии с ID пользователя, полученного от клиента. В этом случае должно быть выполнено связывание параметров, и это почти всегда предпочтительно в целях обеспечения безопасности. В отличии от database/sql
которая разрешает анонимное связывание, ozzo-dbx
использует для связывания именованые параметры. Например,
q := db.NewQuery("SELECT id, name FROM users WHERE id={:id}")
q.Bind(dbx.Params{"id": 100})
q.One(&user)
Приведенный выше пример выберет пользователя id
которого 100. Метод Query.Bind()
связывает набор из именовынных параметров с SQL запросом который содержит плейсхолдеры в формате {:ParamName}
.
Если оператор SQL должен быть выполнен несколько раз с различными параметрами, он должен быть подготовлен для увеличения производительности. Например,
q := db.NewQuery("SELECT id, name FROM users WHERE id={:id}")
q.Prepare()
q.Bind(dbx.Params{"id": 100})
q.One(&user)
q.Bind(dbx.Params{"id": 200})
q.One(&user)
// ...
Обратите внимание, что анонимные параметры не поддерживаются, т.к. это внесет беспорядок в именованное связывание.
Вместо того чтобы писать обычные SQLs запросы, ozzo-dbx
позволяет строить SQL запросы программно, что чаще всего приводит к чистоте кода и большей защищенности, а так делает код не зависимым от используемой базы данных. Вы можете построить три типа запросов: SELECT запросы, запросы для манипуляции с данными и запросы для манипуляции со схемой (структурой) БД.
Построение SELECT запроса начинается с вызова DB.Select()
. Вы можете использовать различные условия для SELECT запросов, используя соответствующие методы построения запросов. Например,
db, _ := dbx.Open("mysql", "user:pass@/example")
db.Select("id", "name").
From("users").
Where(dbx.HashExp{"id": 100}).
One(&user)
Приведенный выше код будет генерировать следующий SQL запрос:
SELECT `id`, `name` FROM `users` WHERE `id`={:p0}
Обратите внимание, что название таблицы и имена стобцов будут правильно экранированы в сосответствии с типом используемой базы данных.
И параметр подстановки значения p0
попадет в условие WHERE
.
Каждое ключевое слово SQL имеет свой соответствующий метод в построителе запросов. Например, SELECT
соответствет Select()
,
FROM
соответствует From()
, WHERE
соответствует Where()
, и так далее. Вы можете использовать эти методы совместно, как и в обычном SQL запросе. Каждый из этих методов возвращает экземпляр запроса (типа dbx.SelectQuery
) который строится.
Как только вы закончите построения запроса, вы можете вызвать методы, такие как One()
, All()
для выполнения запроса и и сохранения данных в переменные. Также вы можете явно вызвать Build()
для построения запроса и превратить его в экземпляр dbx.Query
что может помоч вам получить SQL выражение и делать другую интересную работу.
ozzo-dbx
поддерживает очень гибкий и мощный построитель условий для запроса, который может быть использован для SQL выражений
таких как WHERE
, HAVING
, и т.д. Например,
// id=100
dbx.NewExp("id={:id}", dbx.Params{"id": 100})
// id=100 AND status=1
dbx.HashExp{"id": 100, "status": 1}
// status=1 OR age>30
dbx.Or(dbx.HashExp{"status": 1}, dbx.NewExp("age>30"))
// name LIKE '%admin%' AND name LIKE '%example%'
dbx.Like("name", "admin", "example")
При построении выражения условий в запросе, значение его параметров будет заполнено с помощью параметрического связывания, которое предотвращает использование SQL инекций. Кроме того, если выражение содержит имена столбцов, они будут правильно экранированы. Доступны слудующие функции для построениея условий:
dbx.NewExp()
: создает условие используя заданное строкового выражение и связываемый параметр. Например,dbx.NewExp("id={:id}", dbx.Params{"id":100})
создаст выражениеid=100
.dbx.HashExp
: подставляет пары имя:значение вAND
оператор. Например,dbx.HashExp{"id":100, "status":1}
создаетid=100 AND status=1
.dbx.Not()
: создаетNOT
выражение подставляяNOT
в заданное выражение.dbx.And()
: создаетAND
выражение путем объединения заданных выражений при помощиAND
оператора.dbx.Or()
: создает выражениеOR
путем объединения заданных выражений при помощи оператораOR
.dbx.In()
: создаетIN
выражение для указанного столбца и диапазона значений. Например,dbx.In("age", 30, 40, 50)
создаст выражениеage IN (30, 40, 50)
. Обратите внимание, что, если диапазон значений пуст, он будет генерировать выражение, представляющее ложное значение.dbx.NotIn()
: создаетNOT IN
выражение. Что очень похоже наdbx.In()
.dbx.Like()
: создаетLIKE
выражение для указанного столбца и диапазона значений. Например,dbx.Like("title", "golang", "framework")
создаст выражениеtitle LIKE "%golang%" AND title LIKE "%framework%"
. Вы можете дополнить LIKE выражение при помощиEscape()
и/илиMatch()
функции полученного выражения. Обратите внимание, что, если диапазон значений пуст, он будет генерировать пустое выражение.dbx.NotLike()
: создаетNOT LIKE
выражение. Что очень похоже наdbx.Like()
.dbx.OrLike()
: создаетLIKE
выражение объединением различныхLIKE
подвыражений при помощиOR
вместоAND
.dbx.OrNotLike()
: создаетNOT LIKE
выражение путем объединенияNOT LIKE
выражений используяOR
вместоAND
.dbx.Exists()
: создаетEXISTS
выражение путем добавленияEXISTS
к заданному выражению.dbx.NotExists()
: создаетNOT EXISTS
выражение путем добавленияNOT EXISTS
к заданному выражению.dbx.Between()
: создаетBETWEEN
выражение. Например,dbx.Between("age", 30, 40)
создает выражениеage BETWEEN 30 AND 40
.dbx.NotBetween()
: создаетNOT BETWEEN
выражение.
Вы также можете создавать другие удобные функции для помощи в построении условий, таким образом, чтобы функция возвращала
объект реализующий интерфейс dbx.Expression
.
Запросы манипуляции с данными это запросы которые изменяют данные в базе данных, такие как INSERT, UPDATE, DELETE операторы.
Такого рода запросы могут быть построены путем вызова соответствующих методов DB
. Например,
db, _ := dbx.Open("mysql", "user:pass@/example")
// INSERT INTO `users` (`name`, `email`) VALUES ({:p0}, {:p1})
db.Insert("users", dbx.Params{
"name": "James",
"email": "[email protected]",
}).Execute()
// UPDATE `users` SET `status`={:p0} WHERE `id`={:p1}
db.Update("users", dbx.Params{"status": 1}, dbx.HashExp{"id": 100}).Execute()
// DELETE FROM `users` WHERE `status`={:p0}
db.Delete("users", dbx.HashExp{"status": 2}).Execute()
При построении запросов на манипуляцию с данными, не забудьте в конце использовать Execute()
для выполнения запроса.
Запросы для манипулирования со схемой БД изменяют структуру БД, то есть создают новую таблицу, добавляют новый столбец и т.д.
Эти запросы могут быть построены путем вызова соответствующих методов DB
. Например,
db, _ := dbx.Open("mysql", "user:pass@/example")
// CREATE TABLE `users` (`id` int primary key, `name` varchar(255))
q := db.CreateTable("users", map[string]string{
"id": "int primary key",
"name": "varchar(255)",
})
q.Execute()
Различные базы данных по разному экранируют (заключают в кавычки) название таблиц и названия столбцов. Для возможности писать SQL запросы, независимые от БД (DB-agnostic SQLs), ozzo-dbx предоставляет специальный синтакс для экранирования названия имен таблиц и столбцов. Слово заключенное в {{
и }}
трактуется как имя таблицы и будет экранировано в соответствии с используемым драйвером БД. Подобным образом слово заключенное в [[
и ]]
трактуется как имя столбца и также будет правильно экранировано. Например, при работе с базой данных MySQL, следующий запрос будет должным образом экранирован:
// SELECT * FROM `users` WHERE `status`=1
q := db.NewQuery("SELECT * FROM {{users}} WHERE [[status]]=1")
Обратите внимание, что если имя таблицы или столбца содержит префикс, он все равно будет правильно проэкранирован. Например, {{public.users}}
будет экранирован как "public"."users"
для PostgreSQL.
Вы можете использовать все возможные методы для выполнения запросов с использованием транзакций. Например,
db, _ := dbx.Open("mysql", "user:pass@/example")
tx, _ := db.Begin()
_, err1 := tx.Insert("users", dbx.Params{
"name": "user1",
}).Execute()
_, err2 := tx.Insert("users", dbx.Params{
"name": "user2",
}).Execute()
if err1 == nil && err2 == nil {
tx.Commit()
} else {
tx.Rollback()
}
Когда DB.LogFunc
настроен для логирования, все SQL будут выполены и сохранены в лог.
В следующем примере показано как сконфигурировать логгер при использования стандартного log
пакета:
import (
"fmt"
"log"
"github.com/go-ozzo/ozzo-dbx"
)
func main() {
db, _ := dbx.Open("mysql", "user:pass@/example")
db.LogFunc = log.Printf
// ...
)
А этот пример показывет как применять ozzo-log
пакет, который позволяет использовать уровни важности и категории,
а также отправлять сообщения различным получателям (таким как файлы, окно консоли, сетевое устройство).:
import (
"fmt"
"github.com/go-ozzo/ozzo-dbx"
"github.com/go-ozzo/ozzo-log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
logger := log.NewLogger()
logger.Targets = []log.Target{log.NewConsoleTarget()}
logger.Open()
db, _ := dbx.Open("mysql", "user:pass@/example")
db.LogFunc = logger.Info
// ...
)
Не смотря на то, что ozzo-dbx
"из коробки" предоставляет поддержку всех основных реляционных баз данных, его окрытая архитектура
позволяет вам добавлять поддержку новых баз данных. Действия для добавления новой базы данных включают в себя:
- создание структуры, которая имплементирует интерфейс
QueryBuilder
. Вы можете использоватьBaseQueryBuilder
напрямую или расширить его функциональность. - Создать структуру которая имплементирует интерфейс
Builder
. Вы можете расширитьBaseBuilder
с помощью композиции. - Напишите
init()
функцию для регистрации нового построителя вdbx.BuilderFuncMap
.