Add option to detect unique constraint errors

This commit is contained in:
Lauris BH 2020-09-22 18:46:15 +03:00
parent 68920449f9
commit 17a4bf87d7
12 changed files with 167 additions and 8 deletions

View File

@ -92,12 +92,12 @@ func Create(config *Config) func(db *gorm.DB) {
} }
} }
} else { } else {
db.AddError(err) db.AddErrorFromDB(err)
} }
} }
} }
} else { } else {
db.AddError(err) db.AddErrorFromDB(err)
} }
} }
} }
@ -187,14 +187,14 @@ func CreateWithReturning(db *gorm.DB) {
} }
} }
} else { } else {
db.AddError(err) db.AddErrorFromDB(err)
} }
} }
} else if !db.DryRun && db.Error == nil { } else if !db.DryRun && db.Error == nil {
if result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...); err == nil { if result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...); err == nil {
db.RowsAffected, _ = result.RowsAffected() db.RowsAffected, _ = result.RowsAffected()
} else { } else {
db.AddError(err) db.AddErrorFromDB(err)
} }
} }
} }

View File

@ -8,7 +8,7 @@ func RawExec(db *gorm.DB) {
if db.Error == nil && !db.DryRun { if db.Error == nil && !db.DryRun {
result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...) result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...)
if err != nil { if err != nil {
db.AddError(err) db.AddErrorFromDB(err)
} else { } else {
db.RowsAffected, _ = result.RowsAffected() db.RowsAffected, _ = result.RowsAffected()
} }

View File

@ -80,7 +80,7 @@ func Update(db *gorm.DB) {
if err == nil { if err == nil {
db.RowsAffected, _ = result.RowsAffected() db.RowsAffected, _ = result.RowsAffected()
} else { } else {
db.AddError(err) db.AddErrorFromDB(err)
} }
} }
} }

View File

@ -2,6 +2,8 @@ package gorm
import ( import (
"errors" "errors"
"fmt"
"strings"
) )
var ( var (
@ -32,3 +34,21 @@ var (
// ErrDryRunModeUnsupported dry run mode unsupported // ErrDryRunModeUnsupported dry run mode unsupported
ErrDryRunModeUnsupported = errors.New("dry run mode unsupported") ErrDryRunModeUnsupported = errors.New("dry run mode unsupported")
) )
// ErrUniqueConstraint unique constraint error
type ErrUniqueConstraint struct {
ConstraintName string
Columns []string
}
func (e *ErrUniqueConstraint) Error() string {
if len(e.ConstraintName) > 0 {
return fmt.Sprintf("unique constraint '%s' error", e.ConstraintName)
}
if len(e.Columns) > 0 {
return fmt.Sprintf("unique constraint on columns '%s' error", strings.Join(e.Columns, ", "))
}
return "unique constraint error"
}

44
gorm.go
View File

@ -5,6 +5,7 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"strings"
"sync" "sync"
"time" "time"
@ -252,6 +253,49 @@ func (db *DB) AddError(err error) error {
return db.Error return db.Error
} }
// AddErrorFromDB add error from database
func (db *DB) AddErrorFromDB(err error) error {
msg := err.Error()
// === Unique constraint errors
if strings.HasPrefix(msg, "UNIQUE constraint failed:") {
// SQLite3
cols := strings.Split(msg[25:], ",")
for i := range cols {
cols[i] = strings.TrimSpace(cols[i])
}
err = &ErrUniqueConstraint{
Columns: cols,
}
} else if strings.HasPrefix(msg, "Error 1062: Duplicate entry") {
// MySQL
constr := strings.Trim(strings.TrimSpace(msg[strings.Index(msg, "for key")+7:]), "'")
p := strings.Split(constr, ".")
if len(p) == 1 {
constr = p[0]
} else {
constr = p[1]
}
err = &ErrUniqueConstraint{
ConstraintName: constr,
}
} else if strings.HasPrefix(msg, "ERROR: duplicate key value violates unique constraint") {
// PostgreSQL
constr := ""
i := strings.Index(msg, `"`)
j := strings.LastIndex(msg, `"`)
if i != -1 && j != i {
constr = msg[i+1 : j]
}
err = &ErrUniqueConstraint{
ConstraintName: constr,
}
}
return db.AddError(err)
}
// DB returns `*sql.DB` // DB returns `*sql.DB`
func (db *DB) DB() (*sql.DB, error) { func (db *DB) DB() (*sql.DB, error) {
connPool := db.ConnPool connPool := db.ConnPool

View File

@ -23,6 +23,7 @@ type User struct {
Team []*User `gorm:"foreignkey:ManagerID"` Team []*User `gorm:"foreignkey:ManagerID"`
Languages []*tests.Language `gorm:"many2many:UserSpeak"` Languages []*tests.Language `gorm:"many2many:UserSpeak"`
Friends []*User `gorm:"many2many:user_friends"` Friends []*User `gorm:"many2many:user_friends"`
Contacts []*tests.Contact
Active *bool Active *bool
} }

View File

@ -108,6 +108,10 @@ func checkUserSchema(t *testing.T, user *schema.Schema) {
}}, }},
References: []Reference{{"ID", "User", "UserID", "user_friends", "", true}, {"ID", "User", "FriendID", "user_friends", "", false}}, References: []Reference{{"ID", "User", "UserID", "user_friends", "", true}, {"ID", "User", "FriendID", "user_friends", "", false}},
}, },
{
Name: "Contacts", Type: schema.HasMany, Schema: "User", FieldSchema: "Contact",
References: []Reference{{"ID", "User", "UserID", "Contact", "", true}},
},
} }
for _, relation := range relations { for _, relation := range relations {

View File

@ -0,0 +1,43 @@
package tests_test
import (
"testing"
"gorm.io/gorm"
. "gorm.io/gorm/utils/tests"
)
func TestCreateUniqueConstraint(t *testing.T) {
user1 := GetUser("create-unique-constraint", Config{})
if err := DB.Create(user1).Error; err != nil {
t.Fatalf("errors happened when create: %v", err)
}
user1Contact := &Contact{UserID: &user1.ID, Email: "create-unique-constraint@email"}
if err := DB.Create(user1Contact).Error; err != nil {
t.Fatalf("errors happened when create cotract: %v", err)
}
user2 := GetUser("create-unique-constraint2", Config{})
if err := DB.Create(user2).Error; err != nil {
t.Fatalf("errors happened when create: %v", err)
}
user2Contact := &Contact{UserID: &user2.ID, Email: "create-unique-constraint@email"}
err := DB.Create(user2Contact).Error
if err == nil {
t.Fatal("should return unique constraint error")
}
e, ok := err.(*gorm.ErrUniqueConstraint)
if !ok {
t.Fatalf("should return unique constraint error, got err %v", err)
}
if len(e.ConstraintName) > 0 {
AssertEqual(t, e.ConstraintName, "idx_email")
}
if len(e.Columns) > 0 {
AssertEqual(t, e.Columns[0], "contacts.email")
}
}

View File

@ -19,6 +19,7 @@ type Config struct {
Team int Team int
Languages int Languages int
Friends int Friends int
Contacts int
} }
func GetUser(name string, config Config) *User { func GetUser(name string, config Config) *User {
@ -65,6 +66,10 @@ func GetUser(name string, config Config) *User {
user.Friends = append(user.Friends, GetUser(name+"_friend_"+strconv.Itoa(i+1), Config{})) user.Friends = append(user.Friends, GetUser(name+"_friend_"+strconv.Itoa(i+1), Config{}))
} }
for i := 0; i < config.Contacts; i++ {
user.Contacts = append(user.Contacts, &Contact{Email: name + "_" + strconv.Itoa(i+1) + "@email"})
}
return &user return &user
} }
@ -87,6 +92,19 @@ func CheckPet(t *testing.T, pet Pet, expect Pet) {
} }
} }
func CheckContact(t *testing.T, contact Contact, expect Contact) {
if contact.ID != 0 {
var newContact Contact
if err := DB.Where("id = ?", contact.ID).First(&newContact).Error; err != nil {
t.Fatalf("errors happened when query: %v", err)
} else {
AssertObjEqual(t, newContact, contact, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "UserID", "Email")
}
}
AssertObjEqual(t, contact, expect, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "UserID", "Email")
}
func CheckUser(t *testing.T, user User, expect User) { func CheckUser(t *testing.T, user User, expect User) {
if user.ID != 0 { if user.ID != 0 {
var newUser User var newUser User
@ -227,4 +245,26 @@ func CheckUser(t *testing.T, user User, expect User) {
AssertObjEqual(t, friend, expect.Friends[idx], "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age", "Birthday", "CompanyID", "ManagerID", "Active") AssertObjEqual(t, friend, expect.Friends[idx], "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age", "Birthday", "CompanyID", "ManagerID", "Active")
} }
}) })
t.Run("Contacts", func(t *testing.T) {
if len(user.Contacts) != len(expect.Contacts) {
t.Fatalf("contacts should equal, expect: %v, got %v", len(expect.Contacts), len(user.Contacts))
}
sort.Slice(user.Contacts, func(i, j int) bool {
return user.Contacts[i].ID > user.Contacts[j].ID
})
sort.Slice(expect.Contacts, func(i, j int) bool {
return expect.Contacts[i].ID > expect.Contacts[j].ID
})
for idx, contact := range user.Contacts {
if contact == nil || expect.Contacts[idx] == nil {
t.Errorf("contact#%v should equal, expect: %v, got %v", idx, expect.Contacts[idx], contact)
} else {
CheckContact(t, *contact, *expect.Contacts[idx])
}
}
})
} }

View File

@ -11,7 +11,7 @@ import (
) )
func TestMigrate(t *testing.T) { func TestMigrate(t *testing.T) {
allModels := []interface{}{&User{}, &Account{}, &Pet{}, &Company{}, &Toy{}, &Language{}} allModels := []interface{}{&User{}, &Account{}, &Pet{}, &Company{}, &Toy{}, &Language{}, &Contact{}}
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
rand.Shuffle(len(allModels), func(i, j int) { allModels[i], allModels[j] = allModels[j], allModels[i] }) rand.Shuffle(len(allModels), func(i, j int) { allModels[i], allModels[j] = allModels[j], allModels[i] })

View File

@ -87,7 +87,7 @@ func OpenTestConnection() (db *gorm.DB, err error) {
func RunMigrations() { func RunMigrations() {
var err error var err error
allModels := []interface{}{&User{}, &Account{}, &Pet{}, &Company{}, &Toy{}, &Language{}} allModels := []interface{}{&User{}, &Account{}, &Pet{}, &Company{}, &Toy{}, &Language{}, &Contact{}}
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
rand.Shuffle(len(allModels), func(i, j int) { allModels[i], allModels[j] = allModels[j], allModels[i] }) rand.Shuffle(len(allModels), func(i, j int) { allModels[i], allModels[j] = allModels[j], allModels[i] })

View File

@ -26,6 +26,7 @@ type User struct {
Team []User `gorm:"foreignkey:ManagerID"` Team []User `gorm:"foreignkey:ManagerID"`
Languages []Language `gorm:"many2many:UserSpeak;"` Languages []Language `gorm:"many2many:UserSpeak;"`
Friends []*User `gorm:"many2many:user_friends;"` Friends []*User `gorm:"many2many:user_friends;"`
Contacts []*Contact
Active bool Active bool
} }
@ -58,3 +59,9 @@ type Language struct {
Code string `gorm:"primarykey"` Code string `gorm:"primarykey"`
Name string Name string
} }
type Contact struct {
gorm.Model
UserID *uint
Email string `gorm:"index:idx_email,unique"`
}