Mssql go что это
В предыдущих случаях сначала создавалась база данных, а затем в эту БД добавлялась таблица с помощью отдельных команд SQL. Но можно сразу совместить в одном скрипте несколько команд. В этом случае отдельные наборы команд называются пакетами (batch).
Каждый пакет состоит из одного или нескольких SQL-выражений, которые выполняются как оно целое. В качестве сигнала завершения пакета и выполнения его выражений служит команда GO .
Смысл разделения SQL-выражений на пакеты состоит в том, что одни выражения должны успешно выполниться до запуска других выражений. Например, при добавлении таблиц мы должны бы уверены, что была создана база данных, в которой мы собираемся создать таблицы.
Например, определим следующий скрипт:
CREATE DATABASE internetstore; GO USE internetstore; CREATE TABLE Customers ( Id INT PRIMARY KEY IDENTITY, Age INT DEFAULT 18, FirstName NVARCHAR(20) NOT NULL, LastName NVARCHAR(20) NOT NULL, Email VARCHAR(30) UNIQUE, Phone VARCHAR(20) UNIQUE ); CREATE TABLE Orders ( Id INT PRIMARY KEY IDENTITY, CustomerId INT, CreatedAt DATE, FOREIGN KEY (CustomerId) REFERENCES Customers (Id) ON DELETE CASCADE );
Вначале создается бд internetstore. Затем идет команда GO, которая сигнализирует, что можно выполнять следующий пакет выражений. И далее выполняется второй пакет, который добавляет в нее две таблицы — Customers и Orders.
Изучение SQL: ключевое слово GO
SQL позволяет объединять несколько команд в один пакет, который будет выполняться вместе и представлять одно целое. В качестве сигнала завершения пакета и его выполнения служит команда GO. Смысл в том, что одни команды должны выполниться после выполнения предыдущих. Тем самым мы можем строить последовательность команд.
Например, создание БД и добавления в нее таблиц:
CREATE DATABASE TestDB GO USE TestDB CREATE TABLE Users ( Id uniqueidintefier PRIMARY KEY IDENTITY, FirstName varchar(50) NOT NULL )
Go и SQL базы данных
Это первый туториал из серии материалов про работу с данными в веб приложениях.
В этом посте мы погрузимся в работу с SQL базами данных. Я объясню работу с стандартным пакетом database/sql , приведу примеры рабочих приложений и продемонстрирую несколько хитростей для более красивого структурирования кода.
Для начала, вам необходимо установить драйвера для работы с database/sql .
В этом посте мы будем использовать Postgres и замечательный драйвер pg. Тем не менее, весь код из этого туториала должен нормально работает и с другими драйверами, включая MySQL и SQLite. По ходу я буду указывать на специфические для Postgres моменты(которых будет не очень много).
$ go get github.com/lib/pq
Основы
Давайте напишем простое приложение для книжного магазина, которое будет выполнять CRUD операции с нашей таблицей для книг.
Прежде всего, нам нужно создать эту самую таблицу для книг, как показано ниже:
CREATE TABLE books ( isbn char(14) NOT NULL, title varchar(255) NOT NULL, author varchar(255) NOT NULL, price decimal(5,2) NOT NULL ); INSERT INTO books (isbn, title, author, price) VALUES ('978-1503261969', 'Emma', 'Jayne Austen', 9.44), ('978-1505255607', 'The Time Machine', 'H. G. Wells', 5.99), ('978-1503379640', 'The Prince', 'Niccolò Machiavelli', 6.99); ALTER TABLE books ADD PRIMARY KEY (isbn);
После этого, необходимо настроить свое Go окружение, создать папку bookstore и файл main.go:
$ cd $GOPATH/src $ mkdir bookstore && cd bookstore $ touch main.go
Давайте начнем с простого кода, который будет выполнять запрос SELECT * FROM books и выводить результат в консоль.
package main import ( _ "github.com/lib/pq" "database/sql" "fmt" "log" ) type Book struct < isbn string title string author string price float32 >func main() < db, err := sql.Open("postgres", "postgres://user:pass@localhost/bookstore") if err != nil < log.Fatal(err) >rows, err := db.Query("SELECT * FROM books") if err != nil < log.Fatal(err) >defer rows.Close() bks := make([]*Book, 0) for rows.Next() < bk := new(Book) err := rows.Scan(&bk.isbn, &bk.title, &bk.author, &bk.price) if err != nil < log.Fatal(err) >bks = append(bks, bk) > if err = rows.Err(); err != nil < log.Fatal(err) >for _, bk := range bks < fmt.Printf("%s, %s, %s, £%.2f\n", bk.isbn, bk.title, bk.author, bk.price) >>
В этом куске кода довольно много разных действий. Пройдемся по шагам и рассмотрим как все это работает.
Первый интересный момент, это импортирование пакета драйвера. Мы ничего не используем напрямую из этого пакета, а это означает, что компилятор Go ругнется, если вы попробуете импортировать пакет как обычно. Но нам необходим вызов функции init() этого пакета для того, чтобы драйвер зарегистрировал себя для использования в database/sql . Мы можем обойти это ограничение используя пустой алиас для импортируемого пакета. И это означает, что pq.init() будет выполняться, но благодаря алиасу мы избавимся от ошибок во время компиляции. Такая практика является стандартом для большинства SQL драйверов в Go.
Далее, мы определим наш тип для книги, в котором поля и типы полей будут зависеть от таблицы books. Тут стоит уточнить, что мы можем безопасно использовать string и float32 , так как мы указали NOT NULL для колонок в нашей таблице. Если в таблице есть поля, которые могут содержать NULL, то следует использовать типы sql.NullString и sql.NullFloat64 (тут можно глянуть рабочий пример). Вообще, если у вас есть возможность, старайтесь избегать полей, в которых могут быть NULL значения.
В функции main() мы инициализируем экземпляр sql.DB с помощью вызова sql.Open() . Мы указываем название нашего драйвера(в нашем случае это «postgres») и строку соединения, формат которой должен быть описан в документации к драйверу. Важное замечание, sql.DB это не соединение с базой, это некоторая абстракция над пулом соединений. Вы можете менять максимальное количество открытых и простаиваемых соединений в пуле с помощью методов db.SetMaxOpenConns() и db.SetMaxIdleConns() соответственно. И обратите внимание, что sql.DB можно безопасно использовать в конкурентных приложениях(которыми являются и веб-приложения).
Рассмотрим использованные стандартные паттерны:
- Мы получаем данные из таблицы, используя метод DB.Query() и присваиваем результат переменной rows . После этого, мы пользуемся defer rows.Close() , чтобы наверняка закрыть сет с результатами до выхода из функции. Очень важно не забывать закрывать сет. Все время, пока открыт сет, используемое соединение невозможно вернуть в пул. Если вдруг что-то пойдет не так и ваш сет не будет закрываться, то соединения в пуле могут быстро закончиться. Еще одна ловушка в том(и это оказалось для меня сюрпризом), что defer должен идти после проверки на ошибки DB.Query() . Если DB.Query() вернет ошибку, то вместо сета будет получен nil и при вызове rows.Close() стрельнет паника.
- Для итерации по строкам мы используем rows.Next() . Этот метод подготавливает строку для использования метода rows.Scan() . Не забывайте, что по завершению итерации по всем строкам сет автоматически закрывается и соединение возвращается в пул.
- Мы используем метод rows.Scan() , чтобы скопировать значения всех полей из строки в созданный нами экземпляр Book . Далее, мы проверяем была ли ошибка при работе метода rows.Scan() и добавляем новый экземпляр Book в слайс bks , который мы создали ранее.
- После итераций с помощью rows.Next() мы вызываем rows.Err() . Этот метод возвращает любую ошибку, которая произошла во время выполнения итераций. Этот момент достаточно важен, он позволяет убедиться, что мы прошлись по всему сету без ошибок.
Если все хорошо и мы нормально заполнили на слайс bks , то теперь мы итерируемся по нему и выводим информацию в консоль.
Если вы запустите код, то должно получиться что-то такое:
$ go run main.go 978-1503261969, Emma, Jayne Austen, £9.44 978-1505255607, The Time Machine, H. G. Wells, £5.99 978-1503379640, The Prince, Niccolò Machiavelli, £6.99
Использование в веб-приложении
Давайте изменим наш код, что бы получилось RESTful веб-приложение с 3 роутами:
- GET /books – Список всех книг в магазине
- GET /books/show – Информация о конкретной книге по ISBN
- POST /books/create – Добавление новой книги в магазин
Мы уже реализовали основную логику необходимую для GET /books . Давайте адаптируем ее для использования в HTTP хендлере booksIndex() нашего приложения.
package main import ( _ "github.com/lib/pq" "database/sql" "fmt" "log" "net/http" ) type Book struct < isbn string title string author string price float32 >var db *sql.DB func init() < var err error db, err = sql.Open("postgres", "postgres://user:pass@localhost/bookstore") if err != nil < log.Fatal(err) >if err = db.Ping(); err != nil < log.Fatal(err) >> func main() < http.HandleFunc("/books", booksIndex) http.ListenAndServe(":3000", nil) >func booksIndex(w http.ResponseWriter, r *http.Request) < if r.Method != "GET" < http.Error(w, http.StatusText(405), 405) return >rows, err := db.Query("SELECT * FROM books") if err != nil < http.Error(w, http.StatusText(500), 500) return >defer rows.Close() bks := make([]*Book, 0) for rows.Next() < bk := new(Book) err := rows.Scan(&bk.isbn, &bk.title, &bk.author, &bk.price) if err != nil < http.Error(w, http.StatusText(500), 500) return >bks = append(bks, bk) > if err = rows.Err(); err != nil < http.Error(w, http.StatusText(500), 500) return >for _, bk := range bks < fmt.Fprintf(w, "%s, %s, %s, £%.2f\n", bk.isbn, bk.title, bk.author, bk.price) >>
И в чем же тут отличия?
- Мы используемые функцию init() для настройки нашего пула соединений и указываем его в качестве значения глобальной переменной db . Мы используем глобальную переменную, которая предоставляет доступ к пулу соединений, чтобы иметь возможность использовать ее в разных HTTP хендлерах, но это не единственный возможный способ. Так как sql.Open() не проверяет соединение, то мы вызываем DB.Ping() , чтобы убедиться, что все работает нормально.
- В хендлере booksIndex мы возвращаем 405 Method Not Allowed ответ для всех не GET запросов. Дальше мы работаем с нашими данными. Все работает как в примере выше, за исключением что ошибки теперь возвращаются как HTTP ответ и нет выхода из программы. В результате мы записываем описания книг как обычный текст в http.ResponseWriter .
Запускаем приложение и делаем запрос к нему:
$ curl -i localhost:3000/books HTTP/1.1 200 OK Content-Length: 205 Content-Type: text/plain; charset=utf-8 978-1503261969, Emma, Jayne Austen, £9.44 978-1505255607, The Time Machine, H. G. Wells, £5.99 978-1503379640, The Prince, Niccolò Machiavelli, £6.99
Выборка одной строки
Для GET /books/show нам нужно реализовать получение одной книги из базы по ее ISBN, который будет указываться как параметр в запросе:
/books/show?isbn=978-1505255607
Для этого мы добавим хендлер bookShow() :
// . func main() < http.HandleFunc("/books", booksIndex) http.HandleFunc("/books/show", booksShow) http.ListenAndServe(":3000", nil) >// . func booksShow(w http.ResponseWriter, r *http.Request) < if r.Method != "GET" < http.Error(w, http.StatusText(405), 405) return >isbn := r.FormValue("isbn") if isbn == "" < http.Error(w, http.StatusText(400), 400) return >row := db.QueryRow("SELECT * FROM books WHERE isbn = $1", isbn) bk := new(Book) err := row.Scan(&bk.isbn, &bk.title, &bk.author, &bk.price) if err == sql.ErrNoRows < http.NotFound(w, r) return >else if err != nil < http.Error(w, http.StatusText(500), 500) return >fmt.Fprintf(w, "%s, %s, %s, £%.2f\n", bk.isbn, bk.title, bk.author, bk.price) >
Первым делом, в обработчике проверяется действительно ли пришел GET запрос.
После этого, мы используем метод Request.FormValue() для получения параметров из строки запроса. В случае если нет необходимых параметров, то мы получаем пустую строку и возвращаем ответ 400 Bad Request .
Тут мы подходим к самому интересному. Метод DB.QueryRow() работает аналогично DB.Query() , но получает только одну строку.
Так как у нас есть некоторый ненадежный данные от пользователя(переменная isbn ), то в нашем SQL запросе нужно использовать плейсхолдеры для параметров, сами значения мы указываем как аргументы после строки запроса.
db.QueryRow("SELECT * FROM books WHERE isbn = $1", isbn)
Если чуть углубиться, то можно обнаружить, что db.QueryRow (а также db.Query() и db.Exec() ) создают «подготовленные выражения»(prepared statement) в базе данных и выполняют запросы, подставляя параметры в плейсхолдеры этих выражений. Это означает, что все три метода безопасны в плане SQL-инъекций, если пользоваться ими правильно. Вот что говорит нам википедия:
Подготовленные выражения устойчивы к SQL инъекциям, поскольку значения параметров, которые передаются позже с использованием другого протокола, не нужно ескейпить. Если оригинальное выражение построено не на основании внешнего ввода, то инъекции не может произойти. В зависимости от базы данных, плейсхолдеры указываются по разному. В Postgres используется нотация $N , но в MySQL, SQL Server и в некоторых других используется символ ? .
Окей, давайте вернемся к нашему коду.
После получения строки с помощью DB.QueryRow() , мы используем row.Scan() для копирования значений в наш новы объект Book . Важно, что мы не узнаем про ошибки выполнения запроса в методе DB.QueryRow() , пока не вызовем метод row.Scan() .
Если ваш запрос не нашел ни одной строки, то вызов row.Scan() вернет ошибку sql.ErrNoRows . Мы выполняем проверку на эту ошибку и, если ничего не найдено, возвращаем 404 Not Found ответ. Если возникают другие ошибку, то возвращаем 500 Internal Server Error .
Если все хорошо, то мы записываем в http.ResponseWriter информацию по запрашиваемой книге.
$ curl -i localhost:3000/books/show?isbn=978-1505255607 HTTP/1.1 200 OK Content-Length: 54 Content-Type: text/plain; charset=utf-8 978-1505255607, The Time Machine, H. G. Wells, £5.99
Если вы попробуете указывать разные значения ISBN, то можете увидеть как меняется результат ответа. В случае неправильного запроса, вы должны получить соответствующий код ошибки.
Выполнение выражений
Для нашего роута POST /books/create мы создадим хендлер booksCreate() , в котором будем использовать DB.Exec() для выполнения выражения INSERT . Вы можете использовать схожий подход для UPDATE , DELETE или других операций, которые не подразумевают получение результата в виде строк таблиц.
Код выглядит так:
// . import ( _ "github.com/lib/pq" "database/sql" "fmt" "log" "net/http" "strconv" ) // . func main() < http.HandleFunc("/books", booksIndex) http.HandleFunc("/books/show", booksShow) http.HandleFunc("/books/create", booksCreate) http.ListenAndServe(":3000", nil) >// . func booksCreate(w http.ResponseWriter, r *http.Request) < if r.Method != "POST" < http.Error(w, http.StatusText(405), 405) return >isbn := r.FormValue("isbn") title := r.FormValue("title") author := r.FormValue("author") if isbn == "" || title == "" || author == "" < http.Error(w, http.StatusText(400), 400) return >price, err := strconv.ParseFloat(r.FormValue("price"), 32) if err != nil < http.Error(w, http.StatusText(400), 400) return >result, err := db.Exec("INSERT INTO books VALUES($1, $2, $3, $4)", isbn, title, author, price) if err != nil < http.Error(w, http.StatusText(500), 500) return >rowsAffected, err := result.RowsAffected() if err != nil < http.Error(w, http.StatusText(500), 500) return >fmt.Fprintf(w, "Book %s created successfully (%d row affected)\n", isbn, rowsAffected) >
Думаю, вы уже находите много знакомого в этом коде.
В хендлере booksCreate() мы проверяем, действительно ли пришел POST запрос и получаем параметры из запроса с помощью request.FormValue() . Мы проверяем наличие всех необходимых параметров, а цену еще и конвертируем в float с помощью strconv.ParseFloat() .
После этого, мы используем db.Exec() с указанием полученных парметров, аналогично как мы делали это ранее. Важно, что DB.Exec() , DB.Query() и DB.QueryRow() , — это функции которое могут принимать переменное число параметров.
Метод db.Exec() в качестве результата возвращает объект, который удовлетворяет интерфейс sql.Result. При необходимости, этот результат можно использовать или не учитывать, используя пустой идентификатор.
Интерфейс sql.Result предоставляет метод LastInsertId() , который используется для получения последнего значения автоинкремента. Также, можно использовать метод RowsAffected() , который возвращает число строк, затронутых в запросе(удаленных, обновленных, новых и т.д.). В нашем случае, используется второй описанный метод, мы получаем количество строк и формируем сообщение.
Стоит отметить, что не все драйвера поддерживают методы LastInsertId() и RowsAffected() и при их вызове вы получите ошибку. К примеру, драйвер pq не поддерживает метод LastInsertId() и, если вам необходим подобный функционал, то прийдется использовать подход вроде этого.
Давайте проверим роут /books/create с передачей необходимых параметров в POST:
$ curl -i -X POST -d "isbn=978-1470184841&title=Metamorphosis&author=Franz Kafka&price=5.90" \ localhost:3000/books/create HTTP/1.1 200 OK Content-Length: 58 Content-Type: text/plain; charset=utf-8 Book 978-1470184841 created successfully (1 row affected)
Использование DB.Prepare()
Возможно, вам стало интересно, почему не используется DB.Prepare() .
Как я объяснял выше, методы методы DB.Query() , DB.Exec() и DB.QueryRow() создают подготовленные выражения в базе данных, запускают их с указанными параметрами и затем закрывают(точнее деаллоцируют) эти выражения.
Недостатки использования такого подхода очевидны. У нас аж три обращения к базе данных на каждый HTTP запрос. Чтобы избежать этого, мы можем воспользоваться DB.Prepare() (например, в функции init() ).
Но на самом деле все не так просто. Подготовленные выражения работают только с конкретной сессией базы данных. Когда сессия заканчивается, подготовленные выражения необходимо создавать заново. Это может произойти, если есть большой таймаут между запросами или вам пришлось рестартовать базу.
Для веб-приложений, в которых задержка имеет большое значение, возможно стоит заморочиться и добавить некоторый мониторинг, который будет реинициализировать подготовленные выражения. Но в таком приложении, как наше, это слишком большой оверхед и нам достаточно использовать DB.Query() как есть.
В этом треде описанная проблема обсуждается несколько глубже.
Рефакторинг
В данный момент, вся наша логика работы с базой перемешана с обработкой HTTP запросов. И это отличный подвод для рефакторинга, который позволит упростить наш код и сделать его более логичным.
Но этот туториал уже и так достаточно большой, поэтому оставим это для следующего поста — «Practical Persistence in Go: Organising Database Access» (в скором времени).
Дополнительные инструменты
Пакет Sqlx от Jason Moiron предоставляет расширенный функционал для работы с базой, включая именованные плейсхолдеры, и автоматический маршалинг результатов запроса в структуры.
Если вам интересны более ORM-ориентированные подходы, то взгляните в сторону Modl того же автора или gorp от James Cooper.
Пакет null может помочь вам при работе с null-значениями.
Позже, я нашел отличный туториал: «go-database-sql.org». Обратите особое внимание на раздел про сюрпризы и ограничения.
Можно по подробнее о команде GO?
Можно: GO — это вообще не SQL, это — специфика MS SQL. Ну, а USE существует практически в любом распространенном диалекте и предназначена для «выбора» БД, для использования в последующих запросах.
Вызывать GO в конце этого запроса — вряд ли. хотя, смысл может появиться, если до и/или после него делается что-то еще, чего мы не видим 🙂