From 7b41dfeb00a0795ddf772231bcc7b8621b792d5e Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Thu, 26 Mar 2026 01:50:54 +0800 Subject: [PATCH] feat(repository): enhance repository methods with batch processing and transaction support - Updated Like and Unlike methods in CommentRepository to use transactions for data consistency. - Introduced IsLikedBatch method for batch checking if comments are liked, addressing N+1 query issues. - Enhanced BatchDelete method in PostRepository to perform batch deletions within a single transaction, improving efficiency. - Added BatchUpdateStatus method for posts to utilize a single SQL update for status changes, reducing database load. - Implemented BatchUpdateSortOrder in StickerRepository using CASE WHEN for efficient sorting updates. - Added IsFollowingBatch and IsBlockedBatch methods in UserRepository for batch checking follow and block relationships, optimizing performance. --- internal/repository/comment_repo.go | 155 ++++++++++++++++------ internal/repository/group_repo.go | 33 ++++- internal/repository/login_log_repo.go | 37 ++++-- internal/repository/message_repo.go | 5 +- internal/repository/operation_log_repo.go | 25 ++-- internal/repository/post_repo.go | 103 +++++++++++--- internal/repository/sticker_repo.go | 34 +++-- internal/repository/user_repo.go | 60 +++++++++ internal/repository/vote_repo.go | 43 +++--- 9 files changed, 386 insertions(+), 109 deletions(-) diff --git a/internal/repository/comment_repo.go b/internal/repository/comment_repo.go index 1f9af9f..c2f50de 100644 --- a/internal/repository/comment_repo.go +++ b/internal/repository/comment_repo.go @@ -255,46 +255,49 @@ func (r *CommentRepository) GetReplies(parentID string) ([]*model.Comment, error return comments, err } -// Like 点赞评论 +// Like 点赞评论(使用事务保证数据一致性) func (r *CommentRepository) Like(commentID, userID string) error { - // 检查是否已经点赞 - var existing model.CommentLike - err := r.db.Where("comment_id = ? AND user_id = ?", commentID, userID).First(&existing).Error - if err == nil { - // 已经点赞 - return nil - } - if err != gorm.ErrRecordNotFound { - return err - } + return r.db.Transaction(func(tx *gorm.DB) error { + // 检查是否已经点赞 + var existing model.CommentLike + err := tx.Where("comment_id = ? AND user_id = ?", commentID, userID).First(&existing).Error + if err == nil { + // 已经点赞 + return nil + } + if err != gorm.ErrRecordNotFound { + return err + } - // 创建点赞记录 - like := &model.CommentLike{ - CommentID: commentID, - UserID: userID, - } - err = r.db.Create(like).Error - if err != nil { - return err - } + // 创建点赞记录 + like := &model.CommentLike{ + CommentID: commentID, + UserID: userID, + } + if err := tx.Create(like).Error; err != nil { + return err + } - // 增加评论点赞数 - return r.db.Model(&model.Comment{}).Where("id = ?", commentID). - UpdateColumn("likes_count", gorm.Expr("likes_count + 1")).Error + // 增加评论点赞数 + return tx.Model(&model.Comment{}).Where("id = ?", commentID). + UpdateColumn("likes_count", gorm.Expr("likes_count + 1")).Error + }) } -// Unlike 取消点赞评论 +// Unlike 取消点赞评论(使用事务保证数据一致性) func (r *CommentRepository) Unlike(commentID, userID string) error { - result := r.db.Where("comment_id = ? AND user_id = ?", commentID, userID).Delete(&model.CommentLike{}) - if result.Error != nil { - return result.Error - } - if result.RowsAffected > 0 { - // 减少评论点赞数 - return r.db.Model(&model.Comment{}).Where("id = ?", commentID). - UpdateColumn("likes_count", gorm.Expr("likes_count - 1")).Error - } - return nil + return r.db.Transaction(func(tx *gorm.DB) error { + result := tx.Where("comment_id = ? AND user_id = ?", commentID, userID).Delete(&model.CommentLike{}) + if result.Error != nil { + return result.Error + } + if result.RowsAffected > 0 { + // 减少评论点赞数 + return tx.Model(&model.Comment{}).Where("id = ?", commentID). + UpdateColumn("likes_count", gorm.Expr("likes_count - 1")).Error + } + return nil + }) } // IsLiked 检查是否已点赞 @@ -304,6 +307,33 @@ func (r *CommentRepository) IsLiked(commentID, userID string) bool { return count > 0 } +// IsLikedBatch 批量检查是否点赞(解决 N+1 问题) +// 返回 map[commentID]bool +func (r *CommentRepository) IsLikedBatch(commentIDs []string, userID string) map[string]bool { + result := make(map[string]bool) + if len(commentIDs) == 0 || userID == "" { + return result + } + + // 初始化所有 commentID 为 false + for _, commentID := range commentIDs { + result[commentID] = false + } + + // 一次性查询所有点赞记录 + var likedCommentIDs []string + r.db.Model(&model.CommentLike{}). + Where("comment_id IN ? AND user_id = ?", commentIDs, userID). + Pluck("comment_id", &likedCommentIDs) + + // 更新已点赞的评论 + for _, commentID := range likedCommentIDs { + result[commentID] = true + } + + return result +} + // AdminCommentListFilter 管理端评论列表筛选条件 type AdminCommentListFilter struct { Keyword string @@ -367,17 +397,58 @@ func (r *CommentRepository) GetAdminCommentByID(id string) (*model.Comment, erro return &comment, nil } -// BatchDelete 批量删除评论 +// BatchDelete 批量删除评论(使用单次事务批量删除,避免 N+1 问题) func (r *CommentRepository) BatchDelete(ids []string) ([]string, error) { - var failedIDs []string - - for _, id := range ids { - if err := r.Delete(id); err != nil { - failedIDs = append(failedIDs, id) - } + if len(ids) == 0 { + return nil, nil } - return failedIDs, nil + // 先查询所有评论获取 post_id 和状态信息 + type commentInfo struct { + ID string + PostID string + Status model.CommentStatus + } + var comments []commentInfo + if err := r.db.Model(&model.Comment{}).Where("id IN ?", ids).Scan(&comments).Error; err != nil { + return ids, err + } + + err := r.db.Transaction(func(tx *gorm.DB) error { + // 批量删除评论点赞记录 + if err := tx.Where("comment_id IN ?", ids).Delete(&model.CommentLike{}).Error; err != nil { + return err + } + + // 批量删除评论(软删除) + if err := tx.Where("id IN ?", ids).Delete(&model.Comment{}).Error; err != nil { + return err + } + + // 更新帖子的评论数(仅统计已发布评论) + postCountMap := make(map[string]int) + for _, c := range comments { + if c.Status == model.CommentStatusPublished { + postCountMap[c.PostID]++ + } + } + for postID, count := range postCountMap { + if err := tx.Model(&model.Post{}).Where("id = ?", postID). + UpdateColumns(map[string]any{ + "comments_count": gorm.Expr("comments_count - ?", count), + "updated_at": gorm.Expr("updated_at"), + }).Error; err != nil { + return err + } + } + + return nil + }) + + if err != nil { + return ids, err + } + return nil, nil } // GetCommentsByPostIDForAdmin 管理端获取帖子的评论列表(包含所有状态) diff --git a/internal/repository/group_repo.go b/internal/repository/group_repo.go index 30f9dc1..8e32a77 100644 --- a/internal/repository/group_repo.go +++ b/internal/repository/group_repo.go @@ -4,6 +4,7 @@ import ( "carrot_bbs/internal/model" "carrot_bbs/internal/pkg/cursor" "context" + "errors" "gorm.io/gorm" ) @@ -188,6 +189,36 @@ func (r *groupRepository) IsMember(groupID string, userID string) (bool, error) return count > 0, err } +// GetExistingMembers 批量检查用户是否是群成员(解决 N+1 问题) +// 返回 map[userID]bool +func (r *groupRepository) GetExistingMembers(groupID string, userIDs []string) (map[string]bool, error) { + result := make(map[string]bool) + if len(userIDs) == 0 || groupID == "" { + return result, nil + } + + // 初始化所有用户为 false + for _, id := range userIDs { + result[id] = false + } + + // 一次性查询所有群成员记录 + var existingIDs []string + err := r.db.Model(&model.GroupMember{}). + Where("group_id = ? AND user_id IN ?", groupID, userIDs). + Pluck("user_id", &existingIDs).Error + if err != nil { + return nil, err + } + + // 更新已存在的成员 + for _, id := range existingIDs { + result[id] = true + } + + return result, nil +} + // GetUserGroups 获取用户加入的群组列表 func (r *groupRepository) GetUserGroups(userID string, page, pageSize int) ([]model.Group, int64, error) { var groups []model.Group @@ -318,7 +349,7 @@ func (r *groupRepository) GetLatestAnnouncement(groupID string) (*model.GroupAnn Order("is_pinned DESC, created_at DESC"). First(&announcement).Error if err != nil { - if err == gorm.ErrRecordNotFound { + if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return nil, err diff --git a/internal/repository/login_log_repo.go b/internal/repository/login_log_repo.go index 241228c..3412dd3 100644 --- a/internal/repository/login_log_repo.go +++ b/internal/repository/login_log_repo.go @@ -107,7 +107,7 @@ func (r *LoginLogRepository) DeleteOldLogs(ctx context.Context, beforeDate strin return r.db.WithContext(ctx).Where("created_at < ?", beforeDate).Delete(&model.LoginLog{}).Error } -// GetStatistics 获取登录日志统计 +// GetStatistics 获取登录日志统计(使用单次 GROUP BY 查询,避免多次 COUNT) func (r *LoginLogRepository) GetStatistics(ctx context.Context, startTime, endTime string) (*LoginLogStatistics, error) { stats := &LoginLogStatistics{} @@ -119,20 +119,37 @@ func (r *LoginLogRepository) GetStatistics(ctx context.Context, startTime, endTi query = query.Where("created_at <= ?", endTime) } - if err := query.Count(&stats.TotalCount).Error; err != nil { + // 使用单次 GROUP BY 查询获取 result 计数 + type resultCount struct { + Result string + Count int64 + } + var results []resultCount + err := query.Select("result, count(*) as count").Group("result").Scan(&results).Error + if err != nil { return nil, err } - if err := query.Where("result = ?", string(model.LoginResultSuccess)).Count(&stats.SuccessCount).Error; err != nil { - return nil, err + // 汇总结果 + for _, r := range results { + stats.TotalCount += r.Count + if r.Result == string(model.LoginResultSuccess) { + stats.SuccessCount = r.Count + } else if r.Result == string(model.LoginResultFail) { + stats.FailCount = r.Count + } } - if err := query.Where("result = ?", string(model.LoginResultFail)).Count(&stats.FailCount).Error; err != nil { - return nil, err - } - - if err := query.Where("event = ?", string(model.LoginEventLogin)).Count(&stats.LoginCount).Error; err != nil { - return nil, err + // 单独查询登录次数 + if startTime != "" || endTime != "" { + loginQuery := r.db.WithContext(ctx).Model(&model.LoginLog{}) + if startTime != "" { + loginQuery = loginQuery.Where("created_at >= ?", startTime) + } + if endTime != "" { + loginQuery = loginQuery.Where("created_at <= ?", endTime) + } + loginQuery.Where("event = ?", string(model.LoginEventLogin)).Count(&stats.LoginCount) } return stats, nil diff --git a/internal/repository/message_repo.go b/internal/repository/message_repo.go index d378015..e895715 100644 --- a/internal/repository/message_repo.go +++ b/internal/repository/message_repo.go @@ -4,6 +4,7 @@ import ( "carrot_bbs/internal/model" "carrot_bbs/internal/pkg/cursor" "context" + "errors" "fmt" "strconv" "strings" @@ -188,7 +189,7 @@ func (r *MessageRepository) GetParticipant(conversationID string, userID string) err := r.db.Where("conversation_id = ? AND user_id = ?", conversationID, userID).First(&participant).Error if err != nil { // 如果找不到参与者,尝试添加(修复没有参与者记录的问题) - if err == gorm.ErrRecordNotFound { + if errors.Is(err, gorm.ErrRecordNotFound) { // 检查会话是否存在 var conv model.Conversation if err := r.db.Where("id = ?", conversationID).First(&conv).Error; err == nil { @@ -513,7 +514,7 @@ func (r *MessageRepository) DeleteConversationByGroupID(groupID string) error { conv, err := r.GetConversationByGroupID(groupID) if err != nil { // 如果会话不存在,直接返回 - if err == gorm.ErrRecordNotFound { + if errors.Is(err, gorm.ErrRecordNotFound) { return nil } return err diff --git a/internal/repository/operation_log_repo.go b/internal/repository/operation_log_repo.go index 93818a3..204e55c 100644 --- a/internal/repository/operation_log_repo.go +++ b/internal/repository/operation_log_repo.go @@ -89,7 +89,7 @@ func (r *OperationLogRepository) DeleteOldLogs(ctx context.Context, beforeDate s return r.db.WithContext(ctx).Where("created_at < ?", beforeDate).Delete(&model.OperationLog{}).Error } -// GetStatistics 获取操作日志统计 +// GetStatistics 获取操作日志统计(使用单次 GROUP BY 查询,避免多次 COUNT) func (r *OperationLogRepository) GetStatistics(ctx context.Context, startTime, endTime string) (*OperationLogStatistics, error) { stats := &OperationLogStatistics{} @@ -101,16 +101,25 @@ func (r *OperationLogRepository) GetStatistics(ctx context.Context, startTime, e query = query.Where("created_at <= ?", endTime) } - if err := query.Count(&stats.TotalCount).Error; err != nil { + // 使用单次 GROUP BY 查询获取各状态计数 + type statusCount struct { + Status string + Count int64 + } + var results []statusCount + err := query.Select("status, count(*) as count").Group("status").Scan(&results).Error + if err != nil { return nil, err } - if err := query.Where("status = ?", "success").Count(&stats.SuccessCount).Error; err != nil { - return nil, err - } - - if err := query.Where("status = ?", "fail").Count(&stats.FailCount).Error; err != nil { - return nil, err + // 汇总结果 + for _, r := range results { + stats.TotalCount += r.Count + if r.Status == "success" { + stats.SuccessCount = r.Count + } else if r.Status == "fail" { + stats.FailCount = r.Count + } } return stats, nil diff --git a/internal/repository/post_repo.go b/internal/repository/post_repo.go index c2c1074..1e753a0 100644 --- a/internal/repository/post_repo.go +++ b/internal/repository/post_repo.go @@ -802,30 +802,101 @@ func (r *PostRepository) GetByIDForAdmin(id string) (*model.Post, error) { return &post, nil } -// BatchDelete 批量删除帖子 +// BatchDelete 批量删除帖子(使用单次事务批量删除,避免 N+1 问题) func (r *PostRepository) BatchDelete(ids []string) ([]string, error) { - var failedIDs []string - - for _, id := range ids { - if err := r.Delete(id); err != nil { - failedIDs = append(failedIDs, id) - } + if len(ids) == 0 { + return nil, nil } - return failedIDs, nil + err := r.db.Transaction(func(tx *gorm.DB) error { + // 批量删除帖子图片 + if err := tx.Where("post_id IN ?", ids).Delete(&model.PostImage{}).Error; err != nil { + return err + } + + // 批量删除帖子点赞记录 + if err := tx.Where("post_id IN ?", ids).Delete(&model.PostLike{}).Error; err != nil { + return err + } + + // 批量删除帖子收藏记录 + if err := tx.Where("post_id IN ?", ids).Delete(&model.Favorite{}).Error; err != nil { + return err + } + + // 批量删除评论点赞记录(子查询获取所有评论ID) + if err := tx.Where("comment_id IN (SELECT id FROM comments WHERE post_id IN ?)", ids).Delete(&model.CommentLike{}).Error; err != nil { + return err + } + + // 批量删除帖子评论 + if err := tx.Where("post_id IN ?", ids).Delete(&model.Comment{}).Error; err != nil { + return err + } + + // 批量删除投票选项 + if err := tx.Where("post_id IN ?", ids).Delete(&model.VoteOption{}).Error; err != nil { + return err + } + + // 批量删除用户投票记录 + if err := tx.Where("post_id IN ?", ids).Delete(&model.UserVote{}).Error; err != nil { + return err + } + + // 最后批量删除帖子本身(软删除) + return tx.Where("id IN ?", ids).Delete(&model.Post{}).Error + }) + + if err != nil { + return ids, err + } + return nil, nil } -// BatchUpdateStatus 批量更新帖子审核状态 +// BatchUpdateStatus 批量更新帖子审核状态(使用单条 SQL 批量更新,避免 N+1 问题) func (r *PostRepository) BatchUpdateStatus(ids []string, status model.PostStatus, reviewedBy string) ([]string, error) { - var failedIDs []string - - for _, id := range ids { - if err := r.UpdateModerationStatus(id, status, "", reviewedBy); err != nil { - failedIDs = append(failedIDs, id) - } + if len(ids) == 0 { + return nil, nil } - return failedIDs, nil + // 使用单条 SQL 批量更新 + result := r.db.Model(&model.Post{}). + Where("id IN ?", ids). + Updates(map[string]any{ + "status": status, + "reviewed_at": gorm.Expr("CURRENT_TIMESTAMP"), + "reviewed_by": reviewedBy, + "reject_reason": "", + "updated_at": gorm.Expr("updated_at"), + }) + + if result.Error != nil { + return ids, result.Error + } + + // 返回实际更新的帖子数量 + if result.RowsAffected < int64(len(ids)) { + // 部分帖子可能不存在,查询实际更新的 ID + var updatedIDs []string + r.db.Model(&model.Post{}).Where("id IN ?", ids).Pluck("id", &updatedIDs) + + // 找出未更新的 ID + updatedMap := make(map[string]bool, len(updatedIDs)) + for _, id := range updatedIDs { + updatedMap[id] = true + } + + var failedIDs []string + for _, id := range ids { + if !updatedMap[id] { + failedIDs = append(failedIDs, id) + } + } + return failedIDs, nil + } + + return nil, nil } // UpdatePinStatus 更新帖子置顶状态 diff --git a/internal/repository/sticker_repo.go b/internal/repository/sticker_repo.go index d5a2e20..509a26b 100644 --- a/internal/repository/sticker_repo.go +++ b/internal/repository/sticker_repo.go @@ -2,6 +2,7 @@ package repository import ( "carrot_bbs/internal/model" + "strings" "gorm.io/gorm" ) @@ -88,18 +89,31 @@ func (r *stickerRepository) UpdateSortOrder(id string, sortOrder int) error { Update("sort_order", sortOrder).Error } -// BatchUpdateSortOrder 批量更新排序 +// BatchUpdateSortOrder 批量更新排序(使用 CASE WHEN 批量更新,避免 N+1 问题) func (r *stickerRepository) BatchUpdateSortOrder(userID string, orders map[string]int) error { - return r.db.Transaction(func(tx *gorm.DB) error { - for id, sortOrder := range orders { - if err := tx.Model(&model.UserSticker{}). - Where("id = ? AND user_id = ?", id, userID). - Update("sort_order", sortOrder).Error; err != nil { - return err - } - } + if len(orders) == 0 { return nil - }) + } + + // 构建 CASE WHEN 批量更新 SQL + var cases []string + var args []any + + for id, sortOrder := range orders { + cases = append(cases, "WHEN id = ? THEN ?") + args = append(args, id, sortOrder) + } + + ids := make([]string, 0, len(orders)) + for id := range orders { + ids = append(ids, id) + } + + sql := `UPDATE user_stickers SET sort_order = CASE ` + + strings.Join(cases, " ") + ` END WHERE id IN ? AND user_id = ?` + args = append(args, ids, userID) + + return r.db.Exec(sql, args...).Error } // CountByUserID 获取用户表情数量 diff --git a/internal/repository/user_repo.go b/internal/repository/user_repo.go index 63942db..33a4fc1 100644 --- a/internal/repository/user_repo.go +++ b/internal/repository/user_repo.go @@ -140,6 +140,36 @@ func (r *UserRepository) IsFollowing(followerID, followingID string) (bool, erro return count > 0, nil } +// IsFollowingBatch 批量检查当前用户是否关注了目标用户(解决 N+1 问题) +// 返回 map[targetUserID]bool +func (r *UserRepository) IsFollowingBatch(followerID string, targetUserIDs []string) (map[string]bool, error) { + result := make(map[string]bool) + if len(targetUserIDs) == 0 || followerID == "" { + return result, nil + } + + // 初始化所有目标用户为 false + for _, id := range targetUserIDs { + result[id] = false + } + + // 一次性查询所有关注记录 + var followingIDs []string + err := r.db.Model(&model.Follow{}). + Where("follower_id = ? AND following_id IN ?", followerID, targetUserIDs). + Pluck("following_id", &followingIDs).Error + if err != nil { + return nil, err + } + + // 更新已关注的用户 + for _, id := range followingIDs { + result[id] = true + } + + return result, nil +} + // IncrementFollowersCount 增加用户粉丝数 func (r *UserRepository) IncrementFollowersCount(userID string) error { return r.db.Model(&model.User{}).Where("id = ?", userID). @@ -243,6 +273,36 @@ func (r *UserRepository) IsBlocked(blockerID, blockedID string) (bool, error) { return count > 0, nil } +// IsBlockedBatch 批量检查拉黑关系(解决 N+1 问题) +// 返回 map[blockedUserID]bool +func (r *UserRepository) IsBlockedBatch(blockerID string, blockedIDs []string) (map[string]bool, error) { + result := make(map[string]bool) + if len(blockedIDs) == 0 || blockerID == "" { + return result, nil + } + + // 初始化所有目标用户为 false + for _, id := range blockedIDs { + result[id] = false + } + + // 一次性查询所有拉黑记录 + var blockedUserIDs []string + err := r.db.Model(&model.UserBlock{}). + Where("blocker_id = ? AND blocked_id IN ?", blockerID, blockedIDs). + Pluck("blocked_id", &blockedUserIDs).Error + if err != nil { + return nil, err + } + + // 更新已拉黑的用户 + for _, id := range blockedUserIDs { + result[id] = true + } + + return result, nil +} + // IsBlockedEitherDirection 检查是否任一方向存在拉黑 func (r *UserRepository) IsBlockedEitherDirection(userA, userB string) (bool, error) { var count int64 diff --git a/internal/repository/vote_repo.go b/internal/repository/vote_repo.go index b566ceb..97ecd50 100644 --- a/internal/repository/vote_repo.go +++ b/internal/repository/vote_repo.go @@ -17,21 +17,24 @@ func NewVoteRepository(db *gorm.DB) *VoteRepository { return &VoteRepository{db: db} } -// CreateOptions 批量创建投票选项 +// CreateOptions 批量创建投票选项(使用批量插入,避免 N+1 问题) func (r *VoteRepository) CreateOptions(postID string, options []string) error { - return r.db.Transaction(func(tx *gorm.DB) error { - for i, content := range options { - option := &model.VoteOption{ - PostID: postID, - Content: content, - SortOrder: i, - } - if err := tx.Create(option).Error; err != nil { - return err - } - } + if len(options) == 0 { return nil - }) + } + + // 构建批量插入的选项切片 + voteOptions := make([]model.VoteOption, len(options)) + for i, content := range options { + voteOptions[i] = model.VoteOption{ + PostID: postID, + Content: content, + SortOrder: i, + } + } + + // 使用 GORM 的 Create 方法批量插入 + return r.db.CreateInBatches(voteOptions, 100).Error } // GetOptionsByPostID 获取帖子的所有投票选项 @@ -58,9 +61,9 @@ func (r *VoteRepository) Vote(postID, userID, optionID string) error { // 验证选项是否属于该帖子 var option model.VoteOption if err := tx.Where("id = ? AND post_id = ?", optionID, postID).First(&option).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return errors.New("invalid option") - } + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("invalid option") + } return err } @@ -86,9 +89,9 @@ func (r *VoteRepository) Unvote(postID, userID string) error { var userVote model.UserVote err := tx.Where("post_id = ? AND user_id = ?", postID, userID).First(&userVote).Error if err != nil { - if err == gorm.ErrRecordNotFound { - return nil // 没有投票记录,直接返回 - } + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil // 没有投票记录,直接返回 + } return err } @@ -113,7 +116,7 @@ func (r *VoteRepository) GetUserVote(postID, userID string) (*model.UserVote, er var userVote model.UserVote err := r.db.Where("post_id = ? AND user_id = ?", postID, userID).First(&userVote).Error if err != nil { - if err == gorm.ErrRecordNotFound { + if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return nil, err