-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Assigning nil value to *uuid.UUID field in Updates #7099
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR.
Why is Array special, what about Slice? I think we shouldn't provide unique compatibility for external packages.
Actually the issue is whether we should update nil values to the original object when performing Update. If so, how should the Embedded object handle it?
Actually this PR still doesn't work in Embedded objects.
Hey @a631807682 thank you so much for your review. My responses to your questions are as below.
The
Yes, we should surely update nil values in the original object when performing Update, since original object needs to be consistent with the underlying database changes performed in the update operation. It's usually a best practice in ORMs to keep the model entity object (i.e. the original object) consistent with the underlying database, as developers may continue using the original object in code for further operations after the update, assuming that it's already in sync with the database.
This PR specifically fixes only the issue with |
For this issue, my first instinct is to use Customize Data Types: package datatypes
import (
"database/sql/driver"
"github.com/google/uuid"
"gorm.io/gorm/schema"
)
type UUID uuid.UUID
func (UUID) GormDataType() string {
return string(schema.String)
}
func (u *UUID) Scan(value interface{}) error {
var result uuid.UUID
if err := result.Scan(value); err != nil {
return err
}
*u = UUID(result)
return nil
}
func (u UUID) Value() (driver.Value, error) {
return uuid.UUID(u).Value()
}
func (u UUID) String() string {
return uuid.UUID(u).String()
} then use var uuidPtr *datatypes.UUID = nil
DB.Model(&p).Updates(map[string]interface{}{"unique_id": uuidPtr}).Error
// UPDATE "p" SET "unique_id"=NULL WHERE "id" = 1 |
Yes, we should try to support it, but I would like it to be in a more general way.
This test describes the problem with updating the original object in an embedded model, and the behavior that this PR breaks. The problem here is that to set the value of a field of an embedded object, you need to instantiate the object, even if it is a nil value. In my opinion, a more general solution may be to use Scanner and Valuer as @iTanken said (if feasible, it can be included in the https://github.com/go-gorm/datatypes project), or to check whether the embedded object of pointer type is zero value after setting all the fields of the object. gorm/tests/embedded_struct_test.go Line 101 in 4a50b36
type Author struct {
ID string
Name string
Email string
Age int
Content Content
ContentPtr *Content
Birthday time.Time
BirthdayPtr *time.Time
+ UUID *uuid.UUID
} |
369e5e5
to
4cd55f6
Compare
Hey @a631807682 @iTanken, thank you both for your comments. Yes I totally agree that we should keep it more general, so as per both your suggestions above, I've raised this PR on the @a631807682 I've also added handling for embedded structs and have included the test case that you provided above (refer here). That test case is passing now. Also have replaced Note: PR check diff --git a/tests/update_test.go b/tests/update_test.go
index a4f902b..84d41c9 100644
--- a/tests/update_test.go
+++ b/tests/update_test.go
@@ -9,6 +9,7 @@ import (
"time"
"github.com/google/uuid"
+ "gorm.io/datatypes"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/utils"
@@ -185,7 +186,7 @@ func TestUpdates(t *testing.T) {
user3.Age += 100
AssertObjEqual(t, user4, user3, "UpdatedAt", "Age")
- // Updates() with map and uuid.UUID - Case 1 - Update with UUID value
+ // Updates() with map and datatypes.UUID - Case 1 - Update with UUID value
uuidVal, uuidErr := uuid.NewUUID()
if uuidErr != nil {
t.Errorf("No error should occur while generating UUID, but got %v", uuidErr)
@@ -198,8 +199,8 @@ func TestUpdates(t *testing.T) {
// Expecting the model object (user4) to reflect the UUID value assignment.
AssertEqual(t, user4.UserUUID, uuidVal)
- // Updates() with map and uuid.UUID - Case 2 - Update with UUID nil pointer
- var nilUUIDPtr *uuid.UUID = nil
+ // Updates() with map and datatypes.UUID - Case 2 - Update with UUID nil pointer
+ var nilUUIDPtr *datatypes.UUID = nil
uuidErr = tx.Updates(map[string]interface{}{"user_uuid": nilUUIDPtr}).Error
if uuidErr != nil {
t.Errorf("No error should occur while updating with nil UUID pointer, but got %v", uuidErr)
@@ -207,12 +208,13 @@ func TestUpdates(t *testing.T) {
// Expecting the model object (user4) to reflect the UUID nil pointer assignment.
AssertEqual(t, user4.UserUUID, nilUUIDPtr)
- // Updates() with map and uuid.UUID - Case 3 - Update with a non-nil UUID pointer
+ // Updates() with map and datatypes.UUID - Case 3 - Update with a non-nil UUID pointer
uuidVal2, uuidErr := uuid.NewUUID()
if uuidErr != nil {
t.Errorf("No error should occur while generating UUID, but got %v", uuidErr)
}
- var nonNilUUIDPtr *uuid.UUID = &uuidVal2
+ castedUUIDVal2 := datatypes.UUID(uuidVal2)
+ var nonNilUUIDPtr *datatypes.UUID = &castedUUIDVal2
uuidErr = tx.Updates(map[string]interface{}{"user_uuid": nonNilUUIDPtr}).Error
if uuidErr != nil {
t.Errorf("No error should occur while updating with non-nil UUID pointer, but got %v", uuidErr)
diff --git a/utils/tests/models.go b/utils/tests/models.go
index 7690641..060467c 100644
--- a/utils/tests/models.go
+++ b/utils/tests/models.go
@@ -4,7 +4,7 @@ import (
"database/sql"
"time"
- "github.com/google/uuid"
+ "gorm.io/datatypes"
"gorm.io/gorm"
)
@@ -31,7 +31,7 @@ type User struct {
Languages []Language `gorm:"many2many:UserSpeak;"`
Friends []*User `gorm:"many2many:user_friends;"`
Active bool
- UserUUID *uuid.UUID
+ UserUUID *datatypes.UUID
}
type Account struct {
|
This PR adds handling for the assignment of a nil value to a *uuid.UUID field (resolved as a reflect.Ptr to a reflect.Array), to ensure that the model object reflects the correct value after Updates() has completed. This PR also adds few supporting test cases for Updates() with a map and uuid.UUID column.
4cd55f6
to
316303a
Compare
Hi! As per our discussion in the above comment, I've added a datatype I've updated this PR to use datatypes v1.2.2 and also replaced |
a944e37
to
316303a
Compare
Will dig deeper on why this is happening, as I see similar failures in #7198 as well. If anyone has any context on this issue, please share. Thanks! |
@@ -30,6 +31,7 @@ type User struct { | |||
Languages []Language `gorm:"many2many:UserSpeak;"` | |||
Friends []*User `gorm:"many2many:user_friends;"` | |||
Active bool | |||
UserUUID *datatypes.UUID |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding is that the expectation of this PR is to rewrite uuid to nil, so this expectation should actually take effect in embedded (the embedded structure is nil, or if the embedded has other fields that are not nil, then the field is nil).
But it cannot be done through the judgment of reflect.Array
and field.OwnerSchema
, and the judgment of reflect.Array
is not easy to understand unless someone sees this PR.
From the expectation, I think the PR is only partially completed, although it does solve the problem in #7090.
-UserUUID *datatypes.UUID
+EmbeddedUUID *EmbeddedUUID `gorm:"embedded"`
+type EmbeddedUUID struct {
+ UserUUID *datatypes.UUID
+}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I agree with you that this is not the cleanest way to fix this problem.
Using an embedded structure is much cleaner (and no code change here!), although I'm not sure how many users will be aware of using it when the problem arises - which is why I felt that we should support this way.
But if using embedded structures is the only right way, please feel free to close this PR. Thank you! 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't mean we should use embedded, but we should keep the same behavior as embedded.
You can wait for jinzhu's review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure no problem, thank you for your review though.
@@ -5,5 +5,14 @@ go 1.18 | |||
require ( | |||
github.com/jinzhu/inflection v1.0.0 | |||
github.com/jinzhu/now v1.1.5 | |||
golang.org/x/text v0.14.0 | |||
golang.org/x/text v0.18.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, we need to ensure that gorm.io/gorm does not introduce any external dependencies.
We could put all related dependencies within the tests package.
Fixes #7090.
What did this pull request do?
This PR adds handling for the assignment of a
nil
value to a*uuid.UUID
field (resolved as areflect.Ptr
to areflect.Array
), to ensure that the model object reflects the correct value after Updates() has completed.This PR also adds few supporting test cases for
Updates()
with a map anduuid.UUID
column.User Case Description
This fix is necessary to keep the gorm model object consistent with the database operation performed via
Updates()
. Which means that ifnil
value is set to a UUID column in the database, then the same should reflect in the model object as well.Other Notes
Please note that I had to inevitably add the
github.com/google/uuid
package to the maingo.mod
file here since theUserUUID
column in the testUser
model needed it as an import. TheUserUUID
field is being used in the new test cases that I've added in this PR.