数据库约束 和 Struct Tag 标签到底是干什么的?
🥰个人主页:会编程的土豆(欢迎来访)
💎作者简介:后端学习者
❄️个人专栏:数据结构与算法,数据库,leetcode
✨那些你一个人走过的夜路,终将化作照亮未来的光
适合对象:刚接触 MySQL + GORM 的新手
结合场景:影院票务(users / seats / orders 等表)
读完你能分清:SQL 里的约束管什么,Go 结构体上的 tag 又管什么
一、先建立整体印象
做 Web 后端时,数据会经过两层“说明书”:
| 层级 | 写在哪 | 作用 |
|---|---|---|
| 数据库约束 | CREATE TABLE里 | 数据库强制执行的规则,违规会直接报错 |
| Struct Tag | Go 的struct字段后面`...` | 给框架看的标注:GORM 怎么映射表、JSON 怎么输出 |
可以记一句:
- 约束:数据库的“铁规矩”
- Tag:Go 代码和框架之间的“说明书便签”
两者经常对应(比如表里username唯一,模型上也写uniqueIndex),但职责不同:
真正拦非法数据的,主要是数据库约束;Tag 更多是让 GORM/JSON 知道怎么读写。
二、数据库约束是什么?
建表时,除了写“有哪些列”,还会写“这些列必须遵守什么规则”。这些规则就叫约束(Constraint)。
2.1 为什么需要约束?
假如没有约束:
- 可以插入两个相同的用户名 → 登录时到底算谁?
- 订单可以写一个不存在的
user_id→ 脏数据 - 座位状态乱填 999 → 业务判断全乱
约束就是在数据库层把这些问题挡掉。
2.2 票务项目里常见约束(对照理解)
1)主键 PRIMARY KEY
id BIGINT PRIMARY KEY AUTO_INCREMENT- 每一行的唯一身份证
AUTO_INCREMENT:插入时 id 自动增长- 一张表通常一个主键
2)非空 NOT NULL
username VARCHAR(50) NOT NULL这一列不允许是NULL(“没填”)。
注意:空字符串''和NULL不是一回事。
3)唯一 UNIQUE
username VARCHAR(50) NOT NULL UNIQUE同一张表里,这个值不能重复。
用户名、订单号常用。
4)默认值 DEFAULT
role TINYINT NOT NULL DEFAULT 0插入时如果不写role,就默认是0(顾客)。
5)外键 FOREIGN KEY
CONSTRAINT fk_sch_movie FOREIGN KEY (movie_id) REFERENCES movies(id)含义:schedules.movie_id必须能在movies.id里找到。
作用:防止场次挂到一部不存在的电影上。
常见连带策略(了解即可):
ON DELETE CASCADE:主表删了,子表相关行也删(如删场次时座位跟着删)ON DELETE SET NULL:主表删了,子表外键置空
6)索引 INDEX / UNIQUE KEY
INDEX idx_order_user (user_id) UNIQUE KEY uk_seat (schedule_id, row_no, col_no)- 索引:让按某列查询更快(如查某用户订单)
- 唯一索引:既加速,又保证组合不重复
例如同一场次不能有两个“3排5座”
7)引擎 ENGINE=InnoDB
购票要用事务和行锁(FOR UPDATE),一般用 InnoDB。
这不是列约束,但是建表时很重要的选择。
三、结合座位和订单理解约束
座位和订单的关系可以记成:
一笔订单对应多个座位;座位通过
order_id挂到订单上。
相关约束帮助保证:
orders.user_id必须是真实用户(外键)orders.schedule_id必须是真实场次(外键)- 同一场次的
(schedule_id, row_no, col_no)唯一(唯一索引) - 订单状态、座位状态用整数表示业务含义(配合代码常量,不一定全靠数据库枚举)
没有这些约束,超卖、脏订单会更容易出现(当然超卖还要靠事务和行锁,约束是基础防线)。
四、Go 里的 Tag 标签是什么?
看模型代码时,常看到:
type User struct { ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` Username string `gorm:"size:50;uniqueIndex;not null" json:"username"` Password string `gorm:"size:32;not null" json:"-"` }字段后面的反引号字符串,就是struct tag(标签)
特点:
- 不影响 Go 语法本身能不能编译通过(写错 tag 内容往往仍能编译)
- 主要给反射 + 框架读取,例如 GORM、encoding/json
一个字段上可以写多个 tag,用空格分隔:
`gorm:"..." json:"..."`五、json tag:控制接口返回长什么样
| 写法 | 含义 |
|---|---|
json:"username" | JSON 里字段名用username |
json:"-" | 不要序列化出去(密码、盐常用) |
json:"nickname,omitempty" | 空值时可以省略该字段 |
示例:
Password string `json:"-"` Salt string `json:"-"`登录成功返回用户信息时,就不会把密码哈希泄露给前端。这是非常基础的安全习惯。
六、gorm tag:控制怎么和数据库打交道
GORM 通过 tag 知道:
- 主键是谁
- 列名是什么
- 是否唯一
- 字段长度
- 是否只读(联表查出来的字段)等
6.1 常用 gorm tag(入门够用)
| tag | 含义 |
|---|---|
primaryKey | 主键 |
autoIncrement | 自增 |
not null | 非空(模型层提示/迁移时有用) |
size:50 | 字符串长度 |
uniqueIndex | 唯一索引 |
index | 普通索引 |
column:rows_num | 数据库列名和 Go 字段名不一致时指定列名 |
type:decimal(10,2) | 指定数据库类型 |
default:0 | 默认值 |
-> | 只读:查可以填充,一般不用于写入该表 |
- | 忽略该字段(完全不映射) |
6.2 TableName:指定表名
func (User) TableName() string { return "users" }告诉 GORM:User对应表名users。
否则 GORM 可能按默认规则猜表名(有时是users,有时你想精确控制)。
6.3 为什么有的字段是指针?
GroupID *int64 LockUserID *int64 PaidAt *time.Time因为数据库里这些列可能是NULL:
- 用指针:可以区分“没值(nil)”和“有值 0”
- 用普通
int64:很难表达 SQL 的NULL
七、约束 vs Tag:别混在一起
| 对比 | 数据库约束 | Struct Tag |
|---|---|---|
| 写在哪 | SQL / 数据库 | Go 结构体 |
| 谁强制执行 | MySQL | 框架约定(GORM/JSON) |
| 作用 | 保证数据合法性 | 指导映射与序列化 |
| 失败时 | 插入/更新直接报错 | 可能映射错、字段丢失、或迁移不一致 |
注意:
- 只在 Go 的 gorm tag 写了
uniqueIndex,但数据库里没建唯一约束,不一定能防重复(尤其你是用schema.sql建表、而不是全靠 AutoMigrate 时)。 - 本票务项目是SQL 建表为主,models 上的 gorm tag 更多是让 GORM 读写时行为正确、字段对应正确。
- 真正的底线仍是数据库约束 + 业务代码(事务/行锁)
八、一张对照表(users)
| SQL | Go |
|---|---|
id BIGINT PRIMARY KEY AUTO_INCREMENT | ID int64 \gorm:“primaryKey;autoIncrement” json:“id”`` |
username VARCHAR(50) NOT NULL UNIQUE | Username string \gorm:“size:50;uniqueIndex;not null” json:“username”`` |
password CHAR(32) NOT NULL | Password string \gorm:“size:32;not null” json:“-”`` |
role TINYINT DEFAULT 0 | Role int \gorm:“not null;default:0” json:“role”`` |
看到没有:左边是库的规矩,右边是 Go 的说明书。两边对齐,项目才稳
