diff --git a/ru/05.1.md b/ru/05.1.md new file mode 100644 index 00000000..47880e26 --- /dev/null +++ b/ru/05.1.md @@ -0,0 +1,211 @@ +# 5.1 Интерфейс database/sql + +Как я уже говорил, Go не предоставляет официальных драйверов баз данных, как это делает, например, PHP, но у него есть некоторые стандарты интерфейсов драйверов для разработчиков драйверов баз данных. Если ваш код будет разрабатываться в соответствии с этими стандартами, вам не придется его менять в случае изменения базы данных. Давайте посмотрим, что представляют из себя эти стандарты интерфейсов. + +## sql.Register + +Эта функция из пакета `database/sql` обеспечивает регистрацию драйверов баз данных сторонних разработчиков. Все сторонние драйвера должны вызывать функцию `Register(name string, driver driver.Driver)` в функции `init()` для того чтобы обеспечить регистрацию драйвера. + +Давайте посмотрим на соответствующий код в драйверах mymysql и sqlite3: + + //https://github.com/mattn/go-sqlite3 driver + func init() { + sql.Register("sqlite3", &SQLiteDriver{}) + } + + //https://github.com/mikespook/mymysql driver + // Driver automatically registered in database/sql + var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"} + func init() { + Register("SET NAMES utf8") + sql.Register("mymysql", &d) + } + +Мы видим, что все сторонние драйвера вызывают данную функцию для саморегистрации. При этом Go использует карту для сохранения информации о пользовательском драйвере внутри `databse/sql`. + +var drivers = make(map[string]driver.Driver) + +drivers[name] = driver + +Таким образом, данная функция может зарегистрировать любое количество драйверов с разными именами. + +Если посмотреть примеры использования сторонних драйверов, мы всегда увидим следующий код: + + import ( + "database/sql" + _ "github.com/mattn/go-sqlite3" + ) + +Бланк `_`, в примере, приводит в замешательство многих начинающих программистов, но он является примером отличного дизайна языка Go. Как вы знаете, этот идентификатор предназначен для удаления значений из функции return. Так же вы должны знать, что вы должны использовать все импортированные пакеты в коде. В данном случае, использование бланка при импорте означает, что вам необходимо исполнить функцию init() данного пакета без его прямого использования, которая, в свою очередь, используется для регистрации драйвера базы данных. + +## driver.Driver + +`Driver` - это интерфейс, который имеет метод `Open(name string)` который возвращает интерфейс `Conn`. + + type Driver interface { + Open(name string) (Conn, error) + } + +`Conn` - это одноразовое соединение. Это означает, что его можно использовать только один раз в одной горутине. Следующй код вызовет исключение: + + ... + go goroutineA (Conn) // запрос + go goroutineB (Conn) // вставка + ... + +потому, что Go не имеет представления в какой горутине выполняется операция. Операция "запрос" может получить результаты "вставки" и наоборот. + +Все сторонние драйверы должны реализовывать эту функцию для разбора имени соединения и корректного возврата результатов. + +## driver.Conn + +Driver.Conn - это интерфейс подключения базы данных с несколькими методами. + + type Conn interface { + Prepare(query string) (Stmt, error) + Close() error + Begin() (Tx, error) + } + +- `Prepare` - возвращает статус подготовки соответствующих команд SQL для запроса, удаления и т.д. +- `Close` - закрывает текущее соединение и освобождает ресурсы. Большинство сторонних драйверов реализуют пулы соединений самостоятельно, поэтому вам не стоит подключать кэш соединений если вы не хотите иметь неожиданных ошибок. +- `Begin` - запускает и возвращает дескриптор новой транзакции `Tx`, вы можете использовать его для запросов, апдейтов, отката транзакций и т.д. + +## driver.Stmt + +`Stmt` - это интерфейс состояния готовности, соответствующий данному соединению, поэтому он может быть использован только в одной горутине, как и `Conn`. + + type Stmt interface { + Close() error + NumInput() int + Exec(args []Value) (Result, error) + Query(args []Value) (Rows, error) + } + +- `Close` - закрывает текущее соединение, но по прежнему возвращает строки данных, если запрос продолжает выполнятся. +- `NumInput` - возвращает число обязательных аргументов, драйвер базы данных должен проверить аргументы вызывающей стороны и вернуть результат больше 0 и -1 в случае, если драйвер не смог определить количество аргументов. +- `Exec` выполняет SQL - команды `update/insert`, которые были подготовлены в `Prepare`. Возвращает `Result`. +- `Query` - выполняет SQL - команды `select`, которые были подготовлены в `Prepare`. Возвращает `Result`. + +## driver.Tx + +Данный интерфейс, как правило, управляет операциями commit и roll back. Драйвер базы данных должен реализовать эти два метода. + + type Tx interface { + Commit() error + Rollback() error + } + +## driver.Execer + +Это необязательный интерфейс. + + type Execer interface { + Exec(query string, args []Value) (Result, error) + } + +Если драйвер не реализует этот интерфейс, то при вызове `DB.Exec`, он автоматически вызывает `Prepare` и возвращает `Stmt`, выполняет `Exec` из `Stmt` затем закрывает `Stmt`. + +## driver.Result + +Этот интерфейс возвращает результаты операций `update/insert`. + + type Result interface { + LastInsertId() (int64, error) + RowsAffected() (int64, error) + } + +- `LastInsertId` - возвращает автоинкрементный Id после операций вставки в базу данных. +- `RowsAffected` - возвращает номера строк, затронутых операцией `update/insert`. + +## driver.Rows + +driver.Rows - это интерфейс для возвращения результатов набора операций запроса. + + type Rows interface { + Columns() []string + Close() error + Next(dest []Value) error + } + +- `Columns` - возвращает информацию о наименовании столбцов таблицы. +- `Close` - закрывает итератор строк. +- `Next` - возвращает следующее поле таблицы и связывает с `dest`, все строки должны быть преобразованы к массиву байт. Если доступные данные закончились, возникает ошибка `io.EOF`. + +## diriver.RowsAffected + +Данный тип является псевдонимом int64 для одноименного метода реализованного в интерфейсе `Result`. + + type RowsAffected int64 + + func (RowsAffected) LastInsertId() (int64, error) + + func (v RowsAffected) RowsAffected() (int64, error) + +## driver.Value + +Это пустой интерфейс, который может содержать любой тип данных. + + type Value interface{} + +`Value` должно быть содержать значения, совместимые с драйвером, или быть nil, поэтому оно должно быть одним из следующих типов: + + int64 + float64 + bool + []byte + string [*] За исключением `Rows.Next`, который не может возвращать строку. + time.Time + +## driver.ValueConverter + +`ValueConverter` - это интерфейс для преобразования наших данных к типам `driver.Value`. + + type ValueConverter interface { + ConvertValue(v interface{}) (Value, error) + } + +Обычно он используется в драйверах баз данных и предоставляет массу полезных особенностей: + +- Преобразует `driver.Value` в соответствующее типу поля базы данных. Например преобразует int64 в uint16. +- Преобразует результаты запросов к `driver.Value`. +- Преобразует `driver.Value` к определенному пользователем значению в функции `scan`. + +## driver.Valuer + +`Valuer` - определяет интерфейс для возврата ` driver.Value`. + + type Valuer interface { + Value() (Value, error) + } + +Многие типы реализуют этот интерфейс для преобразования типов между собой и driver.Value. + +На данном этапе вы должны иметь представление о том, как разработать драйвер базы данных. После того, как вы реализуете интерфейсы для различных операций, таких как добавление, удаление, обновление и так далее, останется решить проблему коммуникации с конкретной базой данных. + +## database/sql + +`databse/sql` определяет высокоуровневые методы для более удобной работы с базами данных (выше чем драйвера) и предлагает вам реализовать пул соединений. + + type DB struct { + driver driver.Driver + dsn string + mu sync.Mutex // защищает и закрывает freeConn + freeConn []driver.Conn + closed bool + } + +Как вы видите, функция `Open` возвращает тип `DB`, который содержит freeConn, - это и есть простой пулл. Его реализация очень проста и несколько уродлива. Он использует `defer db.putConn(ci, err)` в функции `Db.prepare` для помещения соединения в пулл соединений. Каждый раз, когда вызывается функция `Conn`, он проверяет длину ` freeConn` и, если она больше нуля, - это означает, что соединение можно повторно использовать и оно напрямую возвращается вам. В противном случае он создает новое соединение и возвращает его. + +## Ссылки + +- [Содержание](preface.md) +- Предыдущий раздел: [Базы данных](05.0.md) +- Следующий раздел: [MySQL](05.2.md) + + + + + + + diff --git a/ru/05.2.md b/ru/05.2.md new file mode 100644 index 00000000..7da9a34e --- /dev/null +++ b/ru/05.2.md @@ -0,0 +1,129 @@ +# 5.2 MySQL + +Комплекс северного программного обеспечения LAMP последние годы очень популярен в интернете. Буква М в данном акрониме означает MySQL. MySQL является наиболее известной базой данных с открытым исходным кодом. Она проста в использовании, поэтому ее часто используют в бэкенде множества веб-сайтов. + +## Драйвера MySQL + +Есть несколько драйверов, которые поддерживают MySQL в Go, некоторые из них включают `database/sql`, некоторые используют только стандарты интерфейсов. + +- [https://github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) поддерживает `database/sql`, чистый код Go. +- [https://github.com/ziutek/mymysql](https://github.com/ziutek/mymysql) поддерживает `database/sql` и определенные пользователем интерфейсы, чистый код Go. +- [https://github.com/Philio/GoMySQL](https://github.com/Philio/GoMySQL) поддерживает только определенные пользователем интерфейсы, чистый код Go. + +Я буду использовать первый драйвер в моих будущих примерах (в моих проекта я тоже использую его). Я рекомендую вам использовать его по следующим причинам: + +- Это новый драйвер базы данных и он предоставляет множество возможностей. +- Полностью поддерживает стандарты интерфейса `databse/sql`. +- Поддерживает постоянное соединение между потоками. + +##Примеры + +Во всех следующих разделах я буду использовать одинаковую структуру таблицы базы данных для различных баз данных, создать которую можно следующим SQL запросом: + + CREATE TABLE `userinfo` ( + `uid` INT(10) NOT NULL AUTO_INCREMENT, + `username` VARCHAR(64) NULL DEFAULT NULL, + `departname` VARCHAR(64) NULL DEFAULT NULL, + `created` DATE NULL DEFAULT NULL, + PRIMARY KEY (`uid`) + ); + +В следующем примере показано, как работать с базой данных на основе стандартов интерфейса `database/sql`. + + package main + + import ( + "database/sql" + "fmt" + + _ "github.com/go-sql-driver/mysql" + ) + + func main() { + db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8") + checkErr(err) + + // вставка + stmt, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?") + checkErr(err) + + res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") + checkErr(err) + + id, err := res.LastInsertId() + checkErr(err) + + fmt.Println(id) + // обновление + stmt, err = db.Prepare("update userinfo set username=? where uid=?") + checkErr(err) + + res, err = stmt.Exec("astaxieupdate", id) + checkErr(err) + + affect, err := res.RowsAffected() + checkErr(err) + + fmt.Println(affect) + + // запрос + rows, err := db.Query("SELECT * FROM userinfo") + checkErr(err) + + for rows.Next() { + var uid int + var username string + var department string + var created string + err = rows.Scan(&uid, &username, &department, &created) + checkErr(err) + fmt.Println(uid) + fmt.Println(username) + fmt.Println(department) + fmt.Println(created) + } + + // удаление + stmt, err = db.Prepare("delete from userinfo where uid=?") + checkErr(err) + + res, err = stmt.Exec(id) + checkErr(err) + + affect, err = res.RowsAffected() + checkErr(err) + + fmt.Println(affect) + + db.Close() + + } + + func checkErr(err error) { + if err != nil { + panic(err) + } + } + +Позвольте мне объяснить несколько важных функций: + +- `sql.Open()` - открывает зарегистрированный драйвер базы данных. В нашем примере это Go-MySQL-Driver. Второй аргумент в данной функции это DSN (Data Source Name), который определяет набор информации, необходимой для подключения к базе данных. Он поддерживает следующие форматы: + + User@UNIX(/path/to/socket)/dbname? charset = utf8 + user:password@tcp(localhost:5555)/dbname?charset=utf8 + user:password@/dbname + user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname + +- `db.Prepare()` - возвращает SQL операцию, которая будет выполнятся, так же возвращает статус исполнения после исполнения SQL запроса. +- `db.Query()` - исполняет SQL и возвращает результат в виде набора строк. +- `stmt.Exec()` - выполняет SQL, который подготовлен в Stmt. + +Обратите внимание, что мы используем формат «=?» для передачи аргументов. Это делается для предотвращения SQL-инъекций. + +## Ссылки + +- [Содержание](preface.md) +- Предыдущий раздел: [Интерфейс database/sql](05.1.md) +- Следующий раздел: [SQLite](05.3.md) + + diff --git a/ru/05.3.md b/ru/05.3.md new file mode 100644 index 00000000..b240c1e4 --- /dev/null +++ b/ru/05.3.md @@ -0,0 +1,113 @@ +# 5.3 SQLite + +SQLite это открытая встраиваемая реляционная база данных. Она автономна, не требует конфигурации и является полноценной СУБД. Ее основные характеристики это: высокая портативность, простота использования, эффективность и надежость. В большинстве случаев вам нужен только двоичный файл SQLite для создания, подключения и эксплуатации базы данных. Если вы ищите встраиваемое решение СУБД, вам стоит рассмотреть SQLite. По сути SQLite является версией Access с открыты исходным кодом. + +## Драйвера SQLite + +В Go есть множество драйверов баз данных для SQLite, но многие из них не поддерживают стандарты интерфейсов `database/sql`. + +- [https://github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) поддерживает `database/sql`, базируется на cgo. +- [https://github.com/feyeleanor/gosqlite3](https://github.com/feyeleanor/gosqlite3) не поддерживает `database/sql`, базируется на cgo. +- [https://github.com/phf/go-sqlite3](https://github.com/phf/go-sqlite3) не поддерживает `database/sql`, базируется на cgo. + +Первый драйвер является единственным, который поддерживает стандарты интерфейса `database/sql` в SQLite, поэтому я использую его в моих проектах. Поддержка стандартов позволит легко мигрировать на другую базу в будущем. + +##Примеры + +Создайте таблицу следующим запросом: + + CREATE TABLE `userinfo` ( + `uid` INTEGER PRIMARY KEY AUTOINCREMENT, + `username` VARCHAR(64) NULL, + `departname` VARCHAR(64) NULL, + `created` DATE NULL + ); + +Пример: + + package main + + import ( + "database/sql" + "fmt" + + _ "github.com/mattn/go-sqlite3" + ) + + func main() { + db, err := sql.Open("sqlite3", "./foo.db") + checkErr(err) + + // вставка + stmt, err := db.Prepare("INSERT INTO userinfo(username, departname, created) values(?,?,?)") + checkErr(err) + + res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") + checkErr(err) + + id, err := res.LastInsertId() + checkErr(err) + + fmt.Println(id) + // обновление + stmt, err = db.Prepare("update userinfo set username=? where uid=?") + checkErr(err) + + res, err = stmt.Exec("astaxieupdate", id) + checkErr(err) + + affect, err := res.RowsAffected() + checkErr(err) + + fmt.Println(affect) + + // запрос + rows, err := db.Query("SELECT * FROM userinfo") + checkErr(err) + + for rows.Next() { + var uid int + var username string + var department string + var created string + err = rows.Scan(&uid, &username, &department, &created) + checkErr(err) + fmt.Println(uid) + fmt.Println(username) + fmt.Println(department) + fmt.Println(created) + } + + // удаление + stmt, err = db.Prepare("delete from userinfo where uid=?") + checkErr(err) + + res, err = stmt.Exec(id) + checkErr(err) + + affect, err = res.RowsAffected() + checkErr(err) + + fmt.Println(affect) + + db.Close() + + } + + func checkErr(err error) { + if err != nil { + panic(err) + } + } + + +Вы наверняка заметили, что код очень похож на пример из предыдущего раздела, и мы изменили только имя зарегистрированного драйвера и вызвали `sql.Open` для соединения с SQLite по-другому. + +Ниже дана ссылка на инструмент управления SQLite: [http://sqliteadmin.orbmu2k.de/](http://sqliteadmin.orbmu2k.de/) + +## Ссылки + +- [Содержание](preface.md) +- Предыдущий раздел: [MySQL](05.2.md) +- Следующий раздел: [PostgreSQL](05.4.md) + diff --git a/ru/05.4.md b/ru/05.4.md new file mode 100644 index 00000000..29e4c43b --- /dev/null +++ b/ru/05.4.md @@ -0,0 +1,127 @@ +# 5.4 PostgreSQL + +PostgreSQL - свободная объектно-реляционная система управления базами данных доступная для множества платформ, включая Linux, FreeBSD, Solaris, Microsoft Windows и Mac OS X. Выпускается под собственной MIT-подобной лицензией. В отличие от MySQL, она разработана и позиционируется для использования в корпоративном сегменте, как Oracle. Так что PostgreSQL - это хороший выбор для использования в корпоративных проектах. + +## Драйвера PostgreSQL + +Есть множество драйверов баз данных для PostgreSQL, мы рассмотрим три из них: + +- [https://github.com/bmizerany/pq](https://github.com/bmizerany/pq) поддерживает `database/sql`, чистый код Go. +- [https://github.com/jbarham/gopgsqldriver](https://github.com/jbarham/gopgsqldriver) поддерживает `database/sql`, чистый код Go. +- [https://github.com/lxn/go-pgsql](https://github.com/lxn/go-pgsql) поддерживает `database/sql`, чистый код Go. + +Я буду использовать первый драйвер в последующих примерах. + +##Примеры + +Создайте таблицу следующим запросом: + + CREATE TABLE userinfo + ( + uid serial NOT NULL, + username character varying(100) NOT NULL, + departname character varying(500) NOT NULL, + Created date, + CONSTRAINT userinfo_pkey PRIMARY KEY (uid) + ) + WITH (OIDS=FALSE); + +Пример: + + package main + + import ( + "database/sql" + "fmt" + "time" + + _ "github.com/bmizerany/pq" + ) + + const ( + DB_USER = "postgres" + DB_PASSWORD = "postgres" + DB_NAME = "test" + ) + + func main() { + dbinfo := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", + DB_USER, DB_PASSWORD, DB_NAME) + db, err := sql.Open("postgres", dbinfo) + checkErr(err) + defer db.Close() + + fmt.Println("# Inserting values") + + var lastInsertId int + err = db.QueryRow("INSERT INTO userinfo(username,departname,created) VALUES($1,$2,$3) returning uid;", "astaxie", "研发部门", "2012-12-09").Scan(&lastInsertId) + checkErr(err) + fmt.Println("last inserted id =", lastInsertId) + + fmt.Println("# Updating") + stmt, err := db.Prepare("update userinfo set username=$1 where uid=$2") + checkErr(err) + + res, err := stmt.Exec("astaxieupdate", lastInsertId) + checkErr(err) + + affect, err := res.RowsAffected() + checkErr(err) + + fmt.Println(affect, "rows changed") + + fmt.Println("# Querying") + rows, err := db.Query("SELECT * FROM userinfo") + checkErr(err) + + for rows.Next() { + var uid int + var username string + var department string + var created time.Time + err = rows.Scan(&uid, &username, &department, &created) + checkErr(err) + fmt.Println("uid | username | department | created ") + fmt.Printf("%3v | %8v | %6v | %6v\n", uid, username, department, created) + } + + fmt.Println("# Deleting") + stmt, err = db.Prepare("delete from userinfo where uid=$1") + checkErr(err) + + res, err = stmt.Exec(lastInsertId) + checkErr(err) + + affect, err = res.RowsAffected() + checkErr(err) + + fmt.Println(affect, "rows changed") + } + + func checkErr(err error) { + if err != nil { + panic(err) + } + } + +Обратите внимание, что PostgreSQL использует формат подстановки вида `$1,$2`, в отличие от MySQL, который использует `?`, а так же используется другой формат DSN при вызове `sql.Open`. +Так же, имейте в виду, что Postgres не поддерживает `sql.Result.LastInsertId()`. +Таким образом, вместо этого, + + stmt, err := db.Prepare("INSERT INTO userinfo(username,departname,created) VALUES($1,$2,$3);") + res, err := stmt.Exec("astaxie", "研发部门", "2012-12-09") + fmt.Println(res.LastInsertId()) + +используйте `db.QueryRow()` и `.Scan()` для получения значения идентификатора последней вставки. + + err = db.QueryRow("INSERT INTO TABLE_NAME values($1) returning uid;", VALUE1").Scan(&lastInsertId) + fmt.Println(lastInsertId) + +## Ссылки + +- [Содержание](preface.md) +- Предыдущий раздел: [SQLite](05.3.md) +- Следующий раздел: [Разработка ORM на основе beedb](05.5.md) + + +