Repository: go-gorm/gorm (go)

Issue:
Allowing Customization of Type and ID Column Names for Polymorphic associations
<!-- DON'T CHANGE THE TEMPLATE -->

## Describe the feature

Allow users to customize the naming conventions for type and id columns when working with polymorphic associations in GORM, specifically for types specified using the `gorm:"polymorphic"` tag.

<!-- Describe the requested Feature -->

## Motivation

In scenarios where GORM is integrated with a database migrated by another language or framework, the default naming conventions for type and id columns may not align with the existing database schema. The need for customization becomes apparent, especially for types specified using the `gorm:"polymorphic"` tag.

The current GORM implementation assumes standard names (`<type>_id` and `<type>_type`), making it challenging to seamlessly integrate with databases where these conventions differ. To address this, there is a proposal to enhance GORM, allowing individual users to specify custom type and id column names for any type marked with the `gorm:"polymorphic"` tag. This enhancement ensures that GORM can adapt to diverse database schemas without risking compatibility issues.

https://github.com/go-gorm/gorm/pull/6716




Pull request description:
feature: bring custom type and id column name to polymorphism
<!--
Make sure these boxes checked before submitting your pull request.

For significant changes, please open an issue to make an agreement on an implementation design/plan first before starting it.
-->

- [x] Do only one thing
- [x] Non breaking API changes
- [x] Tested

### What did this pull request do?

This pull request enables users to customize the type and id column names for polymorphic associations in GORM.

### User Case Description

#### Before this Pull Request

Previously, polymorphic associations were limited to default naming conventions (`<type>_Type` and `<type>_ID`).

#### After this Pull Request

Users can now specify custom type and id column names for polymorphic associations, offering greater adaptability to diverse database schemas.

<!-- Add any other relevant details -->

close #6717


Patch:
diff --git a/clause/locking.go b/clause/locking.go
index 290aac9..2bc48ce 100644
--- a/clause/locking.go
+++ b/clause/locking.go
@@ -1,5 +1,12 @@
 package clause
 
+const (
+	LockingStrengthUpdate    = "UPDATE"
+	LockingStrengthShare     = "SHARE"
+	LockingOptionsSkipLocked = "SKIP LOCKED"
+	LockingOptionsNoWait     = "NOWAIT"
+)
+
 type Locking struct {
 	Strength string
 	Table    Table
diff --git a/schema/relationship.go b/schema/relationship.go
index e03dcc5..5716785 100644
--- a/schema/relationship.go
+++ b/schema/relationship.go
@@ -76,8 +76,8 @@ func (schema *Schema) parseRelation(field *Field) *Relationship {
 		return nil
 	}
 
-	if polymorphic := field.TagSettings["POLYMORPHIC"]; polymorphic != "" {
-		schema.buildPolymorphicRelation(relation, field, polymorphic)
+	if hasPolymorphicRelation(field.TagSettings) {
+		schema.buildPolymorphicRelation(relation, field)
 	} else if many2many := field.TagSettings["MANY2MANY"]; many2many != "" {
 		schema.buildMany2ManyRelation(relation, field, many2many)
 	} else if belongsTo := field.TagSettings["BELONGSTO"]; belongsTo != "" {
@@ -89,7 +89,8 @@ func (schema *Schema) parseRelation(field *Field) *Relationship {
 		case reflect.Slice:
 			schema.guessRelation(relation, field, guessHas)
 		default:
-			schema.err = fmt.Errorf("unsupported data type %v for %v on field %s", relation.FieldSchema, schema, field.Name)
+			schema.err = fmt.Errorf("unsupported data type %v for %v on field %s", relation.FieldSchema, schema,
+				field.Name)
 		}
 	}
 
@@ -124,6 +125,20 @@ func (schema *Schema) parseRelation(field *Field) *Relationship {
 	return relation
 }
 
+// hasPolymorphicRelation check if has polymorphic relation
+// 1. `POLYMORPHIC` tag
+// 2. `POLYMORPHICTYPE` and `POLYMORPHICID` tag
+func hasPolymorphicRelation(tagSettings map[string]string) bool {
+	if _, ok := tagSettings["POLYMORPHIC"]; ok {
+		return true
+	}
+
+	_, hasType := tagSettings["POLYMORPHICTYPE"]
+	_, hasId := tagSettings["POLYMORPHICID"]
+
+	return hasType && hasId
+}
+
 func (schema *Schema) setRelation(relation *Relationship) {
 	// set non-embedded relation
 	if rel := schema.Relationships.Relations[relation.Name]; rel != nil {
@@ -169,23 +184,41 @@ func (schema *Schema) setRelation(relation *Relationship) {
 //	  OwnerID   int
 //	  OwnerType string
 //	}
-func (schema *Schema) buildPolymorphicRelation(relation *Relationship, field *Field, polymorphic string) {
+func (schema *Schema) buildPolymorphicRelation(relation *Relationship, field *Field) {
+	polymorphic := field.TagSettings["POLYMORPHIC"]
+
 	relation.Polymorphic = &Polymorphic{
-		Value:           schema.Table,
-		PolymorphicType: relation.FieldSchema.FieldsByName[polymorphic+"Type"],
-		PolymorphicID:   relation.FieldSchema.FieldsByName[polymorphic+"ID"],
+		Value: schema.Table,
+	}
+
+	var (
+		typeName = polymorphic + "Type"
+		typeId   = polymorphic + "ID"
+	)
+
+	if value, ok := field.TagSettings["POLYMORPHICTYPE"]; ok {
+		typeName = strings.TrimSpace(value)
 	}
 
+	if value, ok := field.TagSettings["POLYMORPHICID"]; ok {
+		typeId = strings.TrimSpace(value)
+	}
+
+	relation.Polymorphic.PolymorphicType = relation.FieldSchema.FieldsByName[typeName]
+	relation.Polymorphic.PolymorphicID = relation.FieldSchema.FieldsByName[typeId]
+
 	if value, ok := field.TagSettings["POLYMORPHICVALUE"]; ok {
 		relation.Polymorphic.Value = strings.TrimSpace(value)
 	}
 
 	if relation.Polymorphic.PolymorphicType == nil {
-		schema.err = fmt.Errorf("invalid polymorphic type %v for %v on field %s, missing field %s", relation.FieldSchema, schema, field.Name, polymorphic+"Type")
+		schema.err = fmt.Errorf("invalid polymorphic type %v for %v on field %s, missing field %s",
+			relation.FieldSchema, schema, field.Name, polymorphic+"Type")
 	}
 
 	if relation.Polymorphic.PolymorphicID == nil {
-		schema.err = fmt.Errorf("invalid polymorphic type %v for %v on field %s, missing field %s", relation.FieldSchema, schema, field.Name, polymorphic+"ID")
+		schema.err = fmt.Errorf("invalid polymorphic type %v for %v on field %s, missing field %s",
+			relation.FieldSchema, schema, field.Name, polymorphic+"ID")
 	}
 
 	if schema.err == nil {
@@ -197,12 +230,14 @@ func (schema *Schema) buildPolymorphicRelation(relation *Relationship, field *Fi
 		primaryKeyField := schema.PrioritizedPrimaryField
 		if len(relation.foreignKeys) > 0 {
 			if primaryKeyField = schema.LookUpField(relation.foreignKeys[0]); primaryKeyField == nil || len(relation.foreignKeys) > 1 {
-				schema.err = fmt.Errorf("invalid polymorphic foreign keys %+v for %v on field %s", relation.foreignKeys, schema, field.Name)
+				schema.err = fmt.Errorf("invalid polymorphic foreign keys %+v for %v on field %s", relation.foreignKeys,
+					schema, field.Name)
 			}
 		}
 
 		if primaryKeyField == nil {
-			schema.err = fmt.Errorf("invalid polymorphic type %v for %v on field %s, missing primaryKey field", relation.FieldSchema, schema, field.Name)
+			schema.err = fmt.Errorf("invalid polymorphic type %v for %v on field %s, missing primaryKey field",
+				relation.FieldSchema, schema, field.Name)
 			return
 		}
 
@@ -317,7 +352,8 @@ func (schema *Schema) buildMany2ManyRelation(relation *Relationship, field *Fiel
 		Tag:  `gorm:"-"`,
 	})
 
-	if relation.JoinTable, err = Parse(reflect.New(reflect.StructOf(joinTableFields)).Interface(), schema.cacheStore, schema.namer); err != nil {
+	if relation.JoinTable, err = Parse(reflect.New(reflect.StructOf(joinTableFields)).Interface(), schema.cacheStore,
+		schema.namer); err != nil {
 		schema.err = err
 	}
 	relation.JoinTable.Name = many2many
@@ -436,7 +472,8 @@ func (schema *Schema) guessRelation(relation *Relationship, field *Field, cgl gu
 			schema.guessRelation(relation, field, guessEmbeddedHas)
 		// case guessEmbeddedHas:
 		default:
-			schema.err = fmt.Errorf("invalid field found for struct %v's field %s: define a valid foreign key for relations or implement the Valuer/Scanner interface", schema, field.Name)
+			schema.err = fmt.Errorf("invalid field found for struct %v's field %s: define a valid foreign key for relations or implement the Valuer/Scanner interface",
+				schema, field.Name)
 		}
 	}
 
@@ -492,7 +529,9 @@ func (schema *Schema) guessRelation(relation *Relationship, field *Field, cgl gu
 
 			lookUpNames := []string{lookUpName}
 			if len(primaryFields) == 1 {
-				lookUpNames = append(lookUpNames, strings.TrimSuffix(lookUpName, primaryField.Name)+"ID", strings.TrimSuffix(lookUpName, primaryField.Name)+"Id", schema.namer.ColumnName(foreignSchema.Table, strings.TrimSuffix(lookUpName, primaryField.Name)+"ID"))
+				lookUpNames = append(lookUpNames, strings.TrimSuffix(lookUpName, primaryField.Name)+"ID",
+					strings.TrimSuffix(lookUpName, primaryField.Name)+"Id", schema.namer.ColumnName(foreignSchema.Table,
+						strings.TrimSuffix(lookUpName, primaryField.Name)+"ID"))
 			}
 
 			for _, name := range lookUpNames {


Test patch:
diff --git a/clause/locking_test.go b/clause/locking_test.go
index 0e60731..e45c8e7 100644
--- a/clause/locking_test.go
+++ b/clause/locking_test.go
@@ -14,17 +14,21 @@ func TestLocking(t *testing.T) {
 		Vars    []interface{}
 	}{
 		{
-			[]clause.Interface{clause.Select{}, clause.From{}, clause.Locking{Strength: "UPDATE"}},
+			[]clause.Interface{clause.Select{}, clause.From{}, clause.Locking{Strength: clause.LockingStrengthUpdate}},
 			"SELECT * FROM `users` FOR UPDATE", nil,
 		},
 		{
-			[]clause.Interface{clause.Select{}, clause.From{}, clause.Locking{Strength: "SHARE", Table: clause.Table{Name: clause.CurrentTable}}},
+			[]clause.Interface{clause.Select{}, clause.From{}, clause.Locking{Strength: clause.LockingStrengthShare, Table: clause.Table{Name: clause.CurrentTable}}},
 			"SELECT * FROM `users` FOR SHARE OF `users`", nil,
 		},
 		{
-			[]clause.Interface{clause.Select{}, clause.From{}, clause.Locking{Strength: "UPDATE"}, clause.Locking{Strength: "UPDATE", Options: "NOWAIT"}},
+			[]clause.Interface{clause.Select{}, clause.From{}, clause.Locking{Strength: clause.LockingStrengthUpdate, Options: clause.LockingOptionsNoWait}},
 			"SELECT * FROM `users` FOR UPDATE NOWAIT", nil,
 		},
+		{
+			[]clause.Interface{clause.Select{}, clause.From{}, clause.Locking{Strength: clause.LockingStrengthUpdate, Options: clause.LockingOptionsSkipLocked}},
+			"SELECT * FROM `users` FOR UPDATE SKIP LOCKED", nil,
+		},
 	}
 
 	for idx, result := range results {
diff --git a/schema/relationship_test.go b/schema/relationship_test.go
index 1eb66bb..23d79bb 100644
--- a/schema/relationship_test.go
+++ b/schema/relationship_test.go
@@ -577,6 +577,193 @@ func TestEmbeddedHas(t *testing.T) {
 	})
 }
 
+func TestPolymorphic(t *testing.T) {
+	t.Run("has one", func(t *testing.T) {
+		type Toy struct {
+			ID        int
+			Name      string
+			OwnerID   int
+			OwnerType string
+		}
+
+		type Cat struct {
+			ID   int
+			Name string
+			Toy  Toy `gorm:"polymorphic:Owner;"`
+		}
+
+		s, err := schema.Parse(&Cat{}, &sync.Map{}, schema.NamingStrategy{})
+		if err != nil {
+			t.Fatalf("Failed to parse schema, got error %v", err)
+		}
+
+		checkEmbeddedRelations(t, s.Relationships.EmbeddedRelations, map[string]EmbeddedRelations{
+			"Cat": {
+				Relations: map[string]Relation{
+					"Toy": {
+						Name:        "Toy",
+						Type:        schema.HasOne,
+						Schema:      "User",
+						FieldSchema: "Toy",
+						Polymorphic: Polymorphic{ID: "OwnerID", Type: "OwnerType", Value: "users"},
+						References: []Reference{
+							{ForeignKey: "OwnerType", ForeignSchema: "Toy", PrimaryValue: "users"},
+						},
+					},
+				},
+			},
+		})
+	})
+
+	t.Run("has one with custom polymorphic type and id", func(t *testing.T) {
+		type Toy struct {
+			ID    int
+			Name  string
+			RefId int
+			Type  string
+		}
+
+		type Cat struct {
+			ID   int
+			Name string
+			Toy  Toy `gorm:"polymorphic:Owner;polymorphicType:Type;polymorphicId:RefId"`
+		}
+
+		s, err := schema.Parse(&Cat{}, &sync.Map{}, schema.NamingStrategy{})
+		if err != nil {
+			t.Fatalf("Failed to parse schema, got error %v", err)
+		}
+
+		checkEmbeddedRelations(t, s.Relationships.EmbeddedRelations, map[string]EmbeddedRelations{
+			"Cat": {
+				Relations: map[string]Relation{
+					"Toy": {
+						Name:        "Toy",
+						Type:        schema.HasOne,
+						Schema:      "User",
+						FieldSchema: "Toy",
+						Polymorphic: Polymorphic{ID: "ref_id", Type: "Type", Value: "users"},
+						References: []Reference{
+							{ForeignKey: "Type", ForeignSchema: "Toy", PrimaryValue: "users"},
+						},
+					},
+				},
+			},
+		})
+	})
+
+	t.Run("has one with only polymorphic type", func(t *testing.T) {
+		type Toy struct {
+			ID      int
+			Name    string
+			OwnerID int
+			Type    string
+		}
+
+		type Cat struct {
+			ID   int
+			Name string
+			Toy  Toy `gorm:"polymorphic:Owner;polymorphicType:Type"`
+		}
+
+		s, err := schema.Parse(&Cat{}, &sync.Map{}, schema.NamingStrategy{})
+		if err != nil {
+			t.Fatalf("Failed to parse schema, got error %v", err)
+		}
+
+		checkEmbeddedRelations(t, s.Relationships.EmbeddedRelations, map[string]EmbeddedRelations{
+			"Cat": {
+				Relations: map[string]Relation{
+					"Toy": {
+						Name:        "Toy",
+						Type:        schema.HasOne,
+						Schema:      "User",
+						FieldSchema: "Toy",
+						Polymorphic: Polymorphic{ID: "owner_id", Type: "Type", Value: "users"},
+						References: []Reference{
+							{ForeignKey: "Type", ForeignSchema: "Toy", PrimaryValue: "users"},
+						},
+					},
+				},
+			},
+		})
+	})
+
+	t.Run("has many", func(t *testing.T) {
+		type Toy struct {
+			ID        int
+			Name      string
+			OwnerID   int
+			OwnerType string
+		}
+
+		type Cat struct {
+			ID   int
+			Name string
+			Toys []Toy `gorm:"polymorphic:Owner;"`
+		}
+
+		s, err := schema.Parse(&Cat{}, &sync.Map{}, schema.NamingStrategy{})
+		if err != nil {
+			t.Fatalf("Failed to parse schema, got error %v", err)
+		}
+
+		checkEmbeddedRelations(t, s.Relationships.EmbeddedRelations, map[string]EmbeddedRelations{
+			"Cat": {
+				Relations: map[string]Relation{
+					"Toys": {
+						Name:        "Toys",
+						Type:        schema.HasMany,
+						Schema:      "User",
+						FieldSchema: "Toy",
+						Polymorphic: Polymorphic{ID: "OwnerID", Type: "OwnerType", Value: "users"},
+						References: []Reference{
+							{ForeignKey: "OwnerType", ForeignSchema: "Toy", PrimaryValue: "users"},
+						},
+					},
+				},
+			},
+		})
+	})
+
+	t.Run("has many with custom polymorphic type and id", func(t *testing.T) {
+		type Toy struct {
+			ID    int
+			Name  string
+			RefId int
+			Type  string
+		}
+
+		type Cat struct {
+			ID   int
+			Name string
+			Toys []Toy `gorm:"polymorphicType:Type;polymorphicId:RefId"`
+		}
+
+		s, err := schema.Parse(&Cat{}, &sync.Map{}, schema.NamingStrategy{})
+		if err != nil {
+			t.Fatalf("Failed to parse schema, got error %v", err)
+		}
+
+		checkEmbeddedRelations(t, s.Relationships.EmbeddedRelations, map[string]EmbeddedRelations{
+			"Cat": {
+				Relations: map[string]Relation{
+					"Toys": {
+						Name:        "Toys",
+						Type:        schema.HasMany,
+						Schema:      "User",
+						FieldSchema: "Toy",
+						Polymorphic: Polymorphic{ID: "ref_id", Type: "Type", Value: "users"},
+						References: []Reference{
+							{ForeignKey: "Type", ForeignSchema: "Toy", PrimaryValue: "users"},
+						},
+					},
+				},
+			},
+		})
+	})
+}
+
 func TestEmbeddedBelongsTo(t *testing.T) {
 	type Country struct {
 		ID   int `gorm:"primaryKey"`
diff --git a/schema/schema_helper_test.go b/schema/schema_helper_test.go
index 605aa03..bc32668 100644
--- a/schema/schema_helper_test.go
+++ b/schema/schema_helper_test.go
@@ -163,8 +163,8 @@ func checkSchemaRelation(t *testing.T, s *schema.Schema, relation Relation) {
 					t.Errorf("schema %v relation's join table tablename expects %v, but got %v", s, relation.JoinTable.Table, r.JoinTable.Table)
 				}
 
-				for _, f := range relation.JoinTable.Fields {
-					checkSchemaField(t, r.JoinTable, &f, nil)
+				for i := range relation.JoinTable.Fields {
+					checkSchemaField(t, r.JoinTable, &relation.JoinTable.Fields[i], nil)
 				}
 			}
 
diff --git a/schema/schema_test.go b/schema/schema_test.go
index 5bc0fb8..45e152e 100644
--- a/schema/schema_test.go
+++ b/schema/schema_test.go
@@ -46,8 +46,8 @@ func checkUserSchema(t *testing.T, user *schema.Schema) {
 		{Name: "Active", DBName: "active", BindNames: []string{"Active"}, DataType: schema.Bool},
 	}
 
-	for _, f := range fields {
-		checkSchemaField(t, user, &f, func(f *schema.Field) {
+	for i := range fields {
+		checkSchemaField(t, user, &fields[i], func(f *schema.Field) {
 			f.Creatable = true
 			f.Updatable = true
 			f.Readable = true
@@ -136,8 +136,8 @@ func TestParseSchemaWithAdvancedDataType(t *testing.T) {
 		{Name: "Admin", DBName: "admin", BindNames: []string{"Admin"}, DataType: schema.Bool},
 	}
 
-	for _, f := range fields {
-		checkSchemaField(t, user, &f, func(f *schema.Field) {
+	for i := range fields {
+		checkSchemaField(t, user, &fields[i], func(f *schema.Field) {
 			f.Creatable = true
 			f.Updatable = true
 			f.Readable = true
diff --git a/tests/associations_has_many_test.go b/tests/associations_has_many_test.go
index c31c4b4..b8e8ff5 100644
--- a/tests/associations_has_many_test.go
+++ b/tests/associations_has_many_test.go
@@ -422,7 +422,7 @@ func TestPolymorphicHasManyAssociation(t *testing.T) {
 func TestPolymorphicHasManyAssociationForSlice(t *testing.T) {
 	users := []User{
 		*GetUser("slice-hasmany-1", Config{Toys: 2}),
-		*GetUser("slice-hasmany-2", Config{Toys: 0}),
+		*GetUser("slice-hasmany-2", Config{Toys: 0, Tools: 2}),
 		*GetUser("slice-hasmany-3", Config{Toys: 4}),
 	}
 
@@ -430,6 +430,7 @@ func TestPolymorphicHasManyAssociationForSlice(t *testing.T) {
 
 	// Count
 	AssertAssociationCount(t, users, "Toys", 6, "")
+	AssertAssociationCount(t, users, "Tools", 2, "")
 
 	// Find
 	var toys []Toy
@@ -437,6 +438,14 @@ func TestPolymorphicHasManyAssociationForSlice(t *testing.T) {
 		t.Errorf("toys count should be %v, but got %v", 6, len(toys))
 	}
 
+	// Find Tools (polymorphic with custom type and id)
+	var tools []Tools
+	DB.Model(&users).Association("Tools").Find(&tools)
+
+	if len(tools) != 2 {
+		t.Errorf("tools count should be %v, but got %v", 2, len(tools))
+	}
+
 	// Append
 	DB.Model(&users).Association("Toys").Append(
 		&Toy{Name: "toy-slice-append-1"},
diff --git a/tests/helper_test.go b/tests/helper_test.go
index 1a4874e..feb67f9 100644
--- a/tests/helper_test.go
+++ b/tests/helper_test.go
@@ -23,6 +23,7 @@ type Config struct {
 	Languages int
 	Friends   int
 	NamedPet  bool
+	Tools     int
 }
 
 func GetUser(name string, config Config) *User {
@@ -47,6 +48,10 @@ func GetUser(name string, config Config) *User {
 		user.Toys = append(user.Toys, Toy{Name: name + "_toy_" + strconv.Itoa(i+1)})
 	}
 
+	for i := 0; i < config.Tools; i++ {
+		user.Tools = append(user.Tools, Tools{Name: name + "_tool_" + strconv.Itoa(i+1)})
+	}
+
 	if config.Company {
 		user.Company = Company{Name: "company-" + name}
 	}
@@ -118,11 +123,13 @@ func doCheckUser(t *testing.T, user User, expect User, unscoped bool) {
 		if err := db(unscoped).Where("id = ?", user.ID).First(&newUser).Error; err != nil {
 			t.Fatalf("errors happened when query: %v", err)
 		} else {
-			AssertObjEqual(t, newUser, user, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age", "Birthday", "CompanyID", "ManagerID", "Active")
+			AssertObjEqual(t, newUser, user, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age", "Birthday",
+				"CompanyID", "ManagerID", "Active")
 		}
 	}
 
-	AssertObjEqual(t, user, expect, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age", "Birthday", "CompanyID", "ManagerID", "Active")
+	AssertObjEqual(t, user, expect, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age", "Birthday", "CompanyID",
+		"ManagerID", "Active")
 
 	t.Run("Account", func(t *testing.T) {
 		AssertObjEqual(t, user.Account, expect.Account, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "UserID", "Number")
@@ -133,7 +140,8 @@ func doCheckUser(t *testing.T, user User, expect User, unscoped bool) {
 			} else {
 				var account Account
 				db(unscoped).First(&account, "user_id = ?", user.ID)
-				AssertObjEqual(t, account, user.Account, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "UserID", "Number")
+				AssertObjEqual(t, account, user.Account, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "UserID",
+					"Number")
 			}
 		}
 	})
@@ -193,8 +201,10 @@ func doCheckUser(t *testing.T, user User, expect User, unscoped bool) {
 			} else {
 				var manager User
 				db(unscoped).First(&manager, "id = ?", *user.ManagerID)
-				AssertObjEqual(t, manager, user.Manager, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age", "Birthday", "CompanyID", "ManagerID", "Active")
-				AssertObjEqual(t, manager, expect.Manager, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age", "Birthday", "CompanyID", "ManagerID", "Active")
+				AssertObjEqual(t, manager, user.Manager, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age",
+					"Birthday", "CompanyID", "ManagerID", "Active")
+				AssertObjEqual(t, manager, expect.Manager, "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age",
+					"Birthday", "CompanyID", "ManagerID", "Active")
 			}
 		} else if user.ManagerID != nil {
 			t.Errorf("Manager should not be created for zero value, got: %+v", user.ManagerID)
@@ -215,7 +225,8 @@ func doCheckUser(t *testing.T, user User, expect User, unscoped bool) {
 		})
 
 		for idx, team := range user.Team {
-			AssertObjEqual(t, team, expect.Team[idx], "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age", "Birthday", "CompanyID", "ManagerID", "Active")
+			AssertObjEqual(t, team, expect.Team[idx], "ID", "CreatedAt", "UpdatedAt", "DeletedAt", "Name", "Age",
+				"Birthday", "CompanyID", "ManagerID", "Active")
 		}
 	})
 
@@ -250,7 +261,8 @@ func doCheckUser(t *testing.T, user User, expect User, unscoped bool) {
 		})
 
 		for idx, friend := range user.Friends {
-			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")
 		}
 	})
 }
diff --git a/tests/migrate_test.go b/tests/migrate_test.go
index cfd3e0a..28fa315 100644
--- a/tests/migrate_test.go
+++ b/tests/migrate_test.go
@@ -18,7 +18,7 @@ import (
 )
 
 func TestMigrate(t *testing.T) {
-	allModels := []interface{}{&User{}, &Account{}, &Pet{}, &Company{}, &Toy{}, &Language{}}
+	allModels := []interface{}{&User{}, &Account{}, &Pet{}, &Company{}, &Toy{}, &Language{}, &Tools{}}
 	rand.Seed(time.Now().UnixNano())
 	rand.Shuffle(len(allModels), func(i, j int) { allModels[i], allModels[j] = allModels[j], allModels[i] })
 	DB.Migrator().DropTable("user_speaks", "user_friends", "ccc")
@@ -34,7 +34,7 @@ func TestMigrate(t *testing.T) {
 	if tables, err := DB.Migrator().GetTables(); err != nil {
 		t.Fatalf("Failed to get database all tables, but got error %v", err)
 	} else {
-		for _, t1 := range []string{"users", "accounts", "pets", "companies", "toys", "languages"} {
+		for _, t1 := range []string{"users", "accounts", "pets", "companies", "toys", "languages", "tools"} {
 			hasTable := false
 			for _, t2 := range tables {
 				if t2 == t1 {
@@ -93,7 +93,8 @@ func TestAutoMigrateInt8PG(t *testing.T) {
 		Test: func(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
 			sql, _ := fc()
 			if strings.HasPrefix(sql, "ALTER TABLE \"migrate_ints\" ALTER COLUMN \"int8\" TYPE smallint") {
-				t.Fatalf("shouldn't execute ALTER COLUMN TYPE if such type is already existed in DB schema: sql: %s", sql)
+				t.Fatalf("shouldn't execute ALTER COLUMN TYPE if such type is already existed in DB schema: sql: %s",
+					sql)
 			}
 		},
 	}
@@ -432,40 +433,50 @@ func TestTiDBMigrateColumns(t *testing.T) {
 			switch columnType.Name() {
 			case "id":
 				if v, ok := columnType.PrimaryKey(); !ok || !v {
-					t.Fatalf("column id primary key should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+					t.Fatalf("column id primary key should be correct, name: %v, column: %#v", columnType.Name(),
+						columnType)
 				}
 			case "name":
 				dataType := DB.Dialector.DataTypeOf(stmt.Schema.LookUpField(columnType.Name()))
 				if !strings.Contains(strings.ToUpper(dataType), strings.ToUpper(columnType.DatabaseTypeName())) {
-					t.Fatalf("column name type should be correct, name: %v, length: %v, expects: %v, column: %#v", columnType.Name(), columnType.DatabaseTypeName(), dataType, columnType)
+					t.Fatalf("column name type should be correct, name: %v, length: %v, expects: %v, column: %#v",
+						columnType.Name(), columnType.DatabaseTypeName(), dataType, columnType)
 				}
 				if length, ok := columnType.Length(); !ok || length != 100 {
-					t.Fatalf("column name length should be correct, name: %v, length: %v, expects: %v, column: %#v", columnType.Name(), length, 100, columnType)
+					t.Fatalf("column name length should be correct, name: %v, length: %v, expects: %v, column: %#v",
+						columnType.Name(), length, 100, columnType)
 				}
 			case "age":
 				if v, ok := columnType.DefaultValue(); !ok || v != "18" {
-					t.Fatalf("column age default value should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+					t.Fatalf("column age default value should be correct, name: %v, column: %#v", columnType.Name(),
+						columnType)
 				}
 				if v, ok := columnType.Comment(); !ok || v != "my age" {
-					t.Fatalf("column age comment should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+					t.Fatalf("column age comment should be correct, name: %v, column: %#v", columnType.Name(),
+						columnType)
 				}
 			case "code":
 				if v, ok := columnType.Unique(); !ok || !v {
-					t.Fatalf("column code unique should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+					t.Fatalf("column code unique should be correct, name: %v, column: %#v", columnType.Name(),
+						columnType)
 				}
 				if v, ok := columnType.DefaultValue(); !ok || v != "hello" {
-					t.Fatalf("column code default value should be correct, name: %v, column: %#v, default value: %v", columnType.Name(), columnType, v)
+					t.Fatalf("column code default value should be correct, name: %v, column: %#v, default value: %v",
+						columnType.Name(), columnType, v)
 				}
 				if v, ok := columnType.Comment(); !ok || v != "my code2" {
-					t.Fatalf("column code comment should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+					t.Fatalf("column code comment should be correct, name: %v, column: %#v", columnType.Name(),
+						columnType)
 				}
 			case "code2":
 				// Code2 string `gorm:"comment:my code2;default:hello"`
 				if v, ok := columnType.DefaultValue(); !ok || v != "hello" {
-					t.Fatalf("column code default value should be correct, name: %v, column: %#v, default value: %v", columnType.Name(), columnType, v)
+					t.Fatalf("column code default value should be correct, name: %v, column: %#v, default value: %v",
+						columnType.Name(), columnType, v)
 				}
 				if v, ok := columnType.Comment(); !ok || v != "my code2" {
-					t.Fatalf("column code comment should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+					t.Fatalf("column code comment should be correct, name: %v, column: %#v", columnType.Name(),
+						columnType)
 				}
 			}
 		}
@@ -497,7 +508,8 @@ func TestTiDBMigrateColumns(t *testing.T) {
 		t.Fatalf("Failed to add column, got %v", err)
 	}
 
-	if err := DB.Table("column_structs").Migrator().RenameColumn(&NewColumnStruct{}, "NewName", "new_new_name"); err != nil {
+	if err := DB.Table("column_structs").Migrator().RenameColumn(&NewColumnStruct{}, "NewName",
+		"new_new_name"); err != nil {
 		t.Fatalf("Failed to add column, got %v", err)
 	}
 
@@ -561,36 +573,45 @@ func TestMigrateColumns(t *testing.T) {
 			switch columnType.Name() {
 			case "id":
 				if v, ok := columnType.PrimaryKey(); !ok || !v {
-					t.Fatalf("column id primary key should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+					t.Fatalf("column id primary key should be correct, name: %v, column: %#v", columnType.Name(),
+						columnType)
 				}
 			case "name":
 				dataType := DB.Dialector.DataTypeOf(stmt.Schema.LookUpField(columnType.Name()))
 				if !strings.Contains(strings.ToUpper(dataType), strings.ToUpper(columnType.DatabaseTypeName())) {
-					t.Fatalf("column name type should be correct, name: %v, length: %v, expects: %v, column: %#v", columnType.Name(), columnType.DatabaseTypeName(), dataType, columnType)
+					t.Fatalf("column name type should be correct, name: %v, length: %v, expects: %v, column: %#v",
+						columnType.Name(), columnType.DatabaseTypeName(), dataType, columnType)
 				}
 				if length, ok := columnType.Length(); !sqlite && (!ok || length != 100) {
-					t.Fatalf("column name length should be correct, name: %v, length: %v, expects: %v, column: %#v", columnType.Name(), length, 100, columnType)
+					t.Fatalf("column name length should be correct, name: %v, length: %v, expects: %v, column: %#v",
+						columnType.Name(), length, 100, columnType)
 				}
 			case "age":
 				if v, ok := columnType.DefaultValue(); !ok || v != "18" {
-					t.Fatalf("column age default value should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+					t.Fatalf("column age default value should be correct, name: %v, column: %#v", columnType.Name(),
+						columnType)
 				}
 				if v, ok := columnType.Comment(); !sqlite && !sqlserver && (!ok || v != "my age") {
-					t.Fatalf("column age comment should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+					t.Fatalf("column age comment should be correct, name: %v, column: %#v", columnType.Name(),
+						columnType)
 				}
 			case "code":
 				if v, ok := columnType.Unique(); !ok || !v {
-					t.Fatalf("column code unique should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+					t.Fatalf("column code unique should be correct, name: %v, column: %#v", columnType.Name(),
+						columnType)
 				}
 				if v, ok := columnType.DefaultValue(); !sqlserver && (!ok || v != "hello") {
-					t.Fatalf("column code default value should be correct, name: %v, column: %#v, default value: %v", columnType.Name(), columnType, v)
+					t.Fatalf("column code default value should be correct, name: %v, column: %#v, default value: %v",
+						columnType.Name(), columnType, v)
 				}
 				if v, ok := columnType.Comment(); !sqlite && !sqlserver && (!ok || v != "my code2") {
-					t.Fatalf("column code comment should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+					t.Fatalf("column code comment should be correct, name: %v, column: %#v", columnType.Name(),
+						columnType)
 				}
 			case "code2":
 				if v, ok := columnType.Unique(); !sqlserver && (!ok || !v) {
-					t.Fatalf("column code2 unique should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+					t.Fatalf("column code2 unique should be correct, name: %v, column: %#v", columnType.Name(),
+						columnType)
 				}
 			case "code3":
 				// TODO
@@ -627,7 +648,8 @@ func TestMigrateColumns(t *testing.T) {
 		t.Fatalf("Failed to add column, got %v", err)
 	}
 
-	if err := DB.Table("column_structs").Migrator().RenameColumn(&NewColumnStruct{}, "NewName", "new_new_name"); err != nil {
+	if err := DB.Table("column_structs").Migrator().RenameColumn(&NewColumnStruct{}, "NewName",
+		"new_new_name"); err != nil {
 		t.Fatalf("Failed to add column, got %v", err)
 	}
 
@@ -1555,7 +1577,8 @@ func TestMigrateIgnoreRelations(t *testing.T) {
 func TestMigrateView(t *testing.T) {
 	DB.Save(GetUser("joins-args-db", Config{Pets: 2}))
 
-	if err := DB.Migrator().CreateView("invalid_users_pets", gorm.ViewOption{Query: nil}); err != gorm.ErrSubQueryRequired {
+	if err := DB.Migrator().CreateView("invalid_users_pets",
+		gorm.ViewOption{Query: nil}); err != gorm.ErrSubQueryRequired {
 		t.Fatalf("no view should be created, got %v", err)
 	}
 
@@ -1624,17 +1647,20 @@ func TestMigrateExistingBoolColumnPG(t *testing.T) {
 			switch columnType.Name() {
 			case "id":
 				if v, ok := columnType.PrimaryKey(); !ok || !v {
-					t.Fatalf("column id primary key should be correct, name: %v, column: %#v", columnType.Name(), columnType)
+					t.Fatalf("column id primary key should be correct, name: %v, column: %#v", columnType.Name(),
+						columnType)
 				}
 			case "string_bool":
 				dataType := DB.Dialector.DataTypeOf(stmt.Schema.LookUpField(columnType.Name()))
 				if !strings.Contains(strings.ToUpper(dataType), strings.ToUpper(columnType.DatabaseTypeName())) {
-					t.Fatalf("column name type should be correct, name: %v, length: %v, expects: %v, column: %#v", columnType.Name(), columnType.DatabaseTypeName(), dataType, columnType)
+					t.Fatalf("column name type should be correct, name: %v, length: %v, expects: %v, column: %#v",
+						columnType.Name(), columnType.DatabaseTypeName(), dataType, columnType)
 				}
 			case "smallint_bool":
 				dataType := DB.Dialector.DataTypeOf(stmt.Schema.LookUpField(columnType.Name()))
 				if !strings.Contains(strings.ToUpper(dataType), strings.ToUpper(columnType.DatabaseTypeName())) {
-					t.Fatalf("column name type should be correct, name: %v, length: %v, expects: %v, column: %#v", columnType.Name(), columnType.DatabaseTypeName(), dataType, columnType)
+					t.Fatalf("column name type should be correct, name: %v, length: %v, expects: %v, column: %#v",
+						columnType.Name(), columnType.DatabaseTypeName(), dataType, columnType)
 				}
 			}
 		}
@@ -1659,7 +1685,8 @@ func TestTableType(t *testing.T) {
 
 	DB.Migrator().DropTable(&City{})
 
-	if err := DB.Set("gorm:table_options", fmt.Sprintf("ENGINE InnoDB COMMENT '%s'", tblComment)).AutoMigrate(&City{}); err != nil {
+	if err := DB.Set("gorm:table_options",
+		fmt.Sprintf("ENGINE InnoDB COMMENT '%s'", tblComment)).AutoMigrate(&City{}); err != nil {
 		t.Fatalf("failed to migrate cities tables, got error: %v", err)
 	}
 
diff --git a/tests/tests_test.go b/tests/tests_test.go
index f9c6cab..a127734 100644
--- a/tests/tests_test.go
+++ b/tests/tests_test.go
@@ -107,7 +107,7 @@ func OpenTestConnection(cfg *gorm.Config) (db *gorm.DB, err error) {
 
 func RunMigrations() {
 	var err error
-	allModels := []interface{}{&User{}, &Account{}, &Pet{}, &Company{}, &Toy{}, &Language{}, &Coupon{}, &CouponProduct{}, &Order{}, &Parent{}, &Child{}}
+	allModels := []interface{}{&User{}, &Account{}, &Pet{}, &Company{}, &Toy{}, &Language{}, &Coupon{}, &CouponProduct{}, &Order{}, &Parent{}, &Child{}, &Tools{}}
 	rand.Seed(time.Now().UnixNano())
 	rand.Shuffle(len(allModels), func(i, j int) { allModels[i], allModels[j] = allModels[j], allModels[i] })
 
diff --git a/tests/update_test.go b/tests/update_test.go
index b719cc4..9eb9dbf 100644
--- a/tests/update_test.go
+++ b/tests/update_test.go
@@ -882,3 +882,52 @@ func TestSaveWithHooks(t *testing.T) {
 		t.Errorf(`token content should be "token2_encrypted", but got: "%s"`, o2.Token.Content)
 	}
 }
+
+// only postgres, sqlserver, sqlite support update from
+func TestUpdateFrom(t *testing.T) {
+	if DB.Dialector.Name() != "postgres" && DB.Dialector.Name() != "sqlite" && DB.Dialector.Name() != "sqlserver" {
+		return
+	}
+
+	users := []*User{
+		GetUser("update-from-1", Config{Account: true}),
+		GetUser("update-from-2", Config{Account: true}),
+		GetUser("update-from-3", Config{}),
+	}
+
+	if err := DB.Create(&users).Error; err != nil {
+		t.Fatalf("errors happened when create: %v", err)
+	} else if users[0].ID == 0 {
+		t.Fatalf("user's primary value should not zero, %v", users[0].ID)
+	} else if users[0].UpdatedAt.IsZero() {
+		t.Fatalf("user's updated at should not zero, %v", users[0].UpdatedAt)
+	}
+
+	if rowsAffected := DB.Model(&User{}).Clauses(clause.From{Tables: []clause.Table{{Name: "accounts"}}}).Where("accounts.user_id = users.id AND accounts.number = ? AND accounts.deleted_at IS NULL", users[0].Account.Number).Update("name", "franco").RowsAffected; rowsAffected != 1 {
+		t.Errorf("should only update one record, but got %v", rowsAffected)
+	}
+
+	var result User
+	if err := DB.Where("id = ?", users[0].ID).First(&result).Error; err != nil {
+		t.Errorf("errors happened when query before user: %v", err)
+	} else if result.UpdatedAt.UnixNano() == users[0].UpdatedAt.UnixNano() {
+		t.Errorf("user's updated at should be changed, but got %v, was %v", result.UpdatedAt, users[0].UpdatedAt)
+	} else if result.Name != "franco" {
+		t.Errorf("user's name should be updated")
+	}
+
+	if rowsAffected := DB.Model(&User{}).Clauses(clause.From{Tables: []clause.Table{{Name: "accounts"}}}).Where("accounts.user_id = users.id AND accounts.number IN ? AND accounts.deleted_at IS NULL", []string{users[0].Account.Number, users[1].Account.Number}).Update("name", gorm.Expr("accounts.number")).RowsAffected; rowsAffected != 2 {
+		t.Errorf("should update two records, but got %v", rowsAffected)
+	}
+
+	var results []User
+	if err := DB.Preload("Account").Find(&results, []uint{users[0].ID, users[1].ID}).Error; err != nil {
+		t.Errorf("Not error should happen when finding users, but got %v", err)
+	}
+
+	for _, user := range results {
+		if user.Name != user.Account.Number {
+			t.Errorf("user's name should be equal to the account's number %v, but got %v", user.Account.Number, user.Name)
+		}
+	}
+}
diff --git a/utils/tests/models.go b/utils/tests/models.go
index a4bad2f..f9f4f50 100644
--- a/utils/tests/models.go
+++ b/utils/tests/models.go
@@ -20,7 +20,8 @@ type User struct {
 	Account   Account
 	Pets      []*Pet
 	NamedPet  *Pet
-	Toys      []Toy `gorm:"polymorphic:Owner"`
+	Toys      []Toy   `gorm:"polymorphic:Owner"`
+	Tools     []Tools `gorm:"polymorphicType:Type;polymorphicId:CustomID"`
 	CompanyID *int
 	Company   Company
 	ManagerID *uint
@@ -51,6 +52,13 @@ type Toy struct {
 	OwnerType string
 }
 
+type Tools struct {
+	gorm.Model
+	Name     string
+	CustomID string
+	Type     string
+}
+
 type Company struct {
 	ID   int
 	Name string