docs(constitution): 新增数据库设计原则(v2.4.0)
在项目宪章中新增第九条原则"数据库设计原则",明确禁止使用数据库外键约束和ORM关联标签。 主要变更: - 新增原则IX:数据库设计原则(Database Design Principles) - 强制要求:数据库表不得使用外键约束 - 强制要求:GORM模型不得使用ORM关联标签(foreignKey、hasMany等) - 强制要求:表关系必须通过ID字段手动维护 - 强制要求:关联数据查询必须显式编写,避免ORM魔法 - 强制要求:时间字段由GORM处理,不使用数据库触发器 设计理念: - 提升业务逻辑灵活性(无数据库约束限制) - 优化高并发性能(无外键检查开销) - 增强代码可读性(显式查询,无隐式预加载) - 简化数据库架构和迁移流程 - 支持分布式和微服务场景 版本升级:2.3.0 → 2.4.0(MINOR)
This commit is contained in:
550
tests/unit/store_test.go
Normal file
550
tests/unit/store_test.go
Normal file
@@ -0,0 +1,550 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// setupTestStore 创建内存数据库用于单元测试
|
||||
func setupTestStore(t *testing.T) (*postgres.Store, func()) {
|
||||
// 使用 SQLite 内存数据库进行单元测试
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
require.NoError(t, err, "创建内存数据库失败")
|
||||
|
||||
// 自动迁移
|
||||
err = db.AutoMigrate(&model.User{}, &model.Order{})
|
||||
require.NoError(t, err, "数据库迁移失败")
|
||||
|
||||
// 创建测试 logger
|
||||
testLogger := zap.NewNop()
|
||||
store := postgres.NewStore(db, testLogger)
|
||||
|
||||
cleanup := func() {
|
||||
sqlDB, _ := db.DB()
|
||||
if sqlDB != nil {
|
||||
sqlDB.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return store, cleanup
|
||||
}
|
||||
|
||||
// TestUserStore 测试用户 Store 层
|
||||
func TestUserStore(t *testing.T) {
|
||||
store, cleanup := setupTestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("创建用户成功", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Username: "testuser",
|
||||
Email: "test@example.com",
|
||||
Password: "hashedpassword",
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
|
||||
err := store.User.Create(ctx, user)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, user.ID)
|
||||
assert.False(t, user.CreatedAt.IsZero())
|
||||
assert.False(t, user.UpdatedAt.IsZero())
|
||||
})
|
||||
|
||||
t.Run("创建重复用户名失败", func(t *testing.T) {
|
||||
user1 := &model.User{
|
||||
Username: "duplicate",
|
||||
Email: "user1@example.com",
|
||||
Password: "password1",
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
err := store.User.Create(ctx, user1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 尝试创建相同用户名
|
||||
user2 := &model.User{
|
||||
Username: "duplicate",
|
||||
Email: "user2@example.com",
|
||||
Password: "password2",
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
err = store.User.Create(ctx, user2)
|
||||
assert.Error(t, err, "应该返回唯一约束错误")
|
||||
})
|
||||
|
||||
t.Run("根据ID查询用户", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Username: "findbyid",
|
||||
Email: "findbyid@example.com",
|
||||
Password: "hashedpassword",
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
err := store.User.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := store.User.GetByID(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.Username, found.Username)
|
||||
assert.Equal(t, user.Email, found.Email)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的用户", func(t *testing.T) {
|
||||
_, err := store.User.GetByID(ctx, 99999)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, gorm.ErrRecordNotFound, err)
|
||||
})
|
||||
|
||||
t.Run("根据用户名查询用户", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Username: "findbyname",
|
||||
Email: "findbyname@example.com",
|
||||
Password: "hashedpassword",
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
err := store.User.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := store.User.GetByUsername(ctx, "findbyname")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.ID, found.ID)
|
||||
})
|
||||
|
||||
t.Run("更新用户", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Username: "updatetest",
|
||||
Email: "update@example.com",
|
||||
Password: "hashedpassword",
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
err := store.User.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 更新用户
|
||||
user.Email = "newemail@example.com"
|
||||
user.Status = constants.UserStatusInactive
|
||||
err = store.User.Update(ctx, user)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证更新
|
||||
found, err := store.User.GetByID(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "newemail@example.com", found.Email)
|
||||
assert.Equal(t, constants.UserStatusInactive, found.Status)
|
||||
})
|
||||
|
||||
t.Run("软删除用户", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Username: "deletetest",
|
||||
Email: "delete@example.com",
|
||||
Password: "hashedpassword",
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
err := store.User.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 软删除
|
||||
err = store.User.Delete(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证已删除
|
||||
_, err = store.User.GetByID(ctx, user.ID)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, gorm.ErrRecordNotFound, err)
|
||||
})
|
||||
|
||||
t.Run("分页列表查询", func(t *testing.T) {
|
||||
// 创建10个用户
|
||||
for i := 1; i <= 10; i++ {
|
||||
user := &model.User{
|
||||
Username: "listuser" + string(rune('0'+i)),
|
||||
Email: "list" + string(rune('0'+i)) + "@example.com",
|
||||
Password: "password",
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
err := store.User.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// 第一页
|
||||
users, total, err := store.User.List(ctx, 1, 5)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(users), 5)
|
||||
assert.GreaterOrEqual(t, total, int64(10))
|
||||
|
||||
// 第二页
|
||||
users2, total2, err := store.User.List(ctx, 2, 5)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(users2), 5)
|
||||
assert.Equal(t, total, total2)
|
||||
|
||||
// 验证不同页的数据不同
|
||||
if len(users) > 0 && len(users2) > 0 {
|
||||
assert.NotEqual(t, users[0].ID, users2[0].ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestOrderStore 测试订单 Store 层
|
||||
func TestOrderStore(t *testing.T) {
|
||||
store, cleanup := setupTestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试用户
|
||||
user := &model.User{
|
||||
Username: "orderuser",
|
||||
Email: "orderuser@example.com",
|
||||
Password: "hashedpassword",
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
err := store.User.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("创建订单成功", func(t *testing.T) {
|
||||
order := &model.Order{
|
||||
OrderID: "ORD-TEST-001",
|
||||
UserID: user.ID,
|
||||
Amount: 10000,
|
||||
Status: constants.OrderStatusPending,
|
||||
Remark: "测试订单",
|
||||
}
|
||||
|
||||
err := store.Order.Create(ctx, order)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, order.ID)
|
||||
assert.False(t, order.CreatedAt.IsZero())
|
||||
})
|
||||
|
||||
t.Run("创建重复订单号失败", func(t *testing.T) {
|
||||
order1 := &model.Order{
|
||||
OrderID: "ORD-DUP-001",
|
||||
UserID: user.ID,
|
||||
Amount: 10000,
|
||||
Status: constants.OrderStatusPending,
|
||||
}
|
||||
err := store.Order.Create(ctx, order1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 尝试创建相同订单号
|
||||
order2 := &model.Order{
|
||||
OrderID: "ORD-DUP-001",
|
||||
UserID: user.ID,
|
||||
Amount: 20000,
|
||||
Status: constants.OrderStatusPending,
|
||||
}
|
||||
err = store.Order.Create(ctx, order2)
|
||||
assert.Error(t, err, "应该返回唯一约束错误")
|
||||
})
|
||||
|
||||
t.Run("根据ID查询订单", func(t *testing.T) {
|
||||
order := &model.Order{
|
||||
OrderID: "ORD-FIND-001",
|
||||
UserID: user.ID,
|
||||
Amount: 20000,
|
||||
Status: constants.OrderStatusPending,
|
||||
}
|
||||
err := store.Order.Create(ctx, order)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := store.Order.GetByID(ctx, order.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, order.OrderID, found.OrderID)
|
||||
assert.Equal(t, order.Amount, found.Amount)
|
||||
})
|
||||
|
||||
t.Run("根据订单号查询", func(t *testing.T) {
|
||||
order := &model.Order{
|
||||
OrderID: "ORD-FIND-002",
|
||||
UserID: user.ID,
|
||||
Amount: 30000,
|
||||
Status: constants.OrderStatusPending,
|
||||
}
|
||||
err := store.Order.Create(ctx, order)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := store.Order.GetByOrderID(ctx, "ORD-FIND-002")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, order.ID, found.ID)
|
||||
})
|
||||
|
||||
t.Run("根据用户ID列表查询", func(t *testing.T) {
|
||||
// 创建多个订单
|
||||
for i := 1; i <= 5; i++ {
|
||||
order := &model.Order{
|
||||
OrderID: "ORD-LIST-" + string(rune('0'+i)),
|
||||
UserID: user.ID,
|
||||
Amount: int64(i * 10000),
|
||||
Status: constants.OrderStatusPending,
|
||||
}
|
||||
err := store.Order.Create(ctx, order)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
orders, total, err := store.Order.ListByUserID(ctx, user.ID, 1, 10)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(orders), 5)
|
||||
assert.GreaterOrEqual(t, total, int64(5))
|
||||
})
|
||||
|
||||
t.Run("更新订单状态", func(t *testing.T) {
|
||||
order := &model.Order{
|
||||
OrderID: "ORD-UPDATE-001",
|
||||
UserID: user.ID,
|
||||
Amount: 50000,
|
||||
Status: constants.OrderStatusPending,
|
||||
}
|
||||
err := store.Order.Create(ctx, order)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 更新状态
|
||||
now := time.Now()
|
||||
order.Status = constants.OrderStatusPaid
|
||||
order.PaidAt = &now
|
||||
err = store.Order.Update(ctx, order)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证更新
|
||||
found, err := store.Order.GetByID(ctx, order.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, constants.OrderStatusPaid, found.Status)
|
||||
assert.NotNil(t, found.PaidAt)
|
||||
})
|
||||
|
||||
t.Run("软删除订单", func(t *testing.T) {
|
||||
order := &model.Order{
|
||||
OrderID: "ORD-DELETE-001",
|
||||
UserID: user.ID,
|
||||
Amount: 60000,
|
||||
Status: constants.OrderStatusPending,
|
||||
}
|
||||
err := store.Order.Create(ctx, order)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 软删除
|
||||
err = store.Order.Delete(ctx, order.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证已删除
|
||||
_, err = store.Order.GetByID(ctx, order.ID)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, gorm.ErrRecordNotFound, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestStoreTransaction 测试事务功能
|
||||
func TestStoreTransaction(t *testing.T) {
|
||||
store, cleanup := setupTestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("事务提交成功", func(t *testing.T) {
|
||||
var userID uint
|
||||
var orderID uint
|
||||
|
||||
err := store.Transaction(ctx, func(tx *postgres.Store) error {
|
||||
// 创建用户
|
||||
user := &model.User{
|
||||
Username: "txuser1",
|
||||
Email: "txuser1@example.com",
|
||||
Password: "hashedpassword",
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
if err := tx.User.Create(ctx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
userID = user.ID
|
||||
|
||||
// 创建订单
|
||||
order := &model.Order{
|
||||
OrderID: "ORD-TX-001",
|
||||
UserID: user.ID,
|
||||
Amount: 10000,
|
||||
Status: constants.OrderStatusPending,
|
||||
}
|
||||
if err := tx.Order.Create(ctx, order); err != nil {
|
||||
return err
|
||||
}
|
||||
orderID = order.ID
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 验证用户和订单都已创建
|
||||
user, err := store.User.GetByID(ctx, userID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "txuser1", user.Username)
|
||||
|
||||
order, err := store.Order.GetByID(ctx, orderID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ORD-TX-001", order.OrderID)
|
||||
})
|
||||
|
||||
t.Run("事务回滚", func(t *testing.T) {
|
||||
var userID uint
|
||||
|
||||
err := store.Transaction(ctx, func(tx *postgres.Store) error {
|
||||
// 创建用户
|
||||
user := &model.User{
|
||||
Username: "rollbackuser",
|
||||
Email: "rollback@example.com",
|
||||
Password: "hashedpassword",
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
if err := tx.User.Create(ctx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
userID = user.ID
|
||||
|
||||
// 模拟错误,触发回滚
|
||||
return errors.New("模拟错误")
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "模拟错误", err.Error())
|
||||
|
||||
// 验证用户未创建(已回滚)
|
||||
_, err = store.User.GetByID(ctx, userID)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, gorm.ErrRecordNotFound, err)
|
||||
})
|
||||
|
||||
t.Run("嵌套事务回滚", func(t *testing.T) {
|
||||
var user1ID, user2ID uint
|
||||
|
||||
err := store.Transaction(ctx, func(tx1 *postgres.Store) error {
|
||||
// 外层事务:创建第一个用户
|
||||
user1 := &model.User{
|
||||
Username: "nested1",
|
||||
Email: "nested1@example.com",
|
||||
Password: "password",
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
if err := tx1.User.Create(ctx, user1); err != nil {
|
||||
return err
|
||||
}
|
||||
user1ID = user1.ID
|
||||
|
||||
// 内层事务:创建第二个用户并失败
|
||||
err := tx1.Transaction(ctx, func(tx2 *postgres.Store) error {
|
||||
user2 := &model.User{
|
||||
Username: "nested2",
|
||||
Email: "nested2@example.com",
|
||||
Password: "password",
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
if err := tx2.User.Create(ctx, user2); err != nil {
|
||||
return err
|
||||
}
|
||||
user2ID = user2.ID
|
||||
|
||||
// 内层事务失败
|
||||
return errors.New("内层事务失败")
|
||||
})
|
||||
|
||||
// 内层事务失败导致外层事务也失败
|
||||
return err
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
|
||||
// 验证两个用户都未创建
|
||||
_, err = store.User.GetByID(ctx, user1ID)
|
||||
assert.Error(t, err)
|
||||
|
||||
_, err = store.User.GetByID(ctx, user2ID)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcurrentAccess 测试并发访问
|
||||
func TestConcurrentAccess(t *testing.T) {
|
||||
store, cleanup := setupTestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("并发创建用户", func(t *testing.T) {
|
||||
concurrency := 20
|
||||
errChan := make(chan error, concurrency)
|
||||
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(index int) {
|
||||
user := &model.User{
|
||||
Username: "concurrent" + string(rune('A'+index)),
|
||||
Email: "concurrent" + string(rune('A'+index)) + "@example.com",
|
||||
Password: "password",
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
errChan <- store.User.Create(ctx, user)
|
||||
}(i)
|
||||
}
|
||||
|
||||
// 收集结果
|
||||
successCount := 0
|
||||
for i := 0; i < concurrency; i++ {
|
||||
err := <-errChan
|
||||
if err == nil {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, concurrency, successCount, "所有并发创建应该成功")
|
||||
})
|
||||
|
||||
t.Run("并发读写同一用户", func(t *testing.T) {
|
||||
// 创建测试用户
|
||||
user := &model.User{
|
||||
Username: "rwuser",
|
||||
Email: "rwuser@example.com",
|
||||
Password: "password",
|
||||
Status: constants.UserStatusActive,
|
||||
}
|
||||
err := store.User.Create(ctx, user)
|
||||
require.NoError(t, err)
|
||||
|
||||
concurrency := 10
|
||||
done := make(chan bool, concurrency*2)
|
||||
|
||||
// 并发读
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func() {
|
||||
_, err := store.User.GetByID(ctx, user.ID)
|
||||
assert.NoError(t, err)
|
||||
done <- true
|
||||
}()
|
||||
}
|
||||
|
||||
// 并发写
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(index int) {
|
||||
user.Status = constants.UserStatusActive
|
||||
err := store.User.Update(ctx, user)
|
||||
assert.NoError(t, err)
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
// 等待所有操作完成
|
||||
for i := 0; i < concurrency*2; i++ {
|
||||
<-done
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user