diff --git a/cmd/import_breeds/main.go b/cmd/import_breeds/main.go index 484f12fb..89b10436 100644 --- a/cmd/import_breeds/main.go +++ b/cmd/import_breeds/main.go @@ -2,11 +2,12 @@ package main import ( "context" - "database/sql" - "errors" "flag" "log" + utils "github.com/pet-sitter/pets-next-door-api/internal/common" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" + "github.com/pet-sitter/pets-next-door-api/internal/domain/breed" "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" @@ -15,7 +16,6 @@ import ( pnd "github.com/pet-sitter/pets-next-door-api/api" "github.com/pet-sitter/pets-next-door-api/internal/configs" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" - "github.com/pet-sitter/pets-next-door-api/internal/postgres" ) func main() { @@ -93,45 +93,48 @@ func parseFlags() Flags { func importBreed( ctx context.Context, conn *database.DB, petType commonvo.PetType, row breedsimporterservice.Row, -) (*breed.Breed, *pnd.AppError) { +) (*breed.DetailView, *pnd.AppError) { log.Printf("Importing breed with pet_type: %s, name: %s to database", petType, row.Breed) - var breedData *breed.Breed - err := database.WithTransaction(ctx, conn, func(tx *database.Tx) *pnd.AppError { - existing, err := postgres.FindBreedByPetTypeAndName(ctx, tx, petType, row.Breed) - if err != nil && !errors.Is(err.Err, sql.ErrNoRows) { - return err - } - - if existing != nil { - log.Printf( - "Breed with id: %d, pet_type: %s, name: %s already exists in database", - existing.ID, - existing.PetType, - existing.Name, - ) - breedData = existing - return nil - } - - breedData, err = postgres.CreateBreed(ctx, tx, &breed.Breed{PetType: petType, Name: row.Breed}) - if err != nil { - return err - } + existingList, err := databasegen.New(conn).FindBreeds(ctx, databasegen.FindBreedsParams{ + PetType: utils.StrToNullStr(petType.String()), + Name: utils.StrToNullStr(row.Breed), + }) + if err != nil { + return nil, pnd.FromPostgresError(err) + } + if len(existingList) > 1 { + existing := existingList[0] log.Printf( - "Succeeded to import breed with id: %d, pet_type: %s, name: %s to database", - breedData.ID, - breedData.PetType, - breedData.Name, + "Breed with id: %d, pet_type: %s, name: %s already exists in database", + existing.ID, + existing.PetType, + existing.Name, ) - return nil + return breed.ToDetailViewFromRows(existing), nil + } + + breedData, err := databasegen.New(conn).CreateBreed(ctx, databasegen.CreateBreedParams{ + Name: row.Breed, + PetType: petType.String(), }) if err != nil { - return nil, err + return nil, pnd.FromPostgresError(err) } - return breedData, nil + log.Printf( + "Succeeded to import breed with id: %d, pet_type: %s, name: %s to database", + breedData.ID, + breedData.PetType, + breedData.Name, + ) + + return &breed.DetailView{ + ID: int64(breedData.ID), + PetType: commonvo.PetType(breedData.PetType), + Name: breedData.Name, + }, nil } func importBreeds(ctx context.Context, conn *database.DB, petType commonvo.PetType, rows *[]breedsimporterservice.Row) { diff --git a/cmd/server/handler/breed_handler.go b/cmd/server/handler/breed_handler.go index 7cdab7a7..3d49fd91 100644 --- a/cmd/server/handler/breed_handler.go +++ b/cmd/server/handler/breed_handler.go @@ -3,6 +3,8 @@ package handler import ( "net/http" + "github.com/pet-sitter/pets-next-door-api/internal/domain/breed" + "github.com/labstack/echo/v4" pnd "github.com/pet-sitter/pets-next-door-api/api" "github.com/pet-sitter/pets-next-door-api/internal/service" @@ -25,7 +27,7 @@ func NewBreedHandler(breedService service.BreedService) *BreedHandler { // @Param page query int false "페이지 번호" default(1) // @Param size query int false "페이지 사이즈" default(20) // @Param pet_type query string false "펫 종류" Enums(dog, cat) -// @Success 200 {object} breed.BreedListView +// @Success 200 {object} breed.ListView // @Router /breeds [get] func (h *BreedHandler) FindBreeds(c echo.Context) error { petType := pnd.ParseOptionalStringQuery(c, "pet_type") @@ -34,7 +36,11 @@ func (h *BreedHandler) FindBreeds(c echo.Context) error { return c.JSON(err.StatusCode, err) } - res, err := h.breedService.FindBreeds(c.Request().Context(), page, size, petType) + res, err := h.breedService.FindBreeds(c.Request().Context(), &breed.FindBreedsParams{ + Page: page, + Size: size, + PetType: petType, + }) if err != nil { return c.JSON(err.StatusCode, err) } diff --git a/internal/domain/breed/model.go b/internal/domain/breed/model.go deleted file mode 100644 index 922223ea..00000000 --- a/internal/domain/breed/model.go +++ /dev/null @@ -1,37 +0,0 @@ -package breed - -import ( - "context" - - "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" - "github.com/pet-sitter/pets-next-door-api/internal/infra/database" - - pnd "github.com/pet-sitter/pets-next-door-api/api" -) - -type Breed struct { - ID int `field:"id"` - Name string `field:"name"` - PetType commonvo.PetType `field:"pet_type"` - CreatedAt string `field:"created_at"` - UpdatedAt string `field:"updated_at"` - DeletedAt string `field:"deleted_at"` -} - -type BreedList struct { - *pnd.PaginatedView[Breed] -} - -func NewBreedList(page, size int) *BreedList { - return &BreedList{PaginatedView: pnd.NewPaginatedView( - page, size, false, make([]Breed, 0), - )} -} - -type BreedStore interface { - FindBreeds(ctx context.Context, tx *database.Tx, page, size int, petType *string) (*BreedList, *pnd.AppError) - FindBreedByPetTypeAndName( - ctx context.Context, tx *database.Tx, petType commonvo.PetType, name string, - ) (*Breed, *pnd.AppError) - CreateBreed(ctx context.Context, tx *database.Tx, breed *Breed) (*Breed, *pnd.AppError) -} diff --git a/internal/domain/breed/params.go b/internal/domain/breed/params.go new file mode 100644 index 00000000..78b9bafa --- /dev/null +++ b/internal/domain/breed/params.go @@ -0,0 +1,23 @@ +package breed + +import ( + utils "github.com/pet-sitter/pets-next-door-api/internal/common" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" +) + +type FindBreedsParams struct { + Page int + Size int + PetType *string + IncludeDeleted bool +} + +func (p *FindBreedsParams) ToDBParams() databasegen.FindBreedsParams { + pagination := utils.OffsetAndLimit(p.Page, p.Size) + return databasegen.FindBreedsParams{ + Limit: int32(pagination.Limit), + Offset: int32(pagination.Offset), + PetType: utils.StrPtrToNullStr(p.PetType), + IncludeDeleted: p.IncludeDeleted, + } +} diff --git a/internal/domain/breed/view.go b/internal/domain/breed/view.go index 448c6d6c..f0ffd8c9 100644 --- a/internal/domain/breed/view.go +++ b/internal/domain/breed/view.go @@ -3,31 +3,33 @@ package breed import ( pnd "github.com/pet-sitter/pets-next-door-api/api" "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" ) -type BreedView struct { - ID int `json:"id"` +type DetailView struct { + ID int64 `json:"id"` PetType commonvo.PetType `json:"petType"` Name string `json:"name"` } -type BreedListView struct { - *pnd.PaginatedView[*BreedView] +func ToDetailViewFromRows(row databasegen.FindBreedsRow) *DetailView { + return &DetailView{ + ID: int64(row.ID), + PetType: commonvo.PetType(row.PetType), + Name: row.Name, + } } -func (breeds *BreedList) ToBreedListView() *BreedListView { - breedViews := make([]*BreedView, len(breeds.Items)) - for i, breed := range breeds.Items { - breedViews[i] = &BreedView{ - ID: breed.ID, - PetType: breed.PetType, - Name: breed.Name, - } - } +type ListView struct { + *pnd.PaginatedView[*DetailView] +} - return &BreedListView{ - PaginatedView: pnd.NewPaginatedView( - breeds.Page, breeds.Size, breeds.IsLastPage, breedViews, - ), +func ToListViewFromRows(page, size int, rows []databasegen.FindBreedsRow) *ListView { + bl := &ListView{PaginatedView: pnd.NewPaginatedView(page, size, false, make([]*DetailView, len(rows)))} + for i, row := range rows { + bl.Items[i] = ToDetailViewFromRows(row) } + + bl.CalcLastPage() + return bl } diff --git a/internal/domain/commonvo/vo.go b/internal/domain/commonvo/vo.go index b45c17e0..4ceea6be 100644 --- a/internal/domain/commonvo/vo.go +++ b/internal/domain/commonvo/vo.go @@ -6,3 +6,7 @@ const ( PetTypeDog PetType = "dog" PetTypeCat PetType = "cat" ) + +func (p *PetType) String() string { + return string(*p) +} diff --git a/internal/infra/database/gen/breeds.sql.go b/internal/infra/database/gen/breeds.sql.go new file mode 100644 index 00000000..5db89a99 --- /dev/null +++ b/internal/infra/database/gen/breeds.sql.go @@ -0,0 +1,112 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: breeds.sql + +package databasegen + +import ( + "context" + "database/sql" + "time" +) + +const createBreed = `-- name: CreateBreed :one +INSERT INTO breeds (name, + pet_type, + created_at, + updated_at) +VALUES ($1, $2, NOW(), NOW()) +RETURNING id, pet_type, name, created_at, updated_at +` + +type CreateBreedParams struct { + Name string + PetType string +} + +type CreateBreedRow struct { + ID int32 + PetType string + Name string + CreatedAt time.Time + UpdatedAt time.Time +} + +func (q *Queries) CreateBreed(ctx context.Context, arg CreateBreedParams) (CreateBreedRow, error) { + row := q.db.QueryRowContext(ctx, createBreed, arg.Name, arg.PetType) + var i CreateBreedRow + err := row.Scan( + &i.ID, + &i.PetType, + &i.Name, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const findBreeds = `-- name: FindBreeds :many +SELECT id, + name, + pet_type, + created_at, + updated_at +FROM breeds +WHERE (pet_type = $3 OR $3 IS NULL) + AND (name = $4 OR $4 IS NULL) + AND (deleted_at IS NULL OR $5::boolean = TRUE) +ORDER BY id +LIMIT $1 OFFSET $2 +` + +type FindBreedsParams struct { + Limit int32 + Offset int32 + PetType sql.NullString + Name sql.NullString + IncludeDeleted bool +} + +type FindBreedsRow struct { + ID int32 + Name string + PetType string + CreatedAt time.Time + UpdatedAt time.Time +} + +func (q *Queries) FindBreeds(ctx context.Context, arg FindBreedsParams) ([]FindBreedsRow, error) { + rows, err := q.db.QueryContext(ctx, findBreeds, + arg.Limit, + arg.Offset, + arg.PetType, + arg.Name, + arg.IncludeDeleted, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FindBreedsRow + for rows.Next() { + var i FindBreedsRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.PetType, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/postgres/breed_store.go b/internal/postgres/breed_store.go deleted file mode 100644 index c58529ae..00000000 --- a/internal/postgres/breed_store.go +++ /dev/null @@ -1,123 +0,0 @@ -package postgres - -import ( - "context" - - "github.com/pet-sitter/pets-next-door-api/internal/domain/breed" - "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" - - pnd "github.com/pet-sitter/pets-next-door-api/api" - "github.com/pet-sitter/pets-next-door-api/internal/infra/database" -) - -func FindBreeds( - ctx context.Context, tx *database.Tx, page, size int, petType *string) (*breed.BreedList, *pnd.AppError, -) { - const sql = ` - SELECT - id, - name, - pet_type, - created_at, - updated_at - FROM - breeds - WHERE - (pet_type = $1 OR $1 IS NULL) AND - deleted_at IS NULL - ORDER BY id ASC - LIMIT $2 - OFFSET $3 - ` - - breedList := breed.NewBreedList(page, size) - rows, err := tx.QueryContext(ctx, sql, petType, size+1, (page-1)*size) - if err != nil { - return nil, pnd.FromPostgresError(err) - } - defer rows.Close() - - for rows.Next() { - breedData := &breed.Breed{} - if err := rows.Scan( - &breedData.ID, &breedData.Name, &breedData.PetType, &breedData.CreatedAt, &breedData.UpdatedAt, - ); err != nil { - return nil, pnd.FromPostgresError(err) - } - breedList.Items = append(breedList.Items, *breedData) - } - if err := rows.Err(); err != nil { - return nil, pnd.FromPostgresError(err) - } - - breedList.CalcLastPage() - return breedList, nil -} - -func FindBreedByPetTypeAndName( - ctx context.Context, tx *database.Tx, petType commonvo.PetType, name string, -) (*breed.Breed, *pnd.AppError) { - const sql = ` - SELECT - id, - name, - pet_type, - created_at, - updated_at - FROM - breeds - WHERE - pet_type = $1 AND - name = $2 AND - deleted_at IS NULL - ` - - breedData := &breed.Breed{} - if err := tx.QueryRowContext(ctx, sql, - petType, - name, - ).Scan( - &breedData.ID, - &breedData.Name, - &breedData.PetType, - &breedData.CreatedAt, - &breedData.UpdatedAt, - ); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return breedData, nil -} - -func CreateBreed(ctx context.Context, tx *database.Tx, breedData *breed.Breed) (*breed.Breed, *pnd.AppError) { - const sql = ` - INSERT INTO - breeds - ( - id, - name, - pet_type, - created_at, - updated_at - ) - VALUES - (DEFAULT, $1, $2, DEFAULT, DEFAULT) - RETURNING - id, pet_type, name, created_at, updated_at - ` - - if err := tx.QueryRowContext(ctx, sql, - breedData.Name, - breedData.PetType, - ).Scan( - &breedData.ID, - &breedData.PetType, - &breedData.Name, - &breedData.CreatedAt, - &breedData.UpdatedAt, - ); err != nil { - return nil, pnd.FromPostgresError(err) - } - - return breedData, nil -} diff --git a/internal/service/breed_service.go b/internal/service/breed_service.go index ce7bc255..33ff663c 100644 --- a/internal/service/breed_service.go +++ b/internal/service/breed_service.go @@ -3,12 +3,11 @@ package service import ( "context" - "github.com/pet-sitter/pets-next-door-api/internal/domain/breed" - "github.com/pet-sitter/pets-next-door-api/internal/domain/commonvo" + databasegen "github.com/pet-sitter/pets-next-door-api/internal/infra/database/gen" pnd "github.com/pet-sitter/pets-next-door-api/api" + "github.com/pet-sitter/pets-next-door-api/internal/domain/breed" "github.com/pet-sitter/pets-next-door-api/internal/infra/database" - "github.com/pet-sitter/pets-next-door-api/internal/postgres" ) type BreedService struct { @@ -22,47 +21,12 @@ func NewBreedService(conn *database.DB) *BreedService { } func (s *BreedService) FindBreeds( - ctx context.Context, page, size int, petType *string, -) (*breed.BreedListView, *pnd.AppError) { - tx, err := s.conn.BeginTx(ctx) - defer tx.Rollback() - if err != nil { - return nil, err - } - - breeds, err := postgres.FindBreeds(ctx, tx, page, size, petType) + ctx context.Context, params *breed.FindBreedsParams, +) (*breed.ListView, *pnd.AppError) { + rows, err := databasegen.New(s.conn).FindBreeds(ctx, params.ToDBParams()) if err != nil { - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err - } - - return breeds.ToBreedListView(), nil -} - -func (s *BreedService) FindBreedByPetTypeAndName( - ctx context.Context, petType commonvo.PetType, name string, -) (*breed.BreedView, *pnd.AppError) { - tx, err := s.conn.BeginTx(ctx) - defer tx.Rollback() - if err != nil { - return nil, err - } - - breedData, err := postgres.FindBreedByPetTypeAndName(ctx, tx, petType, name) - if err != nil { - return nil, err - } - - if err := tx.Commit(); err != nil { - return nil, err + return nil, pnd.FromPostgresError(err) } - return &breed.BreedView{ - ID: breedData.ID, - PetType: breedData.PetType, - Name: breedData.Name, - }, nil + return breed.ToListViewFromRows(params.Page, params.Size, rows), nil } diff --git a/queries/breeds.sql b/queries/breeds.sql new file mode 100644 index 00000000..58c0419e --- /dev/null +++ b/queries/breeds.sql @@ -0,0 +1,20 @@ +-- name: CreateBreed :one +INSERT INTO breeds (name, + pet_type, + created_at, + updated_at) +VALUES ($1, $2, NOW(), NOW()) +RETURNING id, pet_type, name, created_at, updated_at; + +-- name: FindBreeds :many +SELECT id, + name, + pet_type, + created_at, + updated_at +FROM breeds +WHERE (pet_type = sqlc.narg('pet_type') OR sqlc.narg('pet_type') IS NULL) + AND (name = sqlc.narg('name') OR sqlc.narg('name') IS NULL) + AND (deleted_at IS NULL OR sqlc.arg('include_deleted')::boolean = TRUE) +ORDER BY id +LIMIT $1 OFFSET $2;