Добро пожаловать в мой учебник! В прошлый раз мы создали экземпляр базы данных PostgreSQL и подключили его к нашему бэкенду. В этот раз мы будем использовать базу данных и научимся делать к ней запросы.
Краткое введение в базы данных SQL
Хотя мы установили соединение с базой данных, есть вероятность, что вы все еще не понимаете, как работает база данных. Похожа ли она на проводник файлов на моем компьютере? Как она хранит данные? Что означает запрос к базе данных? Я думаю, что синтаксис SQL становится намного проще, если мы понимаем, как работает база данных.
PostgreSQL является примером того, что называется базой данных SQL. База данных SQL — это тип базы данных, которая хранит данные в формате определенной схемы. Проще говоря, это прославленная электронная таблица, где все данные хранятся в виде строки в таблице. База данных SQL обычно имеет несколько схем (таблиц), которые определяют формат данных.
В нашем примере нам нужна таблица, в которой хранятся данные о книгах. Определение нашей структуры в Go выглядит следующим образом:
type BookData struct {
title string
author string
isbn string
}
Каждая книга имеет название, автора и ISBN. Таблица в нашей базе данных будет выглядеть примерно так:
Название | Автор | ISBN |
---|---|---|
Инструменты титанов | Тим Феррисс | 9781328683786 |
Сиддхартха | Герман Гессе | 9781529024043 |
Атомные привычки | Джеймс Клир | 9780735211292 |
В этом руководстве мы переопределим схему и структуру, чтобы они лучше соответствовали нашим потребностям.
Простой синтаксис SQL
Теперь, когда мы знаем, как база данных хранит данные, пришло время узнать, как ее использовать. Если вы не используете инструмент GUI для упрощения операций, вам придется использовать специальный язык для работы с базой данных. Даже если у вас есть инструмент GUI, полезно знать, как использовать SQL.
SQL — это сокращение от Structured Query Language (язык структурированных запросов), и он является основным способом манипулирования базой данных. Синтаксис языка похож на английский, поэтому очень легко понять, что делает каждая строка. Акт запроса и редактирования данных называется запросом.
Давайте начнем с изучения нескольких наиболее важных команд. Также, да, это идиоматично — набирать SQL-команды прописными буквами, чтобы отличить их от данных. Для большей организованности и удобочитаемости вы можете разбить запрос на несколько строк.
CREATE DATABASE myDatabase;
Эта команда создает базу данных. Мы уже создали базу данных в предыдущем уроке, поэтому этот шаг можно пропустить.
CREATE TABLE myTable (
column1 dataType columnConstraints,
column2 dataType columnConstraints,
column3 dataType columnConstraints,
tableContrainsts
);
Так вы создадите таблицу, подобную той, что описана в первой части этого урока.
-
Вы выбираете имя таблицы.
-
Затем вы должны указать имена столбцов, типы данных и ограничения.
Ограничения — это параметры, которые вы назначаете каждому столбцу. Например, ограничение NOT NULL
обязывает столбец иметь не нулевые значения. Ограничение PRIMARY KEY
назначит столбцу уникальные значения, по которым можно идентифицировать каждую строку.
SELECT column1 FROM myTable WHERE condition;
Так вы выбираете определенные данные из таблицы. Команда не требует пояснений: вы выбираете столбец или набор столбцов из таблицы, для которых выполняется условие.
INSERT INTO myTable (column1, column2, ...)
VALUES (value1, value2, ...);
Так вы вставляете данные в определенные столбцы. value1
вставляется в column1
, и наоборот.
UPDATE myTable
SET column1 = value1, column2 = value2, ...
WHERE condition;
Так можно обновить существующие значения в заданных столбцах.
DELETE myTable WHERE condition;
Так вы удаляете существующие значения, соответствующие условию.
Встать на место
В настоящее время в нашей базе данных ничего нет. Мы должны подготовить ее, прежде чем делать что-либо в нашем бэкенде. Давайте сначала создадим нашу таблицу.
CREATE TABLE books (
isbn VARCHAR(13) PRIMARY KEY,
title VARCHAR(100) NOT NULL,
author VARCHAR(50) NOT NULL
);
Этот скрипт создает таблицу books
с колонками isbn
, title
и author
.
-
VARCHAR(length)
— это тип, используемый строками с максимальной длиной.VARCHAR(13)
будет принимать только строки с 13 символами или меньше. Мы будем использовать ISBN 13 без дефисов в середине, поэтому выделение только 13 символов звучит хорошо. -
PRIMARY KEY
— это ограничение, применяемое к столбцам для уникальной идентификации строки в таблице. Все элементы в столбцеPRIMARY KEY
должны быть уникальными и не должны быть пустыми. -
NOT NULL
— это ограничение, применяемое для того, чтобы данные в столбце не были пустыми.
Итак, мы создали таблицу. Мы могли бы засеять базу данных, вставляя новые строки прямо сейчас, но вместо этого мы сделаем это в нашем бэкенде. Если вы хотите добавить несколько книг прямо сейчас, вы можете использовать этот скрипт:
INSERT INTO books (isbn, title, author)
VALUES ("9781328683786", "Teen Titans", "Tim Ferriss");
INSERT INTO books (isbn, title, author)
VALUES ("9781529024043", "Siddhartha", "Herman Hesse");
INSERT INTO books (isbn, title, author)
VALUES ("9780735211292", "Atomic Habits", "James Clear");
Упс, мы допустили опечатку в нашей первой книге. Название должно быть Tools of Titans, а не Teen Titans! Чтобы исправить это, мы можем использовать этот запрос:
UPDATE books
SET title = "Tools of Titans"
WHERE isbn = "9781328683786";
Довольно просто, верно?
Выполнение запросов в Go
Хорошая работа по руководству до сих пор! Теперь мы узнаем, как выполнить эти запросы из нашего бэкенда. Давайте вернемся к нашему коду.
// main.go
package main
import (
"database/sql"
"fmt"
"log"
"github.com/gorilla/mux"
_"github.com/lib/pq"
)
var DB *sql.DB
const (
HOST = "localhost"
PORT = 5432
USER = "jacob"
PASSWORD = "password"
DBNAME = "bookstoreDB"
)
func main() {
connString := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
HOST, PORT, USER, PASSWORD, DBNAME,
)
DB, err := sql.Open("postgres", connString)
if err != nil {
log.Fatal(err)
}
defer DB.Close()
// mux definition and route registration (from last tutorial)
r := mux.NewRouter()
r.HandleFunc("/", homeHandler)
booksSubR := r.PathPrefix("/books").Subrouter()
booksSubR.HandleFunc("/all", AllHandler).Methods(http.MethodGet)
booksSubR.HandleFunc("/{isbn}", IspnHandler).Methods(http.MethodGet)
booksSubR.HandleFunc("/new", NewHandler).Methods(http.MethodPost)
booksSubR.HandleFunc("/update", UpdateHandler).Methods(http.MethodPut)
booksSubR.HandleFunc("/delete/{isbn}", DeleteIspnHandler).Methods(http.MethodDelete)
log.Fatal(http.ListenAndServe(":8090", r))
}
Мы должны определить обработчики, которые срабатывают при достижении соответствующей конечной точки. Чтобы все было не так запутанно, давайте создадим новый файл controllers.go
и напишем в нем наши обработчики. Если вам интересно, я использую термины контроллеры и обработчики как взаимозаменяемые.
Прежде чем мы создадим controllers.go
, нам нужно создать определенную модель, которая отражает схему базы данных. Эти модели должны находиться внутри models.go
.
// models.go
package main
type Book struct {
Isbn string `json:"isbn"`
Title string `json:"title"`
Author string `json:"author"`
}
Теперь, когда у нас определена модель Book
, мы можем использовать ее в нашем файле controllers.go
.
// controllers.go
package main
import (
"database/sql"
"net/http"
)
func AllHandler(w http.ResponseWriter, r *http.Request) {
var books = make([]Book, 0)
results, err := DB.Query("SELECT * FROM books;")
if err != nil {
log.Println("failed to execute query", err)
w.WriteHeader(500)
return
}
for results.Next() {
var b Book
err = results.Scan(&b.isbn, &b.title, &b.author)
if err != nil {
log.Println("failed to scan", err)
w.WriteHeader(500)
return
}
books = append(books, b)
}
json.NewEncoder(w).Encode(books)
}
// other handler definitions go here
Это пример того, как функция контроллера может запросить базу данных, разобрать результаты в формате JSON, а затем отправить их обратно пользователю.
var books = make([]Book, 0)
Мы создаем фрагмент объектов Book
. Он используется для хранения результатов запроса.
results, err := DB.Query("SELECT * FROM books;")
if err != nil {
log.Println("failed to execute query", err)
w.WriteHeader(500)
return
}
Мы не видели этот код раньше. Это фрагмент, который выполняет запрос.
-
Мы создали
DB
в функцииmain
. Мы используем методQuery
наDB
для выполнения запроса. -
Оператор запроса представляет собой строку:
SELECT * FROM books;
. Звездочка обозначает, что мы хотим выбрать все записи из таблицыbooks
. -
Метод возвращает
results
, который имеет типsql.Rows
. Здесь хранится результат запроса, строка за строкой. Одного этого недостаточно, нам нужно отсканировать содержимое в другую переменную. -
Если произойдет ошибка, мы ответим HTTP-кодом ошибки 500, что означает проблему на стороне сервера.
for results.Next() {
var b Book
err = results.Scan(&b.isbn, &b.title, &b.author)
if err != nil {
log.Println("failed to scan", err)
w.WriteHeader(500)
return
}
books = append(books, b)
}
Здесь мы сохраняем результаты запроса.
-
results.Next
выполняет итерацию по строкамresults
. Поскольку мы запрашивали все записи, есть много строк, которые нам нужно просмотреть. -
results.Scan
сканирует данные строки в целевое назначение. Здесь мы сканируем в экземплярBook
под названиемb
. -
После завершения сканирования мы добавляем
b
к фрагментуbooks
. -
Если произошла ошибка, мы отвечаем HTTP-кодом ошибки 500, что означает проблему на стороне сервера.
json.NewEncoder(w).Encode(books)
Это очень просто. Он кодирует фрагмент books
в JSON, а затем отправляет его обратно пользователю.
Обработка POST-запросов
Нам нужно обрабатывать запросы на добавление новых книг в нашу базу данных. Основы те же, что и в предыдущем примере, но метод запроса будет несколько иным.
// controllers.go
package main
import (
"database/sql"
"net/http"
)
func NewHandler(w http.ResponseWriter, r *http.Request) {
var b Book
err := json.NewDecoder(r.Body).Decode(&b)
if err != nil {
log.Println("error while decoding r.Body", err)
w.WriteHeader(400)
return
}
queryStmt := `
INSERT INTO books (isbn, title, author)
VALUES ($1, $2, $3)
RETURNING isbn;
`
var isbn string
err := DB.QueryRow(queryStmt, &b.isbn, &b.title, &b.author).Scan(&isbn)
if err != nil {
log.Println("failed to execute query", err)
w.WriteHeader(500)
return
}
log.Println("%s has been added to the database", isbn)
}
Код похож на наш первый пример, но он немного отличается.
var b Book
err := json.NewDecoder(r.Body).Decode(&b)
if err != nil {
log.Println("error while decoding r.Body", err)
w.WriteHeader(400)
return
}
-
Поскольку пользователь отправляет объект JSON в качестве тела запроса, нам нужно маршалировать его в удобном для Go формате.
-
Мы создаем экземпляр
Book
под названиемb
и декодируем в него тело запроса. -
Если возникает ошибка, это означает, что тело запроса недействительно, поэтому мы возвращаем код ошибки 400.
queryStmt := `
INSERT INTO books (isbn, title, author)
VALUES ($1, $2, $3)
RETURNING isbn;
`
var isbn string
err := DB.QueryRow(queryStmt, &b.isbn, &b.title, &b.author).Scan(&isbn)
if err != nil {
log.Println("failed to execute query", err)
w.WriteHeader(500)
return
}
log.Println("%s has been added to the database", isbn)
Вот как мы обрабатываем запросы INSERT
или любые запросы, которые требуют динамического создания новых запросов, например UPDATE
.
-
queryStmt
— это оператор запроса. Обратите внимание на вторую строку — в ней есть$1, $2, $3
. Это заполнители для данных, которые мы вставим позже. Поскольку у одного человекаINSERT
будет отличаться от другого, мы не можем иметь просто статичный оператор запроса. Как мы узнаем, какую книгу хотят добавить пользователи, прежде чем они скажут нам об этом? -
Строка
RETURNING isbn;
в конце вернетisbn
добавленной книги. Это полезные данные, и мы можем использовать их для любых целей. -
Исходя из оператора запроса, мы точно знаем, что после запроса будет возвращена только одна строка. Вот почему мы используем
sql.QueryRow
вместоsql.Query
. -
В
DB.QueryRow
мы передаем оператор запроса и данные для замены$1, $2, $3
. Порядок имеет значение, поэтому в нашем примере&b.isbn
заменит$1
, и наоборот.&b.isbn
,&b.title
и&b.author
— это поляb
, которые мы расшифровали ранее. -
Если возникает ошибка, мы отвечаем HTTP-кодом ошибки 500, что означает проблему на стороне сервера.
Отсюда вы сможете понять, как написать другие функции контроллера. Желаю удачи! Посмотрите эти ресурсы, которые помогут вам:
-
sql package — database/sql — pkg.go.dev
-
Учебник по PostgreSQL
Заключение
Поздравляем вас с завершением этой части учебника. Теперь у нас есть базовый REST API. Запустите код, запустите ваш любимый клиент (мой — cURL и Postman) и начинайте тестировать! Не стесняйтесь добавлять новые контроллеры и расширять их. Есть и более продвинутые темы по созданию API, но для учебных целей этого будет достаточно. Скоро я перейду к более продвинутым темам, так что следите за новостями!
Вы также можете прочитать этот пост на Medium и на моем личном сайте.