perf: reduce list query n+1
This commit is contained in:
@@ -38,10 +38,9 @@ func (s *order) ListUserOrders(ctx context.Context, userID int64, status string)
|
|||||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var data []user_dto.Order
|
data, err := s.composeOrderListDTO(ctx, list)
|
||||||
for _, v := range list {
|
if err != nil {
|
||||||
dto, _ := s.composeOrderDTO(ctx, v)
|
return nil, err
|
||||||
data = append(data, dto)
|
|
||||||
}
|
}
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
@@ -411,6 +410,122 @@ func (s *order) composeOrderDTO(ctx context.Context, o *models.Order) (user_dto.
|
|||||||
return dto, nil
|
return dto, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *order) composeOrderListDTO(ctx context.Context, orders []*models.Order) ([]user_dto.Order, error) {
|
||||||
|
if len(orders) == 0 {
|
||||||
|
return []user_dto.Order{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量收集订单、租户、内容 ID,避免逐条查询。
|
||||||
|
orderIDs := make([]int64, 0, len(orders))
|
||||||
|
tenantIDSet := make(map[int64]struct{}, len(orders))
|
||||||
|
for _, o := range orders {
|
||||||
|
orderIDs = append(orderIDs, o.ID)
|
||||||
|
if o.TenantID > 0 {
|
||||||
|
tenantIDSet[o.TenantID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantIDs := make([]int64, 0, len(tenantIDSet))
|
||||||
|
for id := range tenantIDSet {
|
||||||
|
tenantIDs = append(tenantIDs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantMap := make(map[int64]*models.Tenant, len(tenantIDs))
|
||||||
|
if len(tenantIDs) > 0 {
|
||||||
|
tbl, q := models.TenantQuery.QueryContext(ctx)
|
||||||
|
list, err := q.Where(tbl.ID.In(tenantIDs...)).Find()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
for _, t := range list {
|
||||||
|
tenantMap[t.ID] = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsByOrder := make(map[int64][]*models.OrderItem, len(orderIDs))
|
||||||
|
contentIDSet := make(map[int64]struct{}, len(orderIDs))
|
||||||
|
if len(orderIDs) > 0 {
|
||||||
|
tbl, q := models.OrderItemQuery.QueryContext(ctx)
|
||||||
|
items, err := q.Where(tbl.OrderID.In(orderIDs...)).Find()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
itemsByOrder[item.OrderID] = append(itemsByOrder[item.OrderID], item)
|
||||||
|
if item.ContentID > 0 {
|
||||||
|
contentIDSet[item.ContentID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentIDs := make([]int64, 0, len(contentIDSet))
|
||||||
|
for id := range contentIDSet {
|
||||||
|
contentIDs = append(contentIDs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentMap := make(map[int64]*models.Content, len(contentIDs))
|
||||||
|
if len(contentIDs) > 0 {
|
||||||
|
var contents []*models.Content
|
||||||
|
tbl, q := models.ContentQuery.QueryContext(ctx)
|
||||||
|
err := q.Where(tbl.ID.In(contentIDs...)).
|
||||||
|
UnderlyingDB().
|
||||||
|
Preload("ContentAssets").
|
||||||
|
Preload("ContentAssets.Asset").
|
||||||
|
Find(&contents).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
for _, c := range contents {
|
||||||
|
contentMap[c.ID] = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]user_dto.Order, 0, len(orders))
|
||||||
|
for _, o := range orders {
|
||||||
|
dto := user_dto.Order{
|
||||||
|
ID: o.ID,
|
||||||
|
Type: string(o.Type),
|
||||||
|
TypeDescription: o.Type.Description(),
|
||||||
|
Status: string(o.Status),
|
||||||
|
StatusDescription: o.Status.Description(),
|
||||||
|
Amount: float64(o.AmountPaid) / 100.0,
|
||||||
|
CreateTime: o.CreatedAt.Format(time.RFC3339),
|
||||||
|
TenantID: o.TenantID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if t, ok := tenantMap[o.TenantID]; ok {
|
||||||
|
dto.TenantName = t.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
items := itemsByOrder[o.ID]
|
||||||
|
dto.Quantity = len(items)
|
||||||
|
for _, item := range items {
|
||||||
|
c := contentMap[item.ContentID]
|
||||||
|
if c == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ci := transaction_dto.ContentItem{
|
||||||
|
ID: c.ID,
|
||||||
|
Title: c.Title,
|
||||||
|
Genre: c.Genre,
|
||||||
|
AuthorID: c.UserID,
|
||||||
|
Price: float64(item.AmountPaid) / 100.0,
|
||||||
|
}
|
||||||
|
for _, asset := range c.ContentAssets {
|
||||||
|
if asset.Role == consts.ContentAssetRoleCover && asset.Asset != nil {
|
||||||
|
ci.Cover = Common.GetAssetURL(asset.Asset.ObjectKey)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dto.Items = append(dto.Items, ci)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = append(data, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *order) toUserOrderDTO(o *models.Order) user_dto.Order {
|
func (s *order) toUserOrderDTO(o *models.Order) user_dto.Order {
|
||||||
return user_dto.Order{
|
return user_dto.Order{
|
||||||
ID: o.ID,
|
ID: o.ID,
|
||||||
|
|||||||
@@ -36,13 +36,23 @@ func (s *tenant) List(ctx context.Context, filter *dto.TenantListFilter) (*reque
|
|||||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tenantIDs := make([]int64, 0, len(list))
|
||||||
|
for _, t := range list {
|
||||||
|
tenantIDs = append(tenantIDs, t.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量统计关注数与内容数,避免逐条 Count。
|
||||||
|
followerMap, err := s.countFollowers(ctx, tenantIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
contentMap, err := s.countPublishedContents(ctx, tenantIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var data []dto.TenantProfile
|
var data []dto.TenantProfile
|
||||||
for _, t := range list {
|
for _, t := range list {
|
||||||
followers, _ := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(t.ID)).Count()
|
|
||||||
contents, _ := models.ContentQuery.WithContext(ctx).
|
|
||||||
Where(models.ContentQuery.TenantID.Eq(t.ID), models.ContentQuery.Status.Eq(consts.ContentStatusPublished)).
|
|
||||||
Count()
|
|
||||||
|
|
||||||
cfg := t.Config.Data()
|
cfg := t.Config.Data()
|
||||||
data = append(data, dto.TenantProfile{
|
data = append(data, dto.TenantProfile{
|
||||||
ID: t.ID,
|
ID: t.ID,
|
||||||
@@ -50,8 +60,8 @@ func (s *tenant) List(ctx context.Context, filter *dto.TenantListFilter) (*reque
|
|||||||
Avatar: cfg.Avatar,
|
Avatar: cfg.Avatar,
|
||||||
Bio: cfg.Bio,
|
Bio: cfg.Bio,
|
||||||
Stats: dto.Stats{
|
Stats: dto.Stats{
|
||||||
Followers: int(followers),
|
Followers: int(followerMap[t.ID]),
|
||||||
Contents: int(contents),
|
Contents: int(contentMap[t.ID]),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -159,29 +169,56 @@ func (s *tenant) ListFollowed(ctx context.Context, userID int64) ([]dto.TenantPr
|
|||||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(list) == 0 {
|
||||||
|
return []dto.TenantProfile{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantIDs := make([]int64, 0, len(list))
|
||||||
|
tenantIDSet := make(map[int64]struct{}, len(list))
|
||||||
|
for _, tu := range list {
|
||||||
|
if _, ok := tenantIDSet[tu.TenantID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tenantIDs = append(tenantIDs, tu.TenantID)
|
||||||
|
tenantIDSet[tu.TenantID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
tblTenant, qTenant := models.TenantQuery.QueryContext(ctx)
|
||||||
|
tenants, err := qTenant.Where(tblTenant.ID.In(tenantIDs...)).Find()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tenantMap := make(map[int64]*models.Tenant, len(tenants))
|
||||||
|
for _, t := range tenants {
|
||||||
|
tenantMap[t.ID] = t
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量统计关注数与内容数,避免逐条 Count。
|
||||||
|
followerMap, err := s.countFollowers(ctx, tenantIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
contentMap, err := s.countPublishedContents(ctx, tenantIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var data []dto.TenantProfile
|
var data []dto.TenantProfile
|
||||||
for _, tu := range list {
|
for _, tu := range list {
|
||||||
// Fetch Tenant
|
t, ok := tenantMap[tu.TenantID]
|
||||||
t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(tu.TenantID)).First()
|
if !ok {
|
||||||
if err != nil {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats
|
cfg := t.Config.Data()
|
||||||
followers, _ := models.TenantUserQuery.WithContext(ctx).
|
|
||||||
Where(models.TenantUserQuery.TenantID.Eq(tu.TenantID)).
|
|
||||||
Count()
|
|
||||||
contents, _ := models.ContentQuery.WithContext(ctx).
|
|
||||||
Where(models.ContentQuery.TenantID.Eq(tu.TenantID), models.ContentQuery.Status.Eq(consts.ContentStatusPublished)).
|
|
||||||
Count()
|
|
||||||
|
|
||||||
data = append(data, dto.TenantProfile{
|
data = append(data, dto.TenantProfile{
|
||||||
ID: t.ID,
|
ID: t.ID,
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
Avatar: "",
|
Avatar: cfg.Avatar,
|
||||||
Stats: dto.Stats{
|
Stats: dto.Stats{
|
||||||
Followers: int(followers),
|
Followers: int(followerMap[tu.TenantID]),
|
||||||
Contents: int(contents),
|
Contents: int(contentMap[tu.TenantID]),
|
||||||
},
|
},
|
||||||
IsFollowing: true,
|
IsFollowing: true,
|
||||||
})
|
})
|
||||||
@@ -202,3 +239,52 @@ func (s *tenant) GetModelByID(ctx context.Context, id int64) (*models.Tenant, er
|
|||||||
}
|
}
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type tenantStatRow struct {
|
||||||
|
TenantID int64 `gorm:"column:tenant_id"`
|
||||||
|
Total int64 `gorm:"column:total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *tenant) countFollowers(ctx context.Context, tenantIDs []int64) (map[int64]int64, error) {
|
||||||
|
if len(tenantIDs) == 0 {
|
||||||
|
return map[int64]int64{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tbl, q := models.TenantUserQuery.QueryContext(ctx)
|
||||||
|
var rows []tenantStatRow
|
||||||
|
err := q.Where(tbl.TenantID.In(tenantIDs...)).
|
||||||
|
Select(tbl.TenantID, tbl.ALL.Count().As("total")).
|
||||||
|
Group(tbl.TenantID).
|
||||||
|
Scan(&rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[int64]int64, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
result[row.TenantID] = row.Total
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *tenant) countPublishedContents(ctx context.Context, tenantIDs []int64) (map[int64]int64, error) {
|
||||||
|
if len(tenantIDs) == 0 {
|
||||||
|
return map[int64]int64{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tbl, q := models.ContentQuery.QueryContext(ctx)
|
||||||
|
var rows []tenantStatRow
|
||||||
|
err := q.Where(tbl.TenantID.In(tenantIDs...), tbl.Status.Eq(consts.ContentStatusPublished)).
|
||||||
|
Select(tbl.TenantID, tbl.ALL.Count().As("total")).
|
||||||
|
Group(tbl.TenantID).
|
||||||
|
Scan(&rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[int64]int64, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
result[row.TenantID] = row.Total
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user