发布于 2026-01-06 9 阅读
0

API 完整 Golang - 第 7 部分

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.goauth_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)
Enter fullscreen mode Exit fullscreen mode

作为实体的 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"`
  }
Enter fullscreen mode Exit fullscreen mode

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"`
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

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)
  }
Enter fullscreen mode Exit fullscreen mode

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"`
  }
Enter fullscreen mode Exit fullscreen mode

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)
  }
Enter fullscreen mode Exit fullscreen mode

如果处理程序是基本的,并且在使用过程中遇到了一些问题,则可能会出现错误,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"`
  }
Enter fullscreen mode Exit fullscreen mode

Vamos ter 3 dtos para o 产品CreateProductDtoUpdateProductDtoe 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)
  }
Enter fullscreen mode Exit fullscreen mode

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)
  }
Enter fullscreen mode Exit fullscreen mode

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)
  }
Enter fullscreen mode Exit fullscreen mode

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)
  }
Enter fullscreen mode Exit fullscreen mode

必须采取的方法,请务必将前面的帖子分开。

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
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

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)
  }
Enter fullscreen mode Exit fullscreen mode

修复 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"`
  }
Enter fullscreen mode Exit fullscreen mode

请查看产品类别。

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"`
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

没有服务 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。

我希望您能对银行提供的服务负责,并为银行提供服务,以确保银行产品的准确性,并为银行提供产品和产品类别提供帮助。存储库错误CreateProductGetCategoryByIDainda 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
  }
Enter fullscreen mode Exit fullscreen mode

基本的方法是,产品的功能非常丰富,产品的功能也很丰富,产品的分类也很丰富,产品的外观和产品的信息都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")
    }
  }
Enter fullscreen mode Exit fullscreen mode

Primeiro verificamos 被认为是不存在的信息类别categories

  productCategories, err := s.repo.GetCategoriesByProductID(ctx, id)
  if err != nil {
    return errors.New("error getting categories by product id")
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

简单来说,就是购买产品,然后使用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
  }
Enter fullscreen mode Exit fullscreen mode

最后,我们将FindManyProducts列出产品列表,将其存储在存储库中,并将其存储在ProductWithCategoryEntity前面的切片中。

这是一个服务和处理程序实现的主题。

Criando o repository

Primeiro vamos criar os arquivos、crie uma Pasta dentro de repository chamado类别存储库和产品存储库和 os arquivos
product_interface_repository.goproduct_repository.goe category_interface_repository.gocategory_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
  }
Enter fullscreen mode Exit fullscreen mode

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)
  }
Enter fullscreen mode Exit fullscreen mode

Com isso deixamos nossa 接口 pronta。

criando o sql

Vamos criar nosso sql,vamos começar pela 迁移,骑行或comando:

  make create_migration
Enter fullscreen mode Exit fullscreen mode

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)
  );
Enter fullscreen mode Exit fullscreen mode

您可以对我们的客户作出回应,product_category如标签和联赛的标签,以及注册产品的术语。

  DROP TABLE IF EXISTS product_category;
  DROP TABLE IF EXISTS product;
  DROP TABLE IF EXISTS category;
Enter fullscreen mode Exit fullscreen mode

Esse sql acima é o que desfaz nossa 迁移。

Rodando 作为迁徙com o comando:

  make migration_up
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

修复 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
  }
Enter fullscreen mode Exit fullscreen mode

如果您不使用 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;
Enter fullscreen mode Exit fullscreen mode

Agora no arquivo gerado pelo sqlc teremos:

  type FindManyProductsRow struct {
    ID          string
    Title       string
    Description sql.NullString
    Price       int32
    CreatedAt   time.Time
    Category    Category
  }
Enter fullscreen mode Exit fullscreen mode

如果 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
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

我们将产品作为交易使用,首先将产品和产品存放在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
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

查看产品类别。

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
  }
Enter fullscreen mode Exit fullscreen mode

请注意联赛表的注册。

DeleteProduct

  func (r *repository) DeleteProduct(ctx context.Context, id string) error {
    err := r.queries.DeleteProduct(ctx, id)
    if err != nil {
      return err
    }
    return nil
  }
Enter fullscreen mode Exit fullscreen mode

删除我们的产品。

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
  }
Enter fullscreen mode Exit fullscreen mode

最后,总线或产品、aqui tem o que mencionei sobre abusca de categorias

  for _, c := range categories {
      category = append(category, entity.CategoryEntity{
      ID:    c.ID,
      Title: c.Title,
    })
  }
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

Agora é só brincar fazer as chamadas 和 criar 产品和类别。

gif

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) {}
Enter fullscreen mode Exit fullscreen mode

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) {}
Enter fullscreen mode Exit fullscreen mode
// 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) {}
Enter fullscreen mode Exit fullscreen mode
// 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)
Enter fullscreen mode Exit fullscreen mode
//  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) {}
Enter fullscreen mode Exit fullscreen mode

Vamos rodar dois comandos do swag, o primeiro para formatar:

  swag fmt
Enter fullscreen mode Exit fullscreen mode

第二部分是开放 API 的文档。

  swag init -g internal/handler/routes/docs_route.go
Enter fullscreen mode Exit fullscreen mode

展示! 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