Golang: RESTful API, использующий временную таблицу с MariaDB
8 апреля 2023 г.Временные таблицы – это тип таблицы базы данных, в которой хранятся исторические данные, что позволяет пользователям запрашивать данные в том виде, в каком они существовали на момент конкретные моменты времени. Эти таблицы отслеживают изменения данных с течением времени и сохраняют историю всех изменений. Это может быть полезно для аудита, соответствия и отслеживания изменений с течением времени. Временные таблицы отличаются от обычных тем, что в них хранится как текущее состояние данных, так и история изменений.< /p>
MariaDB — это популярная система управления реляционными базами данных с открытым исходным кодом, поддерживающая темпоральные таблицы. Пользователи могут создавать временные таблицы и управлять ими с помощью SQL. Это позволяет пользователям легко отслеживать изменения данных и получать доступ к историческим версиям записей.
Соответственно, на его официальном веб-сайте темпоральные таблицы поддерживаются в трех формах: системная версия , время приложения и битемпоральный. В этом примере мы будем использовать версию системы.
В этой статье мы создадим докеризованный REST API в Go, который реализует такой вариант использования.
Модель домена
Мы создадим простой API, который предлагает конечные точки для:
* список сотрудников; * список сотрудников по id; * создать сотрудника; * обновить сотрудника; * получить историческую информацию о сотруднике; * удалить сотрудника.
Наши столы:
schema_migrations — это таблица, используемая для миграция базы данных.
API
Я не буду вдаваться в подробности того, как я спроектировал и реализовал сам API, так как он заслуживает особого внимания. эксклюзивный пост для него. Вкратце, вот ключевые моменты:
* Я использую httptreemux в качестве маршрутизатора; * Я написал несколько пользовательских промежуточных программ как для обработки ошибок, так и для общего ведения журнала, что сокращает объем кода в функциях обработчика. существенно; * Я решил использовать стратегию управления версиями пути для API.
Ресурс сотрудников
Вот наши обработчики (handlers/v1/employees/employees.go):
// Copyright (c) 2023 Tiago Melo. All rights reserved.
// Use of this source code is governed by the MIT License that can be found in
// the LICENSE file.
package employees
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
mariaDb "github.com/tiagomelo/docker-mariadb-temporal-tables-tutorial/db"
"github.com/tiagomelo/docker-mariadb-temporal-tables-tutorial/db/employees"
"github.com/tiagomelo/docker-mariadb-temporal-tables-tutorial/db/employees/models"
"github.com/tiagomelo/docker-mariadb-temporal-tables-tutorial/validate"
"github.com/tiagomelo/docker-mariadb-temporal-tables-tutorial/web"
v1Web "github.com/tiagomelo/docker-mariadb-temporal-tables-tutorial/web/v1"
)
type Handlers struct {
Db *sql.DB
}
// handleGetEmployeeByIdError handles errors when getting an
// employee by its id.
func handleGetEmployeeByIdError(err error, id uint) error {
if errors.Is(err, mariaDb.ErrDBNotFound) {
return v1Web.NewRequestError(err, http.StatusNotFound)
}
return fmt.Errorf("ID[%d]: %w", id, err)
}
// handleCreateEmployeeError handles errors when creating an
// employee.
func handleCreateEmployeeError(err error) error {
if errors.Is(err, mariaDb.ErrDBDuplicatedEntry) {
return v1Web.NewRequestError(err, http.StatusConflict)
}
return fmt.Errorf("unable to create employee: %w", err)
}
// handleUpdateEmployeeByIdErr handles errors when updating an
// employee by its id.
func handleUpdateEmployeeByIdErr(err error, id uint) error {
if errors.Is(err, mariaDb.ErrDBNotFound) {
return v1Web.NewRequestError(err, http.StatusNotFound)
}
return fmt.Errorf("ID[%d]: %w", id, err)
}
// GetById returns a current employee with given id.
func (h Handlers) GetById(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
idParam := web.Param(r, "id")
id, err := strconv.Atoi(idParam)
if err != nil {
return v1Web.NewRequestError(fmt.Errorf("invalid id: %v", idParam), http.StatusBadRequest)
}
employee, err := employees.GetById(ctx, h.Db, uint(id))
if err != nil {
return handleGetEmployeeByIdError(err, uint(id))
}
return web.Respond(ctx, w, employee, http.StatusOK)
}
// GetAll returns all current employees.
func (h *Handlers) GetAll(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
employees, err := employees.GetAll(ctx, h.Db)
if err != nil {
return fmt.Errorf("unable to query employees: %w", err)
}
return web.Respond(ctx, w, employees, http.StatusOK)
}
// Create creates an employee.
func (h Handlers) Create(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
defer r.Body.Close()
var newEmployee models.NewEmployee
if err := json.NewDecoder(r.Body).Decode(&newEmployee); err != nil {
return v1Web.NewRequestError(err, http.StatusBadRequest)
}
if err := validate.Check(newEmployee); err != nil {
return fmt.Errorf("validating data: %w", err)
}
employee, err := employees.Create(ctx, h.Db, &newEmployee)
if err != nil {
return handleCreateEmployeeError(err)
}
return web.Respond(ctx, w, employee, http.StatusCreated)
}
// Update updates a current employee.
func (h Handlers) Update(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
idParam := web.Param(r, "id")
id, err := strconv.Atoi(idParam)
if err != nil {
return v1Web.NewRequestError(fmt.Errorf("invalid id: %v", idParam), http.StatusBadRequest)
}
var updateEmployee models.UpdateEmployee
if err := json.NewDecoder(r.Body).Decode(&updateEmployee); err != nil {
return v1Web.NewRequestError(err, http.StatusBadRequest)
}
updatedEmployee, err := employees.Update(ctx, h.Db, uint(id), &updateEmployee)
if err != nil {
return handleUpdateEmployeeByIdErr(err, uint(id))
}
return web.Respond(ctx, w, updatedEmployee, http.StatusOK)
}
// Delete deletes a current employee.
func (h Handlers) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
idParam := web.Param(r, "id")
id, err := strconv.Atoi(idParam)
if err != nil {
return v1Web.NewRequestError(fmt.Errorf("invalid id: %v", idParam), http.StatusBadRequest)
}
if err = employees.Delete(ctx, h.Db, uint(id)); err != nil {
return fmt.Errorf("ID[%d]: %w", id, err)
}
return web.Respond(ctx, w, nil, http.StatusNoContent)
}
А вот и наши коррелированные функции БД (db/employees/employees.go):
// Copyright (c) 2023 Tiago Melo. All rights reserved.
// Use of this source code is governed by the MIT License that can be found in
// the LICENSE file.
package employees
import (
"context"
"database/sql"
"github.com/go-sql-driver/mysql"
mariaDb "github.com/tiagomelo/docker-mariadb-temporal-tables-tutorial/db"
"github.com/tiagomelo/docker-mariadb-temporal-tables-tutorial/db/employees/models"
)
// For ease of unit testing.
var (
readEmployee = func(row *sql.Row, dest ...any) error {
return row.Scan(dest...)
}
readEmployees = func(rows *sql.Rows, dest ...any) error {
return rows.Scan(dest...)
}
)
// GetById returns a current employee with given id.
func GetById(ctx context.Context, db *sql.DB, id uint) (*models.Employee, error) {
q := `
SELECT id, first_name, last_name, salary, department
FROM employees
WHERE id = ?
`
var employee models.Employee
row := db.QueryRowContext(ctx, q, id)
if err := readEmployee(row,
&employee.Id,
&employee.FirstName,
&employee.LastName,
&employee.Salary,
&employee.Department,
); err != nil {
return nil, mariaDb.ErrDBNotFound
}
return &employee, nil
}
// GetAll returns all current employees.
func GetAll(ctx context.Context, db *sql.DB) ([]models.Employee, error) {
q := `
SELECT id, first_name, last_name, salary, department
FROM employees
`
employees := make([]models.Employee, 0)
rows, err := db.QueryContext(ctx, q)
if err != nil {
return employees, err
}
defer rows.Close()
for rows.Next() {
var employee models.Employee
if err = readEmployees(rows,
&employee.Id,
&employee.FirstName,
&employee.LastName,
&employee.Salary,
&employee.Department,
); err != nil {
return employees, err
}
employees = append(employees, employee)
}
return employees, nil
}
// Create creates an employee.
func Create(ctx context.Context, db *sql.DB, newEmployee *models.NewEmployee) (*models.Employee, error) {
q := `
INSERT INTO
employees(first_name, last_name, salary, department)
VALUES
(?, ?, ?, ?)
RETURNING
id, first_name, last_name, salary, department
`
var employee models.Employee
row := db.QueryRowContext(ctx, q, newEmployee.FirstName, newEmployee.LastName, newEmployee.Salary, newEmployee.Department)
if err := readEmployee(row,
&employee.Id,
&employee.FirstName,
&employee.LastName,
&employee.Salary,
&employee.Department,
); err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok && mysqlErr.Number == mariaDb.UniqueViolation {
return nil, mariaDb.ErrDBDuplicatedEntry
}
return nil, err
}
return &employee, nil
}
// handleEmployeeChanges updates the changed properties.
func handleEmployeeChanges(updateEmployee *models.UpdateEmployee, dbEmployee *models.Employee) {
if updateEmployee.FirstNameIsFulfilled() {
dbEmployee.FirstName = *updateEmployee.FirstName
}
if updateEmployee.LastNameIsFulfilled() {
dbEmployee.LastName = *updateEmployee.LastName
}
if updateEmployee.SalaryIsFulfilled() {
dbEmployee.Salary = *updateEmployee.Salary
}
if updateEmployee.DepartmentIsFulfilled() {
dbEmployee.Department = *updateEmployee.Department
}
}
// Update updates an employee.
func Update(ctx context.Context, db *sql.DB, employeeId uint, updateEmployee *models.UpdateEmployee) (*models.Employee, error) {
q := `
UPDATE employees
SET
first_name = ?,
last_name = ?,
salary = ?,
department = ?
WHERE
id = ?
`
dbEmployee, err := GetById(ctx, db, employeeId)
if err != nil {
return nil, err
}
handleEmployeeChanges(updateEmployee, dbEmployee)
_, err = db.ExecContext(ctx, q, dbEmployee.FirstName, dbEmployee.LastName, dbEmployee.Salary, dbEmployee.Department, dbEmployee.Id)
return dbEmployee, err
}
// Delete deletes an employee.
func Delete(ctx context.Context, db *sql.DB, id uint) error {
q := `
DELETE FROM
employees
WHERE id = ?
`
_, err := db.ExecContext(ctx, q, id)
return err
}
Как видим, старый добрый CRUD, ничего особенного здесь нет.
Давайте создадим нашего первого сотрудника:
curl 'http://localhost:3000/v1/employee'
> --header 'Content-Type: application/json'
> --data '{
> "first_name": "John",
> "last_name": "Doe",
> "salary": 1234.56,
> "department": "IT"
> }'
{"id":1,"first_name":"John","last_name":"Doe","department":"IT","salary":1234.56}
Теперь предположим, что мы хотим обновить зарплату этого сотрудника через несколько лет. Для этого нам нужно вручную изменить метку времени по умолчанию в MariaDB, чтобы она возвращает отметку времени в будущем.
Для этого я создал две конечные точки:
* GET v1/db/timestamp/advance: переводит отметку времени по умолчанию в MariaDB на случайное число лет от 1 до 5; * GET v1/db/timestamp/default: устанавливает отметку времени по умолчанию на текущую дату.
Давайте вызовем конечную точку для опережения метки времени:
curl 'http://localhost:3000/v1/db/timestamp/advance'
{"timestamp":"2025-04-06"}
Мы видим, что мы случайным образом передвинули текущий год (2023) на два года (2025), а это значит, что MariaDB думает, что мы находимся в 2025 году.
Давайте тогда обновим зарплату сотрудника:
curl --request PUT 'http://localhost:3000/v1/employee/1'
--header 'Content-Type: application/json'
--data '{
"salary": 2500
}'
{"id":1,"first_name":"John","last_name":"Doe","department":"IT","salary":2500}
Теперь мы увидим мощь темпоральных таблиц. Что, если мы хотим запросить все исторические изменения для этого сотрудника?
Ресурс истории сотрудников
У нас есть три доступных конечных точки:
* v1/employee/{id}/history/all, что позволяет нам проверять все исторические данные; * v1/employee/{id}/history/{timestamp}, которая позволяет нам получать исторические данные в заданный момент времени; * v1/employee/{id}/history/{start_timestamp}/{end_timestamp}, что позволяет получить исторические данные между двумя датами.
Вот наши обработчики (handlers/v1/employees/history/history.go):
// Copyright (c) 2023 Tiago Melo. All rights reserved.
// Use of this source code is governed by the MIT License that can be found in
// the LICENSE file.
package history
import (
"context"
"database/sql"
"fmt"
"net/http"
"strconv"
"time"
"github.com/tiagomelo/docker-mariadb-temporal-tables-tutorial/db/employees"
"github.com/tiagomelo/docker-mariadb-temporal-tables-tutorial/web"
v1Web "github.com/tiagomelo/docker-mariadb-temporal-tables-tutorial/web/v1"
)
type Handlers struct {
Db *sql.DB
}
// GetAll returns all historical data for a given employee.
func (h Handlers) GetAll(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
idParam := web.Param(r, "id")
id, err := strconv.Atoi(idParam)
if err != nil {
return v1Web.NewRequestError(fmt.Errorf("invalid id: %v", idParam), http.StatusBadRequest)
}
employeeHistory, err := employees.GetAllHistory(ctx, h.Db, uint(id))
if err != nil {
return fmt.Errorf("ID[%d]: %w", id, err)
}
return web.Respond(ctx, w, employeeHistory, http.StatusOK)
}
// AtPointInTime returns historical data for a given employee at a point in time.
func (h Handlers) AtPointInTime(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
idParam := web.Param(r, "id")
id, err := strconv.Atoi(idParam)
if err != nil {
return v1Web.NewRequestError(fmt.Errorf("invalid id: %v", idParam), http.StatusBadRequest)
}
timestampParam := web.Param(r, "timestamp")
timestamp, err := strconv.ParseInt(timestampParam, 10, 64)
if err != nil {
return v1Web.NewRequestError(fmt.Errorf("invalid timestamp: %v", timestampParam), http.StatusBadRequest)
}
employeeHistory, err := employees.AtPointInTime(ctx, h.Db, uint(id), time.Unix(timestamp, 0).Format("2006-01-02 15:04:05"))
if err != nil {
return fmt.Errorf("ID[%d]: %w", timestamp, err)
}
return web.Respond(ctx, w, employeeHistory, http.StatusOK)
}
// BetweenDates returns historical data for a given employee between dates.
func (h Handlers) BetweenDates(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
idParam := web.Param(r, "id")
id, err := strconv.Atoi(idParam)
if err != nil {
return v1Web.NewRequestError(fmt.Errorf("invalid id: %v", idParam), http.StatusBadRequest)
}
startTimestampParam := web.Param(r, "startTimestamp")
startTimestamp, err := strconv.ParseInt(startTimestampParam, 10, 64)
if err != nil {
return v1Web.NewRequestError(fmt.Errorf("invalid start timestamp: %v", startTimestampParam), http.StatusBadRequest)
}
endTimestampParam := web.Param(r, "endTimestamp")
endTimestamp, err := strconv.ParseInt(endTimestampParam, 10, 64)
if err != nil {
return v1Web.NewRequestError(fmt.Errorf("invalid end timestamp: %v", endTimestampParam), http.StatusBadRequest)
}
employeeHistory, err := employees.BetweenDates(ctx, h.Db, uint(id), time.Unix(startTimestamp, 0).Format("2006-01-02 15:04:05"), time.Unix(endTimestamp, 0).Format("2006-01-02 15:04:05"))
if err != nil {
return fmt.Errorf("ID[%d]: %w", startTimestamp, err)
}
return web.Respond(ctx, w, employeeHistory, http.StatusOK)
}
Получение всех исторических данных
Проверьте запрос SQL (db/employees/employees_history.go), чтобы получить все исторические данные:
// GetAllHistory returns the complete history of a given employee.
func GetAllHistory(ctx context.Context, db *sql.DB, id uint) ([]models.EmployeeHistory, error) {
q := `
SELECT id, first_name, last_name, salary, department, row_start, row_end
FROM employees
FOR SYSTEM_TIME ALL
WHERE id = ?
`
employeeHistory := make([]models.EmployeeHistory, 0)
rows, err := db.QueryContext(ctx, q, id)
if err != nil {
return employeeHistory, err
}
defer rows.Close()
for rows.Next() {
var employeeHist models.EmployeeHistory
if err = readEmployeeHistory(rows,
&employeeHist.Id,
&employeeHist.FirstName,
&employeeHist.LastName,
&employeeHist.Salary,
&employeeHist.Department,
&employeeHist.RowStart,
&employeeHist.RowEnd,
); err != nil {
return nil, err
}
employeeHistory = append(employeeHistory, employeeHist)
}
return employeeHistory, nil
}
Давайте попробуем:
curl 'http://localhost:3000/v1/employee/1/history/all'
[
{
"id": 1,
"first_name": "John",
"last_name": "Doe",
"department": "IT",
"salary": 1234.56,
"row_start": "2023-04-06T13:31:50.830757Z",
"row_end": "2025-04-06T00:00:00Z"
},
{
"id": 1,
"first_name": "John",
"last_name": "Doe",
"department": "IT",
"salary": 2500,
"row_start": "2025-04-06T00:00:00Z",
"row_end": "2038-01-19T03:14:07.999999Z"
}
]
Мы видим, что первая зарплата Джона была зарегистрирована 06 апреля 2023 г. со значением 1234,56, а 6 апреля 2025 г. он получил повышение до значения 2500.
Получение исторических данных на определенный момент времени
Что, если мы хотим проверить зарплату Джона в определенный момент времени? У нас есть этот SQL-запрос:
// AtPointInTime returns the complete history of a given employee in a given
// point in time.
func AtPointInTime(ctx context.Context, db *sql.DB, id uint, timestamp string) ([]models.EmployeeHistory, error) {
q := `
SELECT id, first_name, last_name, salary, department, row_start, row_end
FROM employees
FOR SYSTEM_TIME
AS OF TIMESTAMP ?
WHERE id = ?
`
employeeHistory := make([]models.EmployeeHistory, 0)
rows, err := db.QueryContext(ctx, q, timestamp, id)
if err != nil {
return employeeHistory, err
}
defer rows.Close()
for rows.Next() {
var employeeHist models.EmployeeHistory
if err = readEmployeeHistory(rows,
&employeeHist.Id,
&employeeHist.FirstName,
&employeeHist.LastName,
&employeeHist.Salary,
&employeeHist.Department,
&employeeHist.RowStart,
&employeeHist.RowEnd,
); err != nil {
return nil, err
}
employeeHistory = append(employeeHistory, employeeHist)
}
return employeeHistory, nil
}
Используя такой веб-сайт, как Конвертер эпох, легко получить метку времени. Какова была зарплата Джона в 2024-04-06?
curl 'http://localhost:3000/v1/employee/1/history/1712411281'
[
{
"id": 1,
"first_name": "John",
"last_name": "Doe",
"department": "IT",
"salary": 1234.56,
"row_start": "2023-04-06T13:31:50.830757Z",
"row_end": "2025-04-06T00:00:00Z"
}
]
1234,56 именно потому, что он еще не получил повышения.
Какова была зарплата Джона 06.06.2025?
curl 'http://localhost:3000/v1/employee/1/history/1749217681'
[
{
"id": 1,
"first_name": "John",
"last_name": "Doe",
"department": "IT",
"salary": 2500,
"row_start": "2025-04-06T00:00:00Z",
"row_end": "2038-01-19T03:14:07.999999Z"
}
]
Это было 2500, потому что он получил повышение в 2025-04-06.
Получение исторических данных между датами
Чтобы проверить эту историческую информацию между двумя датами, у нас есть этот запрос:
// BetweenDates returns the complete history of a given employee in a given period.
func BetweenDates(ctx context.Context, db *sql.DB, id uint, startTimestamp, endTimeStamp string) ([]models.EmployeeHistory, error) {
q := `
SELECT id, first_name, last_name, salary, department, row_start, row_end
FROM employees
FOR SYSTEM_TIME
FROM ? TO ?
WHERE id = ?
`
employeeHistory := make([]models.EmployeeHistory, 0)
rows, err := db.QueryContext(ctx, q, startTimestamp, endTimeStamp, id)
if err != nil {
return employeeHistory, err
}
defer rows.Close()
for rows.Next() {
var employeeHist models.EmployeeHistory
if err = readEmployeeHistory(rows,
&employeeHist.Id,
&employeeHist.FirstName,
&employeeHist.LastName,
&employeeHist.Salary,
&employeeHist.Department,
&employeeHist.RowStart,
&employeeHist.RowEnd,
); err != nil {
return nil, err
}
employeeHistory = append(employeeHistory, employeeHist)
}
return employeeHistory, nil
}
Какова была зарплата Джона между 06.04.2023 и 02.01.2024, учитывая, что ему повысили зарплату 06.04.2024?
curl 'http://localhost:3000/v1/employee/1/history/1680788881/1704203281'
[
{
"id": 1,
"first_name": "John",
"last_name": "Doe",
"department": "IT",
"salary": 1234.56,
"row_start": "2023-04-06T13:31:50.830757Z",
"row_end": "2025-04-06T00:00:00Z"
}
]
Ага, 1234,56.
Дополнительные темы
Создание документации Swagger
Проверив наш *Makefile *, мы обнаружили следующие цели:
$ make help
Usage: make [target
help shows this help message
mariadb-console launches mariadb local database console
test-mariadb-console launches mariadb test database console
create-migration creates a migration file
test runs unit tests
coverage run unit tests and generate coverage report in html format
test-db-up starts test database
int-test runs integration tests
vet runs go vet
lint runs linter for all packages
vul-setup installs Golang's vulnerability check tool
vul-check checks for any known vulnerabilities
swagger generates api's documentation
swagger-ui launches swagger ui
run runs the application
stop stops all containers]
Создать документацию Swagger довольно просто. В папке «doc» у нас есть:
doc/doc.go
// Employees API
//
// A sample RESTful API to manage employees.
// Host: localhost:3000
// Version: 0.0.1
// Contact: Tiago Melo <tiagoharris@gmail.com>
//
// Consumes:
// - application/json
//
// Produces:
// - application/json
//
// swagger:meta
package doc
И doc/api.go
// Copyright (c) 2023 Tiago Melo. All rights reserved.
// Use of this source code is governed by the MIT License that can be found in
// the LICENSE file.
package doc
import "github.com/tiagomelo/docker-mariadb-temporal-tables-tutorial/db/employees/models"
// swagger:route GET /v1/employees employees GetAll
// Get all current employees.
// ---
// responses:
// 200: getAllCurrentEmployeesResponse
// swagger:response getAllCurrentEmployeesResponse
type GetAllCurrentEmployeesResponseWrapper struct {
// in:body
Body []models.Employee
}
// swagger:route GET /v1/employee/{id} employee GetById
// Get a current employee by its id.
// ---
// responses:
//
// 200: employee
// 400: description: invalid id
// 404: description: employee not found
//
// swagger:parameters GetById
type GetEmployeeByIdParamsWrapper struct {
// in:path
Id int
}
// swagger:response employee
type EmployeeResponseWrapper struct {
// in:body
Body models.Employee
}
// swagger:route POST /v1/employee employee Create
// Create an employee.
// ---
// responses:
//
// 200: employee
//
// swagger:parameters Create
type PostEmployeeParamsWrapper struct {
// in:body
Body models.NewEmployee
}
// swagger:route PUT /v1/employee/{id} employee Update
// Updates a current employee.
// ---
// responses:
//
// 200: employee
// 400: description: invalid id
// 404: description: employee not found
//
// swagger:parameters Update
type PutEmployeeParamsWrapper struct {
// in:path
Id int
// in:body
Body models.UpdateEmployee
}
// swagger:route DELETE /v1/employee/{id} employee Delete
// Deletes a current employee.
// ---
// responses:
//
// 204: description: no content
// 400: description: invalid id
//
// swagger:parameters Delete
type DeleteEmployeeParamsWrapper struct {
// in:path
Id int
}
// swagger:route GET /v1/employee/{id}/history/all history GetAllEmployeeHistoryById
// Get all historical data about an employee with a given id.
// ---
// responses:
//
// 200: employeeHistory
// 400: description: invalid id
//
// swagger:parameters GetAllEmployeeHistoryById
type GetAllEmployeeHistoryByIdParamsWrapper struct {
// in:path
Id int
}
// swagger:route GET /v1/employee/{id}/history/{timestamp} history GetAllEmployeeHistoryAtPointInTime
// Get historical data about an employee with a given id at a given point in time.
// ---
// responses:
//
// 200: employeeHistory
// 400: description: invalid id
// 400: description: invalid timestamp
//
// swagger:parameters GetAllEmployeeHistoryAtPointInTime
type GetAllEmployeeHistoryAtPointInTimeParamsWrapper struct {
// in:path
Timestamp int
}
// swagger:route GET /v1/employee/{id}/history/{startTimestamp}/{endTimestamp} history GetAllEmployeeHistoryBetweenDates
// Get historical data about an employee with a given id between dates.
// ---
// responses:
//
// 200: employeeHistory
// 400: description: invalid id
// 400: description: invalid start timestamp
// 400: description: invalid end timestamp
//
// swagger:parameters GetAllEmployeeHistoryBetweenDates
type GetAllEmployeeHistoryBetweenDatesParamsWrapper struct {
// in:path
StartTimestamp int
// in:path
EndTimestamp int
}
// swagger:response employeeHistory
type EmployeeHistoryResponseWrapper struct {
// in:body
Body []models.EmployeeHistory
}
Тогда бежим:
make swagger-ui
Проверьте свой браузер по адресу http://localhost/:
Проверить наличие известных уязвимостей
$ make run vul-check
Запустить линтер
$ make lint
Заключение
Временные таблицы – это простой способ извлечения исторических данных, упрощающий путешествие во времени.
Проверьте здесь поставщиков, которые его реализуют.
Скачать исходный код
Здесь: https://github.com/tiagomelo/docker-mariadb-temporal-tables-tutorial.
Оригинал