Запрос к базе данных в веб-приложении Go

Добро пожаловать в мой учебник! В прошлый раз мы создали экземпляр базы данных 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 и на моем личном сайте.

Оцените статью
Procodings.ru
Добавить комментарий