// repo_bench_test.go — repository 层性能基准测试 // 覆盖:批量写入、并发只读查询、分页列表、更新状态、软删除 package repository import ( "context" "fmt" "sync" "sync/atomic" "testing" _ "modernc.org/sqlite" gormsqlite "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/user-management-system/internal/domain" ) var repoBenchCounter int64 // openBenchDB 为 Benchmark 打开独立内存 DB(不依赖 *testing.T) func openBenchDB(b *testing.B) *gorm.DB { b.Helper() id := atomic.AddInt64(&repoBenchCounter, 1) dsn := fmt.Sprintf("file:repobenchdb%d?mode=memory&cache=private", id) db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{ DriverName: "sqlite", DSN: dsn, }), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { b.Fatalf("openBenchDB: %v", err) } if err := db.AutoMigrate( &domain.User{}, &domain.Role{}, &domain.Permission{}, &domain.UserRole{}, &domain.RolePermission{}, ); err != nil { b.Fatalf("AutoMigrate: %v", err) } return db } // seedUsers 往 DB 插入 n 条用户 func seedUsers(b *testing.B, repo *UserRepository, n int) { b.Helper() ctx := context.Background() for i := 0; i < n; i++ { if err := repo.Create(ctx, &domain.User{ Username: fmt.Sprintf("benchuser%06d", i), Email: domain.StrPtr(fmt.Sprintf("bench%06d@example.com", i)), Phone: domain.StrPtr(fmt.Sprintf("1380000%04d", i%10000)), Password: "hashed_placeholder", Status: domain.UserStatusActive, }); err != nil { b.Fatalf("seedUsers i=%d: %v", i, err) } } } // ---------- BenchmarkRepo_Create — 单条写入吞吐 ---------- func BenchmarkRepo_Create(b *testing.B) { db := openBenchDB(b) repo := NewUserRepository(db) ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { repo.Create(ctx, &domain.User{ //nolint:errcheck Username: fmt.Sprintf("cr_%d_%d", b.N, i), Email: domain.StrPtr(fmt.Sprintf("cr_%d_%d@bench.com", b.N, i)), Password: "hash", Status: domain.UserStatusActive, }) } } // ---------- BenchmarkRepo_BulkCreate — 批量写入(串行) ---------- func BenchmarkRepo_BulkCreate(b *testing.B) { sizes := []int{10, 100, 500} for _, size := range sizes { size := size b.Run(fmt.Sprintf("batch=%d", size), func(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() db := openBenchDB(b) repo := NewUserRepository(db) ctx := context.Background() users := make([]*domain.User, size) for j := 0; j < size; j++ { users[j] = &domain.User{ Username: fmt.Sprintf("bulk_%d_%d_%d", i, j, size), Password: "hash", Status: domain.UserStatusActive, } } b.StartTimer() for _, u := range users { repo.Create(ctx, u) //nolint:errcheck } } }) } } // ---------- BenchmarkRepo_GetByID — 主键查询 ---------- func BenchmarkRepo_GetByID(b *testing.B) { db := openBenchDB(b) repo := NewUserRepository(db) seedUsers(b, repo, 1000) ctx := context.Background() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { id := int64(1) for pb.Next() { repo.GetByID(ctx, id) //nolint:errcheck id++ if id > 1000 { id = 1 } } }) } // ---------- BenchmarkRepo_GetByUsername — 索引查询 ---------- func BenchmarkRepo_GetByUsername(b *testing.B) { db := openBenchDB(b) repo := NewUserRepository(db) seedUsers(b, repo, 500) ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { repo.GetByUsername(ctx, fmt.Sprintf("benchuser%06d", i%500)) //nolint:errcheck } } // ---------- BenchmarkRepo_GetByEmail — 索引查询 ---------- func BenchmarkRepo_GetByEmail(b *testing.B) { db := openBenchDB(b) repo := NewUserRepository(db) seedUsers(b, repo, 500) ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { repo.GetByEmail(ctx, fmt.Sprintf("bench%06d@example.com", i%500)) //nolint:errcheck } } // ---------- BenchmarkRepo_List — 分页列表 ---------- func BenchmarkRepo_List(b *testing.B) { pageSizes := []int{10, 50, 200} for _, ps := range pageSizes { ps := ps b.Run(fmt.Sprintf("pageSize=%d", ps), func(b *testing.B) { db := openBenchDB(b) repo := NewUserRepository(db) seedUsers(b, repo, 1000) ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { repo.List(ctx, 0, ps) //nolint:errcheck } }) } } // ---------- BenchmarkRepo_ListByStatus ---------- func BenchmarkRepo_ListByStatus(b *testing.B) { db := openBenchDB(b) repo := NewUserRepository(db) seedUsers(b, repo, 1000) ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { repo.ListByStatus(ctx, domain.UserStatusActive, 0, 20) //nolint:errcheck } } // ---------- BenchmarkRepo_UpdateStatus ---------- func BenchmarkRepo_UpdateStatus(b *testing.B) { db := openBenchDB(b) repo := NewUserRepository(db) seedUsers(b, repo, 200) ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { id := int64(i%200) + 1 repo.UpdateStatus(ctx, id, domain.UserStatusActive) //nolint:errcheck } } // ---------- BenchmarkRepo_Update — 全字段更新 ---------- func BenchmarkRepo_Update(b *testing.B) { db := openBenchDB(b) repo := NewUserRepository(db) seedUsers(b, repo, 100) ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { id := int64(i%100) + 1 u, err := repo.GetByID(ctx, id) if err != nil { continue } u.Nickname = fmt.Sprintf("nick%d", i) repo.Update(ctx, u) //nolint:errcheck } } // ---------- BenchmarkRepo_Delete — 软删除 ---------- func BenchmarkRepo_Delete(b *testing.B) { ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { b.StopTimer() db := openBenchDB(b) repo := NewUserRepository(db) repo.Create(ctx, &domain.User{Username: "victim", Password: "hash", Status: domain.UserStatusActive}) //nolint:errcheck b.StartTimer() repo.Delete(ctx, 1) //nolint:errcheck } } // ---------- BenchmarkRepo_ExistsByUsername ---------- func BenchmarkRepo_ExistsByUsername(b *testing.B) { db := openBenchDB(b) repo := NewUserRepository(db) seedUsers(b, repo, 500) ctx := context.Background() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { i := 0 for pb.Next() { repo.ExistsByUsername(ctx, fmt.Sprintf("benchuser%06d", i%500)) //nolint:errcheck i++ } }) } // ---------- BenchmarkRepo_ConcurrentReadWrite — 高并发读写混合 ---------- func BenchmarkRepo_ConcurrentReadWrite(b *testing.B) { db := openBenchDB(b) repo := NewUserRepository(db) seedUsers(b, repo, 200) ctx := context.Background() var mu sync.Mutex // SQLite 不支持多写并发,需要序列化写入 b.ResetTimer() b.RunParallel(func(pb *testing.PB) { i := int64(1) for pb.Next() { if i%10 == 0 { // 10% 写操作 mu.Lock() repo.UpdateLastLogin(ctx, i%200+1, "10.0.0.1") //nolint:errcheck mu.Unlock() } else { // 90% 读操作 repo.GetByID(ctx, i%200+1) //nolint:errcheck } i++ } }) } // ---------- BenchmarkRepo_Search — 模糊搜索 ---------- func BenchmarkRepo_Search(b *testing.B) { db := openBenchDB(b) repo := NewUserRepository(db) seedUsers(b, repo, 2000) ctx := context.Background() b.ResetTimer() keywords := []string{"benchuser000", "bench0001", "benchuser05"} for i := 0; i < b.N; i++ { repo.Search(ctx, keywords[i%len(keywords)], 0, 20) //nolint:errcheck } }