API 完整 Golang - 第 7 部分
O que vamos fazer?
最后,请参阅第 7 部分,了解产品和类别的地籍功能、产品和类别的功能、编辑产品、列出待办事项和删除产品。
Vamos trabalhar com 是多对多的关系,是多个产品的类别,也是多个类别的产品。 Vamos fazer tudo Neste último post,por isso deve ser um dos maiores da nossa série。
refatorando nosso handler
首先,不要调整任何处理程序,或者将处理程序与用户分开,将产品类别分开,然后将其设置为主要的参数,以参考服务器 criado com go-chi,以解决问题并解决处理程序的问题。
Vamos mover o user_handler.go,auth_handler.go用于user_interface_handler.go意大利面处理程序,vamos também renomear o arquivo user_interface_handler.gopara interface_handler.go,vamos ter apenas uma 界面。 Depois de mover você pode 删除意大利面用户处理程序,ficando assim:
Vamos 重新命名为处理程序更改的功能和接口interface_handler.go:
NewUserHandler帕拉NewHandler
UserHandler帕拉Handler
Precisamos alterar também o nome dos pacotes dos arquivos movidos de package userhandlerpara package handler.
Vamos ajustar nosso main.go:
newHandler := handler.NewHandler(newUserService)
// init routes
router := chi.NewRouter()
routes.InitRoutes(router, newHandler)
routes.InitDocsRoutes(router)
作为实体的 Criando
描述产品类别时,我们的产品类别是多对多关系的,产品类别中的产品类别是相互关联的。
Vamos criar um arquivo category_entity.gona 面食实体:
type CategoryEntity struct {
ID string `json:"id"`
Title string `json:"title"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Vamos criar um outro arquivo product_entity.gona 面食实体:
type ProductEntity struct {
ID string `json:"id"`
Title string `json:"title"`
Price int32 `json:"price"`
Categories []string `json:"categories"`
Description string `json:"description"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ProductCategoryEntity struct {
ID string `json:"id"`
ProductID string `json:"product_id"`
CategoryID string `json:"category_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ProductWithCategoryEntity struct {
ID string `json:"id"`
Title string `json:"title"`
Price int32 `json:"price"`
Description string `json:"description"`
Categories []CategoryEntity `json:"categories"`
CreatedAt time.Time `json:"created_at"`
}
Teremos 3 entidades:
-
ProductEntity:vamos usar para criar o produto。 -
ProductCategoryEntity: vamos usar para criar or relacionamento entre produto e categoria. -
ProductWithCategoryEntity: vamos usar para montar um retorno onde trazemos o 产品和 suas categorias。
动机是什么ProductCategoryEntity?我们的产品与多对多的关系是在标签中进行的,并且对产品类别的响应进行了响应,因此标签可以与中间/混合/连接中的各种命名方式相结合。 Para ficar mais claro, veja a imagem abaixo:
Criando os 处理程序 e 类产品
Vamos iniciar criando os necessário no na Interface handler que acabamos de alterar:
func NewHandler(userService userservice.UserService,
categoryService categoryservice.CategoryService,
productservice productservice.ProductService) Handler {
return &handler{
userService: userService,
categoryService: categoryService,
productservice: productservice,
}
}
type handler struct {
userService userservice.UserService
categoryService categoryservice.CategoryService
productservice productservice.ProductService
}
Primeiro 定义了Handler3 种服务、用户、产品和类别。
type Handler interface {
CreateUser(w http.ResponseWriter, r *http.Request)
UpdateUser(w http.ResponseWriter, r *http.Request)
GetUserByID(w http.ResponseWriter, r *http.Request)
DeleteUser(w http.ResponseWriter, r *http.Request)
FindManyUsers(w http.ResponseWriter, r *http.Request)
UpdateUserPassword(w http.ResponseWriter, r *http.Request)
Login(w http.ResponseWriter, r *http.Request)
CreateCategory(w http.ResponseWriter, r *http.Request)
CreateProduct(w http.ResponseWriter, r *http.Request)
UpdateProduct(w http.ResponseWriter, r *http.Request)
DeleteProduct(w http.ResponseWriter, r *http.Request)
FindManyProducts(w http.ResponseWriter, r *http.Request)
}
Nossa 界面 ficou assim,com os métodos de usuário que já 存在于市场上 adicionamos 新方法。车辆的使用方法、车辆的使用方法、车辆的产品、车辆的使用、删除和各种巴士的使用方法,以及车辆的使用方法。
类别处理程序
Crie um arquivo chamado category_handler,作为意大利面处理程序的终点,vai ser bastante simples,vamos adicionar poucos bados a nossa categoria,vamos criar também nosso dto,crie um arquivo chamado category_dto.gona Pasta dto。
package dto
type CreateCategoryDto struct {
Title string `json:"title" validate:"required,min=3,max=30"`
}
Nossa categoria vai receber apenas um título, nada mais。 Adicione 结束了dados se desejar。
func (h *handler) CreateCategory(w http.ResponseWriter, r *http.Request) {
var req dto.CreateCategoryDto
if r.Body == http.NoBody {
slog.Error("body is empty", slog.String("package", "categoryhandler"))
w.WriteHeader(http.StatusBadRequest)
msg := httperr.NewBadRequestError("body is required")
json.NewEncoder(w).Encode(msg)
return
}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
slog.Error("error to decode body", "err", err, slog.String("package", "categoryhandler"))
w.WriteHeader(http.StatusBadRequest)
msg := httperr.NewBadRequestError("error to decode body")
json.NewEncoder(w).Encode(msg)
return
}
httpErr := validation.ValidateHttpData(req)
if httpErr != nil {
slog.Error(fmt.Sprintf("error to validate data: %v", httpErr), slog.String("package", "categoryhandler"))
w.WriteHeader(httpErr.Code)
json.NewEncoder(w).Encode(httpErr)
return
}
err = h.categoryService.CreateCategory(r.Context(), req)
if err != nil {
slog.Error(fmt.Sprintf("error to create category: %v", err), slog.String("package", "categoryhandler"))
w.WriteHeader(http.StatusBadRequest)
}
w.WriteHeader(http.StatusCreated)
}
如果处理程序是基本的,并且在使用过程中遇到了一些问题,则可能会出现错误,categoryService.CreateCategory但服务中不会出现任何错误,因此处理程序的类别可能会有所不同。
产品处理员
Crie um arquivo chamado product_handler,nas 意大利面处理程序,vamos criar também nosso dto,crie um arquivo chamado product_dto.gona Pasta dto。
package dto
type CreateProductDto struct {
Title string `json:"title" validate:"required,min=3,max=40"`
Price int32 `json:"price" validate:"required,min=1"`
Categories []string `json:"categories" validate:"required,min=1,dive,uuid4"`
Description string `json:"description" validate:"required,min=3,max=500"`
}
type UpdateProductDto struct {
Title string `json:"title" validate:"omitempty,min=3,max=40"`
Price int32 `json:"price" validate:"omitempty,min=1"`
Categories []string `json:"categories" validate:"omitempty,min=1,dive,uuid4"`
Description string `json:"description" validate:"omitempty,min=3,max=500"`
}
type FindProductDto struct {
Search string `json:"search" validate:"omitempty,min=2,max=40"`
Categories []string `json:"categories" validate:"omitempty,min=1,dive,uuid4"`
}
Vamos ter 3 dtos para o 产品CreateProductDto,UpdateProductDtoe para filtrar FindProductDto。
Validamos 可以用作dive数组中的验证元素的类别,也可以使用 Playground 验证器来进行验证,作为使用时的验证。
CreateProduct:
func (h *handler) CreateProduct(w http.ResponseWriter, r *http.Request) {
var req dto.CreateProductDto
if r.Body == http.NoBody {
slog.Error("body is empty", slog.String("package", "producthandler"))
w.WriteHeader(http.StatusBadRequest)
msg := httperr.NewBadRequestError("body is required")
json.NewEncoder(w).Encode(msg)
return
}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
slog.Error("error to decode body", "err", err, slog.String("package", "producthandler"))
w.WriteHeader(http.StatusBadRequest)
msg := httperr.NewBadRequestError("error to decode body")
json.NewEncoder(w).Encode(msg)
return
}
httpErr := validation.ValidateHttpData(req)
if httpErr != nil {
slog.Error(fmt.Sprintf("error to validate data: %v", httpErr), slog.String("package", "producthandler"))
w.WriteHeader(httpErr.Code)
json.NewEncoder(w).Encode(httpErr)
return
}
err = h.productservice.CreateProduct(r.Context(), req)
if err != nil {
if err.Error() == "category not found" {
w.WriteHeader(http.StatusNotFound)
msg := httperr.NewNotFoundError("category not found")
json.NewEncoder(w).Encode(msg)
return
}
slog.Error(fmt.Sprintf("error to create category: %v", err), slog.String("package", "categoryhandler"))
w.WriteHeader(http.StatusBadRequest)
}
w.WriteHeader(http.StatusCreated)
}
UpdateProduct:
func (h *handler) UpdateProduct(w http.ResponseWriter, r *http.Request) {
var req dto.UpdateProductDto
productID := chi.URLParam(r, "id")
if productID == "" {
slog.Error("product id is required", slog.String("package", "producthandler"))
w.WriteHeader(http.StatusBadRequest)
msg := httperr.NewBadRequestError("product id is required")
json.NewEncoder(w).Encode(msg)
return
}
_, err := uuid.Parse(productID)
if err != nil {
slog.Error(fmt.Sprintf("error to parse product id: %v", err), slog.String("package", "producthandler"))
w.WriteHeader(http.StatusBadRequest)
msg := httperr.NewBadRequestError("invalid product id")
json.NewEncoder(w).Encode(msg)
return
}
if r.Body == http.NoBody {
slog.Error("body is empty", slog.String("package", "producthandler"))
w.WriteHeader(http.StatusBadRequest)
msg := httperr.NewBadRequestError("body is required")
json.NewEncoder(w).Encode(msg)
return
}
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
slog.Error("error to decode body", "err", err, slog.String("package", "producthandler"))
w.WriteHeader(http.StatusBadRequest)
msg := httperr.NewBadRequestError("error to decode body")
json.NewEncoder(w).Encode(msg)
return
}
httpErr := validation.ValidateHttpData(req)
if httpErr != nil {
slog.Error(fmt.Sprintf("error to validate data: %v", httpErr), slog.String("package", "producthandler"))
w.WriteHeader(httpErr.Code)
json.NewEncoder(w).Encode(httpErr)
return
}
err = h.productservice.UpdateProduct(r.Context(), productID, req)
if err != nil {
if err.Error() == "product not found" {
w.WriteHeader(http.StatusNotFound)
msg := httperr.NewNotFoundError("product not found")
json.NewEncoder(w).Encode(msg)
return
}
if err.Error() == "category not found" {
w.WriteHeader(http.StatusNotFound)
msg := httperr.NewNotFoundError("category not found")
json.NewEncoder(w).Encode(msg)
return
}
slog.Error(fmt.Sprintf("error to update category: %v", err), slog.String("package", "categoryhandler"))
w.WriteHeader(http.StatusBadRequest)
}
w.WriteHeader(http.StatusOK)
}
DeleteProduct:
func (h *handler) DeleteProduct(w http.ResponseWriter, r *http.Request) {
productID := chi.URLParam(r, "id")
if productID == "" {
slog.Error("product id is required", slog.String("package", "producthandler"))
w.WriteHeader(http.StatusBadRequest)
msg := httperr.NewBadRequestError("product id is required")
json.NewEncoder(w).Encode(msg)
return
}
_, err := uuid.Parse(productID)
if err != nil {
slog.Error(fmt.Sprintf("error to parse product id: %v", err), slog.String("package", "producthandler"))
w.WriteHeader(http.StatusBadRequest)
msg := httperr.NewBadRequestError("invalid product id")
json.NewEncoder(w).Encode(msg)
return
}
err = h.productservice.DeleteProduct(r.Context(), productID)
if err != nil {
if err.Error() == "product not found" {
w.WriteHeader(http.StatusNotFound)
msg := httperr.NewNotFoundError("product not found")
json.NewEncoder(w).Encode(msg)
return
}
slog.Error(fmt.Sprintf("error to delete category: %v", err), slog.String("package", "categoryhandler"))
w.WriteHeader(http.StatusBadRequest)
}
w.WriteHeader(http.StatusOK)
}
FindManyProducts:
func (h *handler) FindManyProducts(w http.ResponseWriter, r *http.Request) {
var req dto.FindProductDto
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
slog.Error("error to decode body", "err", err, slog.String("package", "producthandler"))
w.WriteHeader(http.StatusBadRequest)
msg := httperr.NewBadRequestError("error to decode body")
json.NewEncoder(w).Encode(msg)
return
}
httpErr := validation.ValidateHttpData(req)
if httpErr != nil {
slog.Error(fmt.Sprintf("error to validate data: %v", httpErr), slog.String("package", "producthandler"))
w.WriteHeader(httpErr.Code)
json.NewEncoder(w).Encode(httpErr)
return
}
products, err := h.productservice.FindManyProducts(r.Context(), req)
if err != nil {
slog.Error(fmt.Sprintf("error to find many products: %v", err), slog.String("package", "producthandler"))
w.WriteHeader(http.StatusBadRequest)
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(products)
}
必须采取的方法,请务必将前面的帖子分开。
Criando os 服务类别 e 产品
类别服务
Vamos criar um 意大利面服务chamado类别服务e um arquivo category_interface_service.go:
func NewCategoryService(repo categoryrepository.CategoryRepository) CategoryService {
return &service{
repo,
}
}
type service struct {
repo categoryrepository.CategoryRepository
}
type CategoryService interface {
CreateCategory(ctx context.Context, u dto.CreateCategoryDto) error
}
Assim igual ao handler 或 service será simples, agora vamos Implementar, crie outro arquivo chamadocategory_service.go
func (s *service) CreateCategory(ctx context.Context, u dto.CreateCategoryDto) error {
categoryEntity := entity.CategoryEntity{
ID: uuid.New().String(),
Title: u.Title,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := s.repo.CreateCategory(ctx, &categoryEntity)
if err != nil {
return errors.New("error to create category")
}
return nil
}
Somente isso é suficiente para criar nossa 类别。
产品服务
Vamos criar um Pasta dentro do service chamado产品服务e um arquivo product_interface_service.go:
func NewProductService(repo productrepository.ProductRepository) ProductService {
return &service{
repo,
}
}
type service struct {
repo productrepository.ProductRepository
}
type ProductService interface {
CreateProduct(ctx context.Context, u dto.CreateProductDto) error
UpdateProduct(ctx context.Context, id string, u dto.UpdateProductDto) error
DeleteProduct(ctx context.Context, id string) error
FindManyProducts(ctx context.Context, d dto.FindProductDto) ([]response.ProductResponse, error)
}
修复 que temos um response.ProductResponse、 precisamos criar também 、 crie na 面食反应um arquivo chamadoproduct_response.go和 outro chamado category_response.go:
category_response.go:
type CategoryResponse struct {
ID string `json:"id"`
Title string `json:"title"`
}
请查看产品类别。
product_response.go:
type ProductResponse struct {
ID string `json:"id"`
Title string `json:"title"`
Price int32 `json:"price"`
Description string `json:"description,omitempty"`
Categories []CategoryResponse `json:"categories"`
CreatedAt time.Time `json:"created_at"`
}
Como um produto pode ter muitas categorias, vamos retornar um slice de CategoryResponse.
Vamos 实施我们的服务,começando pelo CreateProduct:
func (s *service) CreateProduct(ctx context.Context, u dto.CreateProductDto) error {
productId := uuid.New().String()
productEntity := entity.ProductEntity{
ID: productId,
Title: u.Title,
Price: u.Price,
Categories: u.Categories,
Description: u.Description,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
var categories []entity.ProductCategoryEntity
for _, categoryID := range u.Categories {
exists, err := s.repo.GetCategoryByID(ctx, categoryID)
if err != nil || !exists {
slog.Error("category not found", slog.String("category_id", categoryID), slog.String("package", "productservice"))
return errors.New("category not found")
}
categories = append(categories, entity.ProductCategoryEntity{
ID: uuid.New().String(),
ProductID: productId,
CategoryID: categoryID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
}
err := s.repo.CreateProduct(ctx, &productEntity, categories)
if err != nil {
return err
}
return nil
}
没有服务 vamos apenas criar 或ProductEntitypara repassar ao nosso repositório,depois fazemos um for para criar a entidade da categoria,como o produto pode ter várias categorias,é necessário。
我希望您能对银行提供的服务负责,并为银行提供服务,以确保银行产品的准确性,并为银行提供产品和产品类别提供帮助。存储库错误CreateProduct,GetCategoryByIDainda vamos 实现。
Nesse mesmo forjá verificamos se a categoria Existe.
UpdateProduct:
func (s *service) UpdateProduct(ctx context.Context, id string, u dto.UpdateProductDto) error {
exists, err := s.repo.GetProductByID(ctx, id)
if err != nil || !exists {
slog.Error("product not found", slog.String("product_id", id), slog.String("package", "productservice"))
return errors.New("product not found")
}
// validate categories if they exist
var categories []entity.ProductCategoryEntity
if len(u.Categories) > 0 {
for _, categoryID := range u.Categories {
exists, err := s.repo.GetCategoryByID(ctx, categoryID)
if err != nil || !exists {
slog.Error("category not found", slog.String("category_id", categoryID), slog.String("package", "productservice"))
return errors.New("category not found")
}
}
// search for all categories of the product
productCategories, err := s.repo.GetCategoriesByProductID(ctx, id)
if err != nil {
return errors.New("error getting categories by product id")
}
// remove all categories that are not in u.Categories
for _, productCategory := range productCategories {
found := false
for _, categoryID := range u.Categories {
if productCategory == categoryID {
found = true
break
}
}
// if not found, then we can delete it
if !found {
err = s.repo.DeleteProductCategory(ctx, id, productCategory)
if err != nil {
return errors.New("error deleting product category")
}
}
}
for _, categoryID := range u.Categories {
found := false
for _, productCategory := range productCategories {
if productCategory == categoryID {
found = true
break
}
}
if !found {
categories = append(categories, entity.ProductCategoryEntity{
ID: uuid.New().String(),
ProductID: id,
CategoryID: categoryID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
}
}
}
productEntity := entity.ProductEntity{
ID: id,
Title: u.Title,
Price: u.Price,
Description: u.Description,
Categories: u.Categories,
UpdatedAt: time.Now(),
}
err = s.repo.UpdateProduct(ctx, &productEntity, categories)
if err != nil {
return err
}
return nil
}
基本的方法是,产品的功能非常丰富,产品的功能也很丰富,产品的分类也很丰富,产品的外观和产品的信息都categories可以验证,需要验证相关信息类别的 ID关联产品、se não estiver、vamos criar se estiver não fazemos nada e caso tenha uma categoria 关联与 esse produto que não esteja 没有切片信息categoriesno dto、vamos deletar、存在多种实施形式、poderíamos criar 结束点到去除剂 uma 类别德姆产品,关联 uma categoria,mas em um único 端点 deixas um pouco mais simples de trabalhar。
for _, categoryID := range u.Categories {
exists, err := s.repo.GetCategoryByID(ctx, categoryID)
if err != nil || !exists {
slog.Error("category not found", slog.String("category_id", categoryID), slog.String("package", "productservice"))
return errors.New("category not found")
}
}
Primeiro verificamos 被认为是不存在的信息类别categories。
productCategories, err := s.repo.GetCategoriesByProductID(ctx, id)
if err != nil {
return errors.New("error getting categories by product id")
}
Agora Buscamos 目前已作为与其他产品相关的类别,请使用主要去除剂作为相关类别且不提供任何信息u.Categories
由于 colocamos os 没有切片对环境和productEntity存储库UpdateProduct。
DeleteProduct:
func (s *service) DeleteProduct(ctx context.Context, id string) error {
exists, err := s.repo.GetProductByID(ctx, id)
if err != nil || !exists {
slog.Error("product not found", slog.String("product_id", id), slog.String("package", "productservice"))
return errors.New("product not found")
}
err = s.repo.DeleteProduct(ctx, id)
if err != nil {
return err
}
return nil
}
简单来说,就是购买产品,然后使用DELETE CASCADE联合联盟的表格进行注册。
FindManyProducts:
func (s *service) FindManyProducts(ctx context.Context, d dto.FindProductDto) ([]response.ProductResponse, error) {
products, err := s.repo.FindManyProducts(ctx, d)
if err != nil {
return nil, err
}
var productsResponse []response.ProductResponse
for _, p := range products {
var categories []response.CategoryResponse
for _, c := range p.Categories {
categories = append(categories, response.CategoryResponse{
ID: c.ID,
Title: c.Title,
})
}
productsResponse = append(productsResponse, response.ProductResponse{
ID: p.ID,
Title: p.Title,
Description: p.Description,
Price: p.Price,
Categories: categories,
CreatedAt: p.CreatedAt,
})
}
if len(productsResponse) == 0 {
return []response.ProductResponse{}, nil
}
return productsResponse, nil
}
最后,我们将FindManyProducts列出产品列表,将其存储在存储库中,并将其存储在ProductWithCategoryEntity前面的切片中。
这是一个服务和处理程序实现的主题。
Criando o repository
Primeiro vamos criar os arquivos、crie uma Pasta dentro de repository chamado类别存储库和产品存储库和 os arquivos product_interface_repository.go、product_repository.goe category_interface_repository.go、category_repository.go。
category_interface_repository.go:
func NewCategoryRepository(db *sql.DB, q *sqlc.Queries) CategoryRepository {
return &repository{
db,
q,
}
}
type repository struct {
db *sql.DB
queries *sqlc.Queries
}
type CategoryRepository interface {
CreateCategory(ctx context.Context, c *entity.CategoryEntity) error
}
product_interface_repository.go:
func NewProductRepository(db *sql.DB, q *sqlc.Queries) ProductRepository {
return &repository{
db,
q,
}
}
type repository struct {
db *sql.DB
queries *sqlc.Queries
}
type ProductRepository interface {
CreateProduct(ctx context.Context, p *entity.ProductEntity, c []entity.ProductCategoryEntity) error
GetCategoryByID(ctx context.Context, id string) (bool, error)
GetProductByID(ctx context.Context, id string) (bool, error)
UpdateProduct(ctx context.Context, p *entity.ProductEntity, c []entity.ProductCategoryEntity) error
GetCategoriesByProductID(ctx context.Context, id string) ([]string, error)
DeleteProductCategory(ctx context.Context, productID, categoryID string) error
DeleteProduct(ctx context.Context, id string) error
FindManyProducts(ctx context.Context, d dto.FindProductDto) ([]entity.ProductWithCategoryEntity, error)
}
Com isso deixamos nossa 接口 pronta。
criando o sql
Vamos criar nosso sql,vamos começar pela 迁移,骑行或comando:
make create_migration
Vamos ter uma nova 迁移 vazia na 面食迁移
CREATE TABLE category (
id CHAR(36) NOT NULL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP(3) NOT NULL
);
CREATE TABLE product (
id CHAR(36) NOT NULL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
price INTEGER NOT NULL,
description TEXT NULL,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP(3) NOT NULL
);
CREATE TABLE product_category (
id CHAR(36) NOT NULL PRIMARY KEY,
product_id VARCHAR(36) NOT NULL,
category_id VARCHAR(36) NOT NULL,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP(3) NOT NULL,
FOREIGN KEY (product_id) REFERENCES product(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
UNIQUE (product_id, category_id)
);
您可以对我们的客户作出回应,product_category如标签和联赛的标签,以及注册产品的术语。
DROP TABLE IF EXISTS product_category;
DROP TABLE IF EXISTS product;
DROP TABLE IF EXISTS category;
Esse sql acima é o que desfaz nossa 迁移。
Rodando 作为迁徙com o comando:
make migration_up
Agora ao acessar o banco, as tabelas já constam no banco, não se esqueça de iniciar o container com
docker-compose up -d
criando as consultas
Crie dois arquivos na 面食查询chamado categories.sqle products.sql:
categories.sql:
-- name: CreateCategory :exec
INSERT INTO category (id, title, created_at, updated_at)
VALUES ($1, $2, $3, $4);
Vamos ter apenas uma query de criação
products.sql:
-- name: CreateProduct :exec
INSERT INTO product (id, title, description, price, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6);
-- name: CreateProductCategory :exec
INSERT INTO product_category (id, product_id, category_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5);
-- name: GetCategoryByID :one
SELECT EXISTS (SELECT 1 FROM category WHERE id = $1) AS category_exists;
-- name: GetProductByID :one
SELECT EXISTS (SELECT 1 FROM product WHERE id = $1) AS product_exists;
-- name: UpdateProduct :exec
UPDATE product
SET
title = COALESCE(sqlc.narg('title'), title),
description = COALESCE(sqlc.narg('description'), description),
price = COALESCE(sqlc.narg('price'), price),
updated_at = $2
WHERE id = $1;
-- name: GetCategoriesByProductID :many
SELECT pc.category_id FROM product_category pc WHERE pc.product_id = $1;
-- name: DeleteProductCategory :exec
DELETE FROM product_category WHERE product_id = $1 AND category_id = $2;
-- name: DeleteProduct :exec
DELETE FROM product WHERE id = $1;
-- name: FindManyProducts :many
SELECT
p.id,
p.title,
p.description,
p.price,
p.created_at
FROM product p
JOIN product_category pc ON pc.product_id = p.id
WHERE
(pc.category_id = ANY(@categories::TEXT[]) OR @categories::TEXT[] IS NULL)
AND (
p.title ILIKE '%' || COALESCE(@search, sqlc.narg('search'), '') || '%'
OR
p.description ILIKE '%' || COALESCE(@search, sqlc.narg('search'), '') || '%'
)
ORDER BY p.created_at DESC;
-- name: GetProductCategories :many
SELECT c.id, c.title FROM category c
JOIN product_category pc ON pc.category_id = c.id
WHERE pc.product_id = $1;
Para o 产品 vamos ter mais 查询,mas todas simples,mais“elaborada”vai ser a de buscar produto GetProductCategories,mas tem um Problema causado pelo sqlc nessa 查询。
查询GetProductCategories将各种产品转为各种产品类别关联,并与其他产品类别关联,将产品分类为各种类别,从JOINtabela开始category,从 sqlc 到 struct aninhada com 或 slice 的结果Category,科莫阿西姆?
Se mudar a query para isso:
-- name: FindManyProducts :many
SELECT
p.id,
p.title,
p.description,
p.price,
p.created_at
c.* // retornando a category
FROM product p
JOIN product_category pc ON pc.product_id = p.id
JOIN category c ON c.id = pc.category_id //adicionando o join
WHERE
(pc.category_id = ANY(@categories::TEXT[]) OR @categories::TEXT[] IS NULL)
AND (
p.title ILIKE '%' || COALESCE(@search, sqlc.narg('search'), '') || '%'
OR
p.description ILIKE '%' || COALESCE(@search, sqlc.narg('search'), '') || '%'
)
ORDER BY p.created_at DESC;
e rodar o comando sqlc generate、 o sqlc vai gerar o código e fazer o scan、se acessar o arquivo gerado chamado products.sql.gona Pasta sqlc vamos ter a struct de retorno da FindManyProducts:
type FindManyProductsRow struct {
ID string
Title string
Description sql.NullString
Price int32
CreatedAt time.Time
ID_2 string
Title_2 string
CreatedAt_2 time.Time
UpdatedAt time.Time
}
修复 mesma 结构中的 sqlc 规范,以使其更准确:
type FindManyProductsRow struct {
ID string
Title string
Description sql.NullString
Price int32
CreatedAt time.Time
Categories []Categories
}
type FindManyProductsCategoriesRow struct {
ID string
Title string
}
如果您不使用 sqlc,则无法立即使用sqlc.embed,请更改查询 ficaria assim:
-- name: FindManyProducts :many
SELECT
p.id,
p.title,
p.description,
p.price,
p.created_at,
sqlc.embed(c) // usando o sqlc embed
FROM product p
JOIN product_category pc ON pc.product_id = p.id
JOIN category c ON c.id = pc.category_id
WHERE
(pc.category_id = ANY(@categories::TEXT[]) OR @categories::TEXT[] IS NULL)
AND (
p.title ILIKE '%' || COALESCE(@search, sqlc.narg('search'), '') || '%'
OR
p.description ILIKE '%' || COALESCE(@search, sqlc.narg('search'), '') || '%'
)
ORDER BY p.created_at DESC;
Agora no arquivo gerado pelo sqlc teremos:
type FindManyProductsRow struct {
ID string
Title string
Description sql.NullString
Price int32
CreatedAt time.Time
Category Category
}
如果 sqlc 没有任何切片,那么很简单,因为存在多种问题,所以没有 sqlc 存储库,因此可以讨论访问 aqui。
一个解决 sqlc 使用问题的解决方案,可以为您提供一个扩展的解决方案,一个延长总线产品的简单解决方案,可以将总线汽车作为forcada 产品类别,供用户查询GetProductCategories。
Ainda sobre a query FindManyProducts,fiz umabusca bem simples por categorias e texto,buscando por titleou description,éumasbusca onde podemos passar um slice decategories,e vai retornar se o produto tiver alguma das das 。
如果您使用巴士或searchfazemos 巴士,则可以使用产品 comAvião或标题来搜索巴士,但aviao不会返回结果,因为涉及特殊字符~。存在多种解决方案,从新的坎波斯查马search多示例开始,坎波斯一系列齐射或帕拉夫拉斯正常化,迪加莫斯奎或标题Avião Voador,没有坎波斯搜索ficaria Avião Voador,aviao voador,assim conseguimos公共汽车和SEM特征特殊。
找到解决办法,然后再进行工作,以解决所有问题。使用 Postgres chamado不重音的扩展方式,以不带口音的方式扩展,以正常的速度进行扩展。 Mas não vamos abordar no momento, talvez em um outro post。
实施存储库
类别存储库
CreateCategory:
func (r *repository) CreateCategory(ctx context.Context, c *entity.CategoryEntity) error {
err := r.queries.CreateCategory(ctx, sqlc.CreateCategoryParams{
ID: c.ID,
Title: c.Title,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
})
if err != nil {
return err
}
return nil
}
Apenas salvamos 是一个新类别。
产品库
CreateProduct:
func (r *repository) CreateProduct(ctx context.Context, p *entity.ProductEntity, c []entity.ProductCategoryEntity) error {
err := transaction.Run(ctx, r.db, func(q *sqlc.Queries) error {
var err error
err = q.CreateProduct(ctx, sqlc.CreateProductParams{
ID: p.ID,
Title: p.Title,
Price: p.Price,
Description: sql.NullString{String: p.Description, Valid: p.Description != ""},
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
})
if err != nil {
return err
}
for _, category := range c {
err = q.CreateProductCategory(ctx, sqlc.CreateProductCategoryParams{
ID: category.ID,
ProductID: p.ID,
CategoryID: category.CategoryID,
CreatedAt: category.CreatedAt,
UpdatedAt: category.UpdatedAt,
})
if err != nil {
return err
}
}
return nil
})
if err != nil {
slog.Error("error to create product, roll back applied", "err", err)
return err
}
return nil
}
我们将产品作为交易使用,首先将产品和产品存放在for萨尔瓦卡达CreateProductCategory。
GetCategoryByID:
func (r *repository) GetCategoryByID(ctx context.Context, id string) (bool, error) {
exists, err := r.queries.GetCategoryByID(ctx, id)
if err != nil || err == sql.ErrNoRows {
return false, err
}
return exists, nil
}
Usamos parabuscar uma categoria pelo id, apenas para validar se existe。
GetProductByID:
func (r *repository) GetProductByID(ctx context.Context, id string) (bool, error) {
exists, err := r.queries.GetProductByID(ctx, id)
if err != nil || err == sql.ErrNoRows {
return false, err
}
return exists, nil
}
Buscamos um produto pelo id,apenas para validar se 存在。
UpdateProduct:
func (r *repository) UpdateProduct(ctx context.Context, p *entity.ProductEntity, c []entity.ProductCategoryEntity) error {
err := transaction.Run(ctx, r.db, func(q *sqlc.Queries) error {
var err error
err = q.UpdateProduct(ctx, sqlc.UpdateProductParams{
ID: p.ID,
Title: sql.NullString{String: p.Title, Valid: p.Title != ""},
Price: sql.NullInt32{Int32: p.Price, Valid: p.Price != 0},
Description: sql.NullString{String: p.Description, Valid: p.Description != ""},
UpdatedAt: p.UpdatedAt,
})
if err != nil {
return err
}
for _, category := range c {
err = q.CreateProductCategory(ctx, sqlc.CreateProductCategoryParams{
ID: category.ID,
ProductID: p.ID,
CategoryID: category.CategoryID,
CreatedAt: category.CreatedAt,
UpdatedAt: category.UpdatedAt,
})
if err != nil {
return err
}
}
return nil
})
if err != nil {
slog.Error("error to update product, roll back applied", "err", err)
return err
}
return nil
}
sql.NullString可以通过交易、使用交易和使用可能的潜在价值来实现产品和服务sql.NullInt32。
GetCategoriesByProductID:
func (r *repository) GetCategoriesByProductID(ctx context.Context, id string) ([]string, error) {
categories, err := r.queries.GetCategoriesByProductID(ctx, id)
if err != nil {
return nil, err
}
return categories, nil
}
查看产品类别。
DeleteProductCategory:
func (r *repository) DeleteProductCategory(ctx context.Context, productID, categoryID string) error {
err := r.queries.DeleteProductCategory(ctx, sqlc.DeleteProductCategoryParams{
ProductID: productID,
CategoryID: categoryID,
})
if err != nil {
return err
}
return nil
}
请注意联赛表的注册。
DeleteProduct:
func (r *repository) DeleteProduct(ctx context.Context, id string) error {
err := r.queries.DeleteProduct(ctx, id)
if err != nil {
return err
}
return nil
}
删除我们的产品。
FindManyProducts:
func (r *repository) FindManyProducts(ctx context.Context, d dto.FindProductDto) ([]entity.ProductWithCategoryEntity, error) {
products, err := r.queries.FindManyProducts(ctx, sqlc.FindManyProductsParams{
Categories: d.Categories,
Search: sql.NullString{String: d.Search, Valid: d.Search != ""},
})
if err != nil {
return nil, err
}
var response []entity.ProductWithCategoryEntity
for _, p := range products {
var category []entity.CategoryEntity
categories, err := r.queries.GetProductCategories(ctx, p.ID)
if err != nil {
return nil, err
}
for _, c := range categories {
category = append(category, entity.CategoryEntity{
ID: c.ID,
Title: c.Title,
})
}
response = append(response, entity.ProductWithCategoryEntity{
ID: p.ID,
Title: p.Title,
Description: p.Description.String,
Price: p.Price,
Categories: category,
CreatedAt: p.CreatedAt,
})
}
return response, nil
}
最后,总线或产品、aqui tem o que mencionei sobre abusca de categorias
for _, c := range categories {
category = append(category, entity.CategoryEntity{
ID: c.ID,
Title: c.Title,
})
}
Nesse forBuscamos 为初级产品类别for。
调整主
都多快点! mas para rodar,precisamos instanciar nossos services no main.goe passar para o 处理程序:
// user
userRepo := userrepository.NewUserRepository(dbConnection, queries)
newUserService := userservice.NewUserService(userRepo)
// category
categoryRepo := categoryrepository.NewCategoryRepository(dbConnection, queries)
newCategoryService := categoryservice.NewCategoryService(categoryRepo)
// product
productRepo := productrepository.NewProductRepository(dbConnection, queries)
productsService := productservice.NewProductService(productRepo)
newHandler := handler.NewHandler(newUserService, newCategoryService, productsService)
// init routes
router := chi.NewRouter()
routes.InitRoutes(router, newHandler)
routes.InitDocsRoutes(router)
Agora Podemos rodas nossa applicação 和 testar com go run cmd/webserver/main.go:
adicionando as chamadas no gttp_client.http:
### Products
## CreateProduct
POST http://localhost:8080/product HTTP/1.1
content-type: application/json
Authorization: Bearer {{token}}
{
"title": "Samsung",
"description": "Celular bacana",
"categories": ["category_id"],
"price": 39900
}
###
## UpdateProduct
PATCH http://localhost:8080/product/37545729-e891-40b5-946c-8e7d55bd686b HTTP/1.1
content-type: application/json
Authorization: Bearer {{token}}
{
"categories": ["07145e70-2a8e-4f71-9165-f0d450afa524"]
}
###
## DeleteProduct
DELETE http://localhost:8080/product/f720e1ce-cb88-4f72-a765-0250c1a525e3 HTTP/1.1
content-type: application/json
Authorization: Bearer {{token}}
###
## FindManyProducts
GET http://localhost:8080/product HTTP/1.1
content-type: application/json
Authorization: Bearer {{token}}
{
"categories": ["category_id"]
}
Agora é só brincar fazer as chamadas 和 criar 产品和类别。
Documentando
Para Finalizar,precisamos apenas 纪录片 nossos novos 端点。
category_handler.go:
// Create category
// @Summary Create new category
// @Description Endpoint for create category
// @Tags category
// @Accept json
// @Produce json
// @Param body body dto.CreateCategoryDto true "Create category dto" true
// @Success 200
// @Failure 400 {object} httperr.RestErr
// @Failure 500 {object} httperr.RestErr
// @Router /category [post]
func (h *handler) CreateCategory(w http.ResponseWriter, r *http.Request) {}
product_handler.go:
// Create product
// @Summary Create new product
// @Description Endpoint for create product
// @Tags product
// @Accept json
// @Produce json
// @Param body body dto.CreateProductDto true "Create product dto" true
// @Success 200
// @Failure 400 {object} httperr.RestErr
// @Failure 500 {object} httperr.RestErr
// @Router /product [post]
func (h *handler) CreateProduct(w http.ResponseWriter, r *http.Request) {}
// Update product
// @Summary Update product
// @Description Endpoint for update product
// @Tags product
// @Accept json
// @Produce json
// @Param body body dto.UpdateProductDto true "Update product dto" true
// @Param id path string true "product id"
// @Success 200
// @Failure 400 {object} httperr.RestErr
// @Failure 500 {object} httperr.RestErr
// @Router /productt/{id} [patch]
func (h *handler) UpdateProduct(w http.ResponseWriter, r *http.Request) {}
// Delete product
// @Summary Delete product
// @Description Endpoint for update product
// @Tags product
// @Accept json
// @Produce json
// @Param id path string true "product id"
// @Success 200
// @Failure 400 {object} httperr.RestErr
// @Failure 500 {object} httperr.RestErr
// @Router /product/{id} [delete]
func (h *handler) DeleteProduct(w http.ResponseWriter, r *http.Request)
// Search products
// @Summary Search products
// @Description Endpoint for search product
// @Tags product
// @Accept json
// @Produce json
// @Param body body dto.FindProductDto true "Search products" true
// @Success 200 {object} response.ProductResponse
// @Failure 400 {object} httperr.RestErr
// @Failure 500 {object} httperr.RestErr
// @Router /product [get]
func (h *handler) FindManyProducts(w http.ResponseWriter, r *http.Request) {}
Vamos rodar dois comandos do swag, o primeiro para formatar:
swag fmt
第二部分是开放 API 的文档。
swag init -g internal/handler/routes/docs_route.go
展示! Agora rodando 或项目和访问 url http://localhost:8080/docs/index.html#/vamos ter nossa pronta:
最终考虑因素
Nesse posta vimos mais sobre como usar sqlc, transactions, e praticamos um pouco mais do que abordamos nos previous posts.
这就是我们的故事。 Mas você deve estar se perguntando e os testes? Mas você deve estar se perguntando e os testes? Bom,não fakeo abordar os teste nesse série,para não ficar algo imenso。 Mas seguindo a pirâmide de teste , o teste unitário não agregaria muito valor a nossa api, uma vez que boaparte da nossa api é pegar bado e salvar no banco, com teste unitário teríamos que criar mocks do banco, no estaríamos testado nada de fato, mas claro que os teste unitários Também são importantes,mas nosso cenário não iria gerar muito valor ao nosso código。
由于 trazer 假装在使用测试容器集成 api 的单独测试中使用了测试容器,因此该测试与 nossa api 的价值相同,并且在不使用模拟的情况下进行测试。
请根据实际情况添加时事通讯。
Espero que vocês tenham curtido a 系列。
仓库链接
项目存储库
link do projeto no meu blog
Se inscreva e receba um aviso sobre novos posts, participar
文章来源:https://dev.to/wiliamvj/api-completa-em-golang-parte-7-4ekg



