В этом уроке мы научимся разрабатывать веб-приложение, которое выполняет различные операции над данными в хранилище: создание, чтение, обновление и удаление. Для веб-разработки это важная тема, потому что почти все веб-приложения выполняют эти операции.
Представим, что вам нужно разработать систему управления сотрудников компании. Если вы неправильно организуете операции для работы с пользователями, то вам и вашим коллегам будет тяжело поддерживать и развивать систему.
CRUD-операции
Любой объект в хранилище можно представить в виде ресурса. С ресурсом можно работать посредством следующих операций:
- Создание — create
- Чтение — read
- Обновление — update
- Удаление — delete
По первым буквам английских названий этих операций образуется акроним CRUD. Так как в веб-приложениях взаимодействие происходит по HTTP, нужно сопоставить операции CRUD с этим протоколом. Обычно для каждой операции используют свой HTTP-метод:
- C (create) — POST
- R (read) — GET
- U (update) — PATCH/PUT
- D (delete) — DELETE
Например, мы разрабатываем систему управления сотрудников компании. Ресурс в данной системе — сотрудник. Путь до него мы определяем как /employees
, а CRUD будет выглядеть следующим образом:
- C (create) — POST /employees
- R (read) — GET /employees или GET /employees/:id
- U (update) — PATCH/PUT /employees/:id
- D (delete) — DELETE /employees/:id
Каждый метод содержит идентификатор сотрудника, кроме метода создания. Это связано с тем, что когда сотрудник создается, его еще нет в хранилище. Поэтому его идентификатор неизвестен.
Мы определили, как будут выглядеть CRUD-операции на протоколе HTTP. Теперь рассмотрим, как реализовать это в микрофреймворке Fiber.
CRUD-операции в Fiber
В Fiber каждый HTTP-метод представлен своей функцией. Чтобы реализовать CRUD-операции в веб-приложении, мы будем использовать следующие функции:
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
)
func main() {
webApp := fiber.New()
// Создание сотрудника
webApp.Post("/employees", ...)
// Получение сотрудника
webApp.Get("/employees/:id", ...)
// Обновление сотрудника
webApp.Patch("/employees/:id", ...)
// Удаление сотрудника
webApp.Delete("/employees/:id", ...)
logrus.Fatal(webApp.Listen(":80"))
}
Для каждой CRUD-операции описывается уникальный обработчик. Каждый обработчик выполняет конкретную маленькую задачу и не должен содержать в себе логики, которая не относится к этой операции. Такое построение веб-приложения позволит легко масштабировать и поддерживать код.
Для простоты представим, что объект сотрудника содержит только идентификатор, электронную почту и роль. Хранить данные будем в оперативной памяти приложения с помощью структуры данных map:
type Employee struct{
ID string
Email string
Role string
}
type MemoryEmployeeStorage struct{
employees map[string]Employee
}
Разберем каждую CRUD-операцию подробнее.
Создание сотрудника
Изначально в хранилище нет сотрудников, поэтому первым делом нам нужно реализовать метод его создания. Для этого используется метод POST /employees
, в котором передаются все данные нового сотрудника:
package main
import (
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
// Создание сотрудника
type (
CreateEmployeeRequest struct {
Email string `json:"email"`
Role string `json:"role"`
}
CreateEmployeeResponse struct {
ID string `json:"id"`
}
)
// Хранилище
type (
Employee struct {
ID string
Email string
Role string
}
EmployeeStorageInMemory struct {
employees map[string]Employee
}
)
func (s *EmployeeStorageInMemory) Create(empl Employee) (string, error) {
// Генерируем ID для сотрудника
empl.ID = uuid.New().String()
s.employees[empl.ID] = empl
return empl.ID, nil
}
func main() {
webApp := fiber.New()
storage := &EmployeeStorageInMemory{
employees: make(map[string]Employee),
}
// Создание сотрудника
webApp.Post("/employees", func(c *fiber.Ctx) error {
// Парсим JSON-тело запроса в объект CreateEmployeeRequest
var req CreateEmployeeRequest
if err := c.BodyParser(&req); err != nil {
return fmt.Errorf("body parser: %w", err)
}
// Сохраняем объект сотрудника в хранилище
// Метод Create возвращает ID созданного сотрудника
id, err := storage.Create(Employee{
Email: req.Email,
Role: req.Role,
})
if err != nil {
return fmt.Errorf("create in storage: %w", err)
}
// Возвращаем ID сотрудника JSON-строкой в теле ответа
return c.JSON(CreateEmployeeResponse{ID: id})
})
logrus.Fatal(webApp.Listen(":80"))
}
Запускаем веб-приложение и отправляем запрос на создание нового сотрудника:
curl --location --request POST 'http://localhost/employees' \
--header 'Content-Type: application/json' \
--data-raw '{"email": "john@corp.com", "role": "salesman"}'
В ответ получаем идентификатор созданного сотрудника:
HTTP/1.1 200 OK
{"id":"fad6ff8c-5e6a-4545-9b41-3dc330ce146f"}
Мы отправили POST /employees
запрос с данными нового сотрудника. Веб-приложение определило обработчик этого запроса, прочитало тело запроса и сохранило данные нового сотрудника в оперативной памяти. В методе создания нового сотрудника в хранилище storage.Create()
генерируется идентификатор сотрудника, по которому в будущем будет происходить поиск сотрудника в хранилище.
Идентификатор сотрудника представлен в виде UUID — универсальный уникальный идентификатор. Это гарантирует, что идентификаторы не будут повторяться при большом количестве сотрудников. Генерация UUID стандартизирована, и в Go есть готовая библиотека для генерации такого идентификатора. Для этого используется функция uuid.New().String()
.
Мы можем создавать сотрудников, но пока не можем получить то, что мы создали. Чтобы решить этот недочет, нам нужен метод чтения.
Чтение данных сотрудников
Метод чтения разделяется на два типа:
- Получение всех сотрудников. Для этого используется метод
GET /employees
- Получение конкретного сотрудника. Для этого используется метод
GET /employees/:id
, где:id
— это идентификатор сотрудника
Для этих операций мы описываем новые объекты ответов:
// Чтение сотрудника
type (
ListEmployeesResponse struct {
Employees []EmployeePayload `json:"employees"`
}
GetEmployeeResponse struct {
EmployeePayload
}
EmployeePayload struct {
ID string `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
}
)
// Получение списка сотрудников
webApp.Get("/employees", func(c *fiber.Ctx) error {
// Получаем список всех сотрудников из хранилища
employees := storage.List()
// Формируем ответ
resp := ListEmployeesResponse{
Employees: make([]EmployeePayload, len(employees)),
}
for i, empl := range employees {
resp.Employees[i] = EmployeePayload(empl)
}
// Возвращаем список сотрудников JSON-строкой в теле ответа
return c.JSON(resp)
})
// Получение одного сотрудника
webApp.Get("/employees/:id", func(c *fiber.Ctx) error {
empl, err := storage.Get(c.Params("id"))
if err != nil {
return fiber.ErrNotFound
}
// Возвращаем данные сотрудника JSON-строкой в теле ответа
return c.JSON(GetEmployeeResponse{EmployeePayload(empl)})
})
Также мы добавляем две функции в хранилище, чтобы получить данные сотрудников:
func (s *EmployeeStorageInMemory) List() []Employee {
// Инициализируем массив с размером равным количеству
// всех сотрудников в хранилище
employees := make([]Employee, 0, len(s.employees))
for _, empl := range s.employees {
employees = append(employees, empl)
}
return employees
}
func (s *EmployeeStorageInMemory) Get(id string) (Employee, error) {
empl, ok := s.employees[id]
if !ok {
// Возвращаем ошибку, если сотрудника с таким
// идентификатором не существует
return Employee{}, errors.New("employee not found")
}
return empl, nil
}
Запускаем веб-приложения и попробуем прочесть данные. Чтобы протестировать получение сотрудников, нам нужно сначала создать пару записей:
curl --location --request POST 'http://localhost/employees' \
--header 'Content-Type: application/json' \
--data-raw '{"email": "john@corp.com", "role": "salesman"}'
curl --location --request POST 'http://localhost/employees' \
--header 'Content-Type: application/json' \
--data-raw '{"email": "jane@corp.com", "role": "CEO"}'
На запросы получаем ответы соответственно:
{"id":"e8f3b261-8b0b-4fb2-b5dd-8714b72a01a6"}%
{"id":"6972cc2c-50e0-400c-808f-deb63f37c4f1"}%
Теперь попробуем получить список всех сотрудников:
curl --location --request GET 'http://localhost/employees'
{
"employees":[
{
"id":"e8f3b261-8b0b-4fb2-b5dd-8714b72a01a6",
"email":"john@corp.com",
"role":"salesman"
},
{
"id":"6972cc2c-50e0-400c-808f-deb63f37c4f1",
"email":"jane@corp.com",
"role":"CEO"
}
]
}
Также проверим метод получения одного сотрудника:
curl --location --request GET 'http://localhost/employees/6972cc2c-50e0-400c-808f-deb63f37c4f1'
{
"id":"6972cc2c-50e0-400c-808f-deb63f37c4f1",
"email":"jane@corp.com",
"role":"CEO"
}
Мы реализовали обработчик запросов на чтение данных сотрудников. Когда мы отправили запрос GET /employees
, веб-приложение определило обработчик и вернуло все записи из хранилища в JSON-виде.
Если указать идентификатор сотрудника при запросе GET /employees/:id
, то веб-приложение вернет данные только одного сотрудника. Мы также учли, что в хранилище может не существовать сотрудник с таким идентификатором. В этом случае веб-приложение вернет ошибку 404.
Обновление сотрудника
Мы научились создавать и читать данные сотрудников, но пока не умеем их обновлять. Например, сотрудник может перейти на другую должность в компании. В этом случае будет отправляться запрос на обновление PATCH /employees/:id
с новым значением поля Role
.
Реализуем обработчик обновления. Начнем с описания запроса на обновление:
// Обновление сотрудников
type (
UpdateEmployeeRequest struct {
Email string `json:"email"`
Role string `json:"role"`
}
)
Теперь опишем обработчик запроса на обновление:
// Получение списка сотрудников
webApp.Patch("/employees/:id", func(c *fiber.Ctx) error {
// Парсим JSON-тело запроса в объект UpdateEmployeeRequest
var req UpdateEmployeeRequest
if err := c.BodyParser(&req); err != nil {
return fmt.Errorf("body parser: %w", err)
}
// Обновляем данные сотрудника в хранилище. Эта функция может вернуть ошибку,
// если сотрудника с таким идентификатором не существует.
err = storage.Update(c.Params("id"), req.Email, req.Role)
if err != nil {
return fmt.Errorf("update: %w", err)
}
return nil
})
Далее опишем метод обновления сотрудника в хранилище:
func (s *EmployeeStorageInMemory) Update(id, email, role string) error {
empl, ok := s.employees[id]
if !ok {
// Возвращаем ошибку, если сотрудника с таким
// идентификатором не существует
return errors.New("employee not found")
}
// Обновляем электронную почту сотрудника,
// если новое значение было передано
if email != "" {
empl.Email = email
}
// Обновляем роль сотрудника,
// если новое значение было передано
if role != "" {
empl.Role = role
}
s.employees[empl.ID] = empl
return nil
}
Запускаем веб-приложение и создаем нового сотрудника:
curl --location --request POST 'http://localhost/employees' \
--header 'Content-Type: application/json' \
--data-raw '{"email": "john@corp.com", "role": "salesman"}'
{"id":"4e98eb9c-13c1-41a2-8b1e-945387df98cb"}
Теперь обновим данные этого сотрудника:
curl --location --request PATCH 'http://localhost/employees/4e98eb9c-13c1-41a2-8b1e-945387df98cb' \
--header 'Content-Type: application/json' \
--data-raw '{"email": "johnv2@corp.com", "role": "CTO"}'
В ответе получаем пустую строку с кодом 204 No Content, что означает — запрос обработан успешно.
Попробуем получить данные этого сотрудника:
curl --location --request GET 'http://localhost/employees/4e98eb9c-13c1-41a2-8b1e-945387df98cb'
И мы видим, что данные сотрудника обновились:
{
"id":"4e98eb9c-13c1-41a2-8b1e-945387df98cb",
"email":"johnv2@corp.com",
"role":"CTO"
}
Когда мы отправили запрос на обновление PATCH /employees/:id
, веб-приложение верно определило обработчик, нашло сотрудника по идентификатору и обновило его данные в хранилище.
Таким образом мы написали почти все операции над ресурсом employees
. Осталось только реализовать удаление сотрудника.
Удаление сотрудника
Со временем сотрудники могут увольняться из компании. В этом случае у нас должен быть метод удаления сотрудника из хранилища. Удаление происходит с помощью метода DELETE /employees/:id
, где :id
— это идентификатор сотрудника.
Для начала опишем функцию удаления сотрудника из хранилища. В нашем случае она состоит из одной строки:
func (s *EmployeeStorageInMemory) Delete(id string) {
delete(s.employees, id)
}
Теперь добавим обработчик для метода DELETE /employees/:id
:
// Удаление сотрудника
webApp.Delete("/employees/:id", func(c *fiber.Ctx) error {
storage.Delete(c.Params("id"))
// Возвращаем успешный ответ без тела
return c.SendStatus(fiber.StatusNoContent)
})
Запускаем веб-приложения и создаем нового сотрудника:
curl --location --request POST 'http://localhost/employees' \
--header 'Content-Type: application/json' \
--data-raw '{"email": "john@corp.com", "role": "salesman"}'
В ответ получаем идентификатор сотрудника:
{"id":"c27bd23f-aa14-47e1-8422-75e18a2eecab"}
Теперь проверим метод удаления:
curl --location --request DELETE 'http://localhost/employees/c27bd23f-aa14-47e1-8422-75e18a2eecab'
Запрос не вернул ошибку, и это значит, что он прошел успешно. Попробуем получить данные удаленного сотрудника:
curl --location --request GET 'http://localhost/employees/c27bd23f-aa14-47e1-8422-75e18a2eecab'
В ответ получаем ожидаемую ошибку, что сотрудник не найден:
HTTP/1.1 404 Not Found
Not Found
Таким образом мы реализовали последнюю CRUD-операцию — удаление сотрудника. Когда мы отправили запрос DELETE /employees/:id
, веб-приложение по идентификатору удалило сотрудника из хранилища.
Выводы
В этом уроке мы научились разрабатывать веб-приложение, которое выполняет различные операции над данными в хранилище. Повторим важные моменты темы:
- CRUD — это аббревиатура, которая означает Create, Read, Update, Delete
- Построение веб-приложений по CRUD-модели распространено благодаря простоте разработки и поддержки кода
- Со стороны HTTP-протокола CRUD-модель реализуется с помощью методов GET, POST, PATCH/PUT, DELETE
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.