golang Gorm与数据库完整性约束详解
数据库约束要点:
主键约束(非空且唯一)外键约束子表外键字段的值必须在主表被参照字段值得范围内,或者为NULL;外键参照的必须是主表的主键或唯一键;主表主键/唯一键被子表参照时,主表相应记录不允许被删除
在golang中,采用orm对数据库进行建模是比较方便的。grom是其中一个比较流行的orm工具。
本篇基于golang、grom1.91、和PostgreSQL来进行说明。
注:本文的例子是极端情况,一般情况只是单字段主键。
1、实体完整性:
每个关系(表)有且仅有一个主键,每一个主键值必须唯一,而且不允许为“空”(NULL)或重复。
typeProductstruct{ Codestring`gorm:"primary_key"` Priceuint UserIDuint//`sql:"type:bigintREFERENCESusers(id)onupdatenoactionondeletecascade"` UserCodestring//`sql:"type:bigintREFERENCESusers(code)onupdatenoactionondeletecascade"` UserUser//`gorm:"foreignkey:UserID;association_foreignkey:ID"` gorm.Model }
在利用gorm的db对象创建表,其使用的SQL如下:
CREATETABLEpublic.products ( codetextCOLLATEpg_catalog."default"NOTNULL, priceinteger, user_idinteger, user_codetextCOLLATEpg_catalog."default", idintegerNOTNULLDEFAULTnextval('products_id_seq'::regclass), created_attimestampwithtimezone, updated_attimestampwithtimezone, deleted_attimestampwithtimezone, CONSTRAINTproducts_pkeyPRIMARYKEY(code,id) ) WITH( OIDS=FALSE ) TABLESPACEpg_default; ALTERTABLEpublic.products OWNERtopostgres; --Index:idx_products_deleted_at --DROPINDEXpublic.idx_products_deleted_at; CREATEINDEXidx_products_deleted_at ONpublic.productsUSINGbtree (deleted_at) TABLESPACEpg_default;
说明:
1.1、grom.Model是gorm预定义的结构,用于实现软删除,定义如下:
typeModelstruct{ IDuint`gorm:"primary_key"` CreatedAttime.Time UpdatedAttime.Time DeletedAt*time.Time`sql:"index"` }
它里面已经定义了主键,在本例中,我们还定义了一个主键:
Codestring`gorm:"primary_key"`
从SQL输出我们看到:
CONSTRAINTproducts_pkeyPRIMARYKEY(code,id)
因此,Gorm实现了完全的实体完整性支持,即可以支持字段主键,也可以支持联合主键。
1.2、对比结构体和sql语句可以看出
1.2.1表名=结构体名小写的复数例子:Product变为products
1.2.2字段名=结构体成员名大写分隔的子串小写形式用下划线连接例子:ID变为idCreatedAt变为created_at
1.3、前述1.1和1.2构成了Gorm的convention,它的文档里有,默认情况下,就是这么处理,但是用户可以不用gorm.Model,自定义表名、字段名,都可以支持。
2、域完整性:
是指数据库表中的列必须满足某种特定的数据类型或约束,又叫用户定义完整性。包括:字段类型、值域、小数位数、CHECK、FOREIGNKEY约束和DEFAULT、NOTNULL。它们有的定义在字段上,有的定义在表上。例如:FOREIGNKEY约束在PostgresSQL中,就是在表级别定义的;而,字段类型、长度、小数位数就是在字段上定义的。
2.1通过结构体tag
`gorm:"xxx"`,在字段上可以使用:type、size、precision、notnull、default,Gorm就可以完成这些域完整性的定义
2.2FOREIGNKEY约束
2.2.1单字段外键约束
typeProductstruct{ Codestring//`gorm:"primary_key"` Priceuint UserIDuint//`sql:"type:bigintREFERENCESusers(id)onupdatenoactionondeletecascade"` //UserCodestring//`sql:"type:bigintREFERENCESusers(code)onupdatenoactionondeletecascade"` UserUser//`gorm:"foreignkey:UserID;association_foreignkey:ID"` gorm.Model } typeUserstruct{ //Codestring`gorm:"primary_key"` Namestring gorm.Model //ProductProduct//`gorm:"EMBEDDED"` }
上面的代码按照gorm文档,创建了一个productsbelongstouser关系。执行的sql是:
CREATETABLE"users"("code"text,"name"text,"id"serial,"created_at"timestampwithtimezone,"updated_at"timestampwithtimezone,"deleted_at"timestampwithtimezone,PRIMARYKEY("code","id")) CREATETABLE"users"("code"text,"name"text,"id"serial,"created_at"timestampwithtimezone,"updated_at"timestampwithtimezone,"deleted_at"timestampwithtimezone,PRIMARYKEY("code","id"))
我们看到,gorm没有添加任何约束。按照Gorm文档,这就是belongsto标准定义。
它不添加外键约束。
那么,改为显式标准的形式,采用foreignkeytag呢?
typeProductstruct{ Codestring//`gorm:"primary_key"` Priceuint UserIDuint//`sql:"type:integerREFERENCESusers(id)`//onupdatenoactionondeletecascade"` //UserCodestring//`sql:"type:bigintREFERENCESusers(code)onupdatenoactionondeletecascade"` //UserIDuint UserUser`gorm:"foreignkey:UserID;association_foreignkey:ID"`//`gorm:"foreignkey:UserID;association_foreignkey:ID"` gorm.Model } typeUserstruct{ //Codestring`gorm:"primary_key"` Namestring gorm.Model //ProductProduct//`gorm:"EMBEDDED"` }
执行的sql是:
CREATETABLE"users"("name"text,"id"serial,"created_at"timestampwithtimezone,"updated_at"timestampwithtimezone,"deleted_at"timestampwithtimezone,PRIMARYKEY("id")) CREATETABLE"products"("code"text,"price"integer,"user_id"integer,"id"serial,"created_at"timestampwithtimezone,"updated_at"timestampwithtimezone,"deleted_at"timestampwithtimezone,PRIMARYKEY("id"))
也没有添加任何外键约束。
因此,gormtag的foreignkey和association_foreignkey并不会添加外键约束。
但是,我们可以用sqltag来添加外键约束!!!如下:
typeProductstruct{ Codestring//`gorm:"primary_key"` Priceuint UserIDuint`sql:"type:integerREFERENCESusers(id)onupdatenoactionondeletenoaction"` //UserCodestring//`sql:"type:bigintREFERENCESusers(code)onupdatenoactionondeletecascade"` //UserIDuint UserUser`gorm:"foreignkey:UserID;association_foreignkey:ID"`//`gorm:"foreignkey:UserID;association_foreignkey:ID"` gorm.Model } typeUserstruct{ //Codestring`gorm:"primary_key"` Namestring gorm.Model //ProductProduct//`gorm:"EMBEDDED"` }
创建products表的语句:
CREATETABLE"products"("code"text,"price"integer,"user_id"integerREFERENCESusers(id)onupdatenoactionondeletenoaction,"id"serial,"created_at"timestampwithtimezone,"updated_at"timestampwithtimezone,"deleted_at"timestampwithtimezone,PRIMARYKEY("id"))
注意,当使用sqltag时,不像gormtag,它要你用数据库表名和字段名,而gorm就只需要你使用结构体和其成员名即可。
外键被定义了,此时,可以满足外键约束,如前述,具体是:
子表外键字段的值必须在主表被参照字段值得范围内,或者为NULL;外键参照的必须是主表的主键或唯一键;主表主键/唯一键被子表参照时,主表相应记录不允许被删除
此时外键约束的名字是数据库自己取的,可能长了,你可以自定义:
UserIDuint`sql:"type:integerconstraintrefREFERENCESusers(id)onupdatenoactionondeletenoaction"`
加上constraintxxx,就可以为约束取名为xx了。
上述外键约束是在定义结构体时,在结构体成员上定义的,因此翻译为sql语句就变成了对字段的外键约束,那如果要定义参照联合主键之类的外键呢?就不能在结构体中定义,而要使用gorm的api了。
2.2.2多字段外键约束
typeProductstruct{ Codestring//`gorm:"primary_key"` Priceuint //UserIDuint`sql:"type:integerREFERENCESusers(id)onupdatenoactionondeletenoaction"` //UserCodestring//`sql:"type:bigintREFERENCESusers(code)onupdatenoactionondeletecascade"` UserCodestring UserIDuint UserUser//`gorm:"foreignkey:UserID;association_foreignkey:ID"`//`gorm:"foreignkey:UserID;association_foreignkey:ID"` gorm.Model } typeUserstruct{ Codestring`gorm:"primary_key"` Namestring gorm.Model //ProductProduct//`gorm:"EMBEDDED"` }
在程序中使用:
postgres.Model(&Product{}).AddForeignKey("user_id,user_code","users(id,code)","noaction","noaction")
这样,products表就有约束:
CONSTRAINTproducts_user_id_user_code_users_id_code_foreignFOREIGNKEY(user_code,user_id) REFERENCESpublic.users(code,id)MATCHSIMPLE ONUPDATENOACTION ONDELETENOACTION
如此就OK了。这里约束的名字就很长了,api没有给你自己取名字的机会。
2.3check约束
typeProductstruct{ Codestring//`gorm:"primary_key"` Priceuint UserIDuint`sql:"type:integercheck(code!='')"` UserCodestring//`sql:"type:bigintconstraintrefREFERENCESusers(code)onupdatenoactionondeletecascade"` //UserCodestring //UserIDuint UserUser//`gorm:"foreignkey:UserID;association_foreignkey:ID"`//`gorm:"foreignkey:UserID;association_foreignkey:ID"` gorm.Model }
这样就行。看起来这个check和userID没有什么关系,是的,check会被定义到表上:
ALTERTABLEpublic.products
ADDCONSTRAINTproductsCHECK(code<>''::text);
因此,Check也完美了,找个结构体的字段,然后加上check就行了。
3、参照完整性:
对于永久关系的相关表,在更新、插入或删除记录时,如果只改其一,就会影响数据的完整性。对于更新、插入或删除表间数据的完整性,统称为参照完整性。
对于外键约束,插入参照完整性被满足。因此,如前述:
UserIDuint`sql:"type:integerREFERENCESusers(id)onupdatenoactionondeletenoaction"`
定义好onupdate和ondelete的参数,就可以满足参照完整性。
具体改为:
UserIDuint`sql:"type:integerREFERENCESusers(id)onupdatecascadeondeletecascade"`
即可,而且数据库还允许有别的选择,这里是级联更新和级联删除,主表已删除,子表就跟着删,这是数据库参照完整性的原初定义。
ps.gorm不默认实施参照完整性,不加约束的原因查了其gitissue,主要是因为postgresql要求被关联的表要先存在。而这会导致创建表和自动升级表migration的顺序依赖,所以用户要sqltag或者调用api手动实施。
4、*1对多和多对多关系
这不属于完整性范畴。
4.11对多
1对多不需要实施完整性约束,因为用户可以对应0到多个产品。因此,表结构里无需添加额外的约束。
typeProductstruct{ Codestring//`gorm:"primary_key"` Priceuint //UserIDuint`sql:"type:integerconstraintrefREFERENCESusers(id)onupdatenoactionondeletenoactioncheck(code!='')"` //UserCodestring//`sql:"type:bigintconstraintrefREFERENCESusers(code)onupdatenoactionondeletecascade"` //UserCodestring //UserIDuint //UserUser//`gorm:"foreignkey:UserID;association_foreignkey:ID"`//`gorm:"foreignkey:UserID;association_foreignkey:ID"` gorm.Model UserIDuint } typeUserstruct{ Codestring//`gorm:"primary_key"` Namestring gorm.Model Products[]Product //ProductProduct//`gorm:"EMBEDDED"` }
上面是gorm一对多的典型定义,users表不会多任何字段,product表会多user_id字段。这里UserID是外键。也可以显式定义,foreignkey和AssociationForeignKey上例相当于:
typeProductstruct{ Codestring//`gorm:"primary_key"` Priceuint //UserIDuint`sql:"type:integerconstraintrefREFERENCESusers(id)onupdatenoactionondeletenoactioncheck(code!='')"` //UserCodestring//`sql:"type:bigintconstraintrefREFERENCESusers(code)onupdatenoactionondeletecascade"` //UserCodestring //UserIDuint //UserUser//`gorm:"foreignkey:UserID;association_foreignkey:ID"`//`gorm:"foreignkey:UserID;association_foreignkey:ID"` gorm.Model UserIDuint } typeUserstruct{ Codestring//`gorm:"primary_key"` Namestring gorm.Model Products[]Product`gorm:"foreignkey:UserID"` //ProductProduct//`gorm:"EMBEDDED"` }
4.2多对多
在关系型数据库中,多对多关系需要多一张表,总共3张表,完整性grom是如何保证的?
typeProductstruct{ Codestring//`gorm:"primary_key"` Priceuint //UserIDuint`sql:"type:integerconstraintrefREFERENCESusers(id)onupdatenoactionondeletenoactioncheck(code!='')"` //UserCodestring//`sql:"type:bigintconstraintrefREFERENCESusers(code)onupdatenoactionondeletecascade"` //UserCodestring //UserIDuint //UserUser//`gorm:"foreignkey:UserID;association_foreignkey:ID"`//`gorm:"foreignkey:UserID;association_foreignkey:ID"` gorm.Model //UserIDuint } typeUserstruct{ Codestring//`gorm:"primary_key"` Namestring gorm.Model Products[]Product`gorm:"many2many:user_language"` //ProductProduct//`gorm:"EMBEDDED"` }
此时,会多一个表(jointtable连接表):
CREATETABLE"user_language"("user_id"integer,"product_id"integer,PRIMARYKEY("user_id","product_id"))
products和users表的主键,被联合作为新表的主键。在新表中,user_id和product_id也是外键,在Gorm中,是可以在many2many关系中自定义外键、关联外键的。当然,外键约束就不要想了。
那么,在上例中,按照grom的语法,对于Products成员,外键和关联外键分别是什么呢?简言之,在gorm所有情况下,将嵌入结构体和其父结构体关联起来的那个字段,就是外键;关联外键是写入外键的值的来源对应的键,通常就是父结构体的主键。在多对多情况下,如上例,连接表的user_id是外键,而写入时,并没有将user_id写入Products[]Product,写入的是product_id代表的数据,因此product_id是associate_foreignkey,这是gorm的约定,很费解,解释也牵强。
下面是多对多自引用:
typeUserstruct{ gorm.Model Friends[]*User`gorm:"many2many:friendships;association_jointable_foreignkey:friend_id"` }
用association_jointable_foreignkey在连接表里创建了一个字段。也比较费解。
综上:
1、字段的基本约束,通过gormtag基本都可以设置。
2、gorm支持实体完整性约束。
3、域完整性约束中,外键约束需要通过sqltag或调用api实现,check约束可以直接在字段上定义。
4、参照完整性gorm不能默认实现,必须通过sqltag或者调用api实现。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持毛票票。如有错误或未考虑完全的地方,望不吝赐教。