Pārlūkot izejas kodu

perf: N+1 в GetByID — один JOIN вместо 3 запросов

PlaceRepo.GetByIDWithDetails: LEFT JOIN + json_agg для tags/features
PlaceService.GetByID: GetByIDWithDetails когда fetchTags=true
neyrogovnarik 1 mēnesi atpakaļ
vecāks
revīzija
5059f29117

+ 49 - 0
backend/internal/repository/places.go

@@ -3,6 +3,7 @@ package repository
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 
 	"github.com/jackc/pgx/v5"
@@ -59,6 +60,54 @@ func (r *PlaceRepo) GetByID(ctx context.Context, id string) (*models.Place, erro
 	return scanPlace(row)
 }
 
+func (r *PlaceRepo) GetByIDWithDetails(ctx context.Context, id string) (*models.Place, error) {
+	query := `
+		SELECT p.id, p.type, p.owner_id, p.title, p.description, p.address,
+		       ST_Y(p.coordinates::geometry), ST_X(p.coordinates::geometry),
+		       p.cover_image, p.access_info, p.status, p.moderation_comment,
+		       p.rating, p.reviews_count, p.hourly_rate, p.currency, p.min_hours, p.booking_url,
+		       p.created_at, p.updated_at, p.published_at, p.deleted_at,
+		       COALESCE(json_agg(DISTINCT jsonb_build_object('id', t.id, 'name', t.name, 'category', t.category))
+		                FILTER (WHERE t.id IS NOT NULL), '[]'::jsonb) as tags,
+		       COALESCE(json_agg(DISTINCT jsonb_build_object('id', f.id, 'name', f.name, 'category', f.category, 'icon', f.icon))
+		                FILTER (WHERE f.id IS NOT NULL), '[]'::jsonb) as features
+		FROM places p
+		LEFT JOIN place_tags pt ON pt.place_id = p.id
+		LEFT JOIN tags t ON t.id = pt.tag_id
+		LEFT JOIN place_features pf ON pf.place_id = p.id
+		LEFT JOIN features f ON f.id = pf.feature_id
+		WHERE p.id = $1 AND p.deleted_at IS NULL
+		GROUP BY p.id`
+
+	row := r.pool.QueryRow(ctx, query, id)
+
+	var p models.Place
+	var tagsJSON, featuresJSON []byte
+	err := row.Scan(
+		&p.ID, &p.Type, &p.OwnerID, &p.Title, &p.Description, &p.Address,
+		&p.Lat, &p.Lng,
+		&p.CoverImage, &p.AccessInfo, &p.Status, &p.ModerationComment,
+		&p.Rating, &p.ReviewsCount, &p.HourlyRate, &p.Currency, &p.MinHours, &p.BookingURL,
+		&p.CreatedAt, &p.UpdatedAt, &p.PublishedAt, &p.DeletedAt,
+		&tagsJSON, &featuresJSON,
+	)
+	if err != nil {
+		if err == pgx.ErrNoRows {
+			return nil, nil
+		}
+		return nil, fmt.Errorf("get place with details: %w", err)
+	}
+
+	if err := json.Unmarshal(tagsJSON, &p.Tags); err != nil {
+		return nil, fmt.Errorf("unmarshal tags: %w", err)
+	}
+	if err := json.Unmarshal(featuresJSON, &p.Features); err != nil {
+		return nil, fmt.Errorf("unmarshal features: %w", err)
+	}
+
+	return &p, nil
+}
+
 func (r *PlaceRepo) List(ctx context.Context, filter models.PlaceFilter) ([]*models.Place, error) {
 	q := `SELECT p.id, p.type, p.owner_id, p.title, p.description, p.address,
 	             ST_Y(p.coordinates::geometry), ST_X(p.coordinates::geometry),

+ 3 - 20
backend/internal/services/places.go

@@ -55,6 +55,7 @@ type ModerationLogRepo interface {
 type PlaceRepo interface {
 	Create(ctx context.Context, place *models.Place) error
 	GetByID(ctx context.Context, id string) (*models.Place, error)
+	GetByIDWithDetails(ctx context.Context, id string) (*models.Place, error)
 	GetByIDRaw(ctx context.Context, id string) (*models.Place, error)
 	List(ctx context.Context, filter models.PlaceFilter) ([]*models.Place, error)
 	Update(ctx context.Context, place *models.Place) error
@@ -131,29 +132,11 @@ func (s *PlaceService) Create(ctx context.Context, input CreatePlaceInput) (*mod
 }
 
 func (s *PlaceService) GetByID(ctx context.Context, id string, fetchTags bool) (*models.Place, error) {
-	place, err := s.placeRepo.GetByID(ctx, id)
-	if err != nil {
-		return nil, err
-	}
-	if place == nil {
-		return nil, nil
-	}
-
 	if fetchTags {
-		tags, err := s.placeRepo.GetTags(ctx, id)
-		if err != nil {
-			return nil, err
-		}
-		place.Tags = tags
-
-		features, err := s.placeRepo.GetFeatures(ctx, id)
-		if err != nil {
-			return nil, err
-		}
-		place.Features = features
+		return s.placeRepo.GetByIDWithDetails(ctx, id)
 	}
 
-	return place, nil
+	return s.placeRepo.GetByID(ctx, id)
 }
 
 func (s *PlaceService) List(ctx context.Context, filter models.PlaceFilter) (*models.PaginatedPlaces, error) {