We are given a function SetUserPreference
that is intended to set a user preference. The function uses GORM's FirstOrCreate
method to either update an existing record or create a new one.
The issue arises when the value
is an empty string. Let's analyze the code:
The function SetUserPreference
:
UserPreference
struct with the provided userID
, key
, and value
.GetDB().WithContext(ctx)
to get a DB instance with the context.Where("user_id = ? AND preference_key = ?", userID, key)
to find an existing record for the same user and key.Assign
to set the PreferenceValue
to the provided value
.FirstOrCreate
to either:
PreferenceValue
with the one from Assign
? But note: the Assign
method is used to set the field for the update, but then we are passing preference
to FirstOrCreate
which has the same PreferenceValue
as the one we assigned? Actually, the Assign
method is used to set the update attributes, but then the FirstOrCreate
method also uses the struct we pass to create a new record if not found.However, note that the Assign
method is used to assign the PreferenceValue
for the update, but the condition for FirstOrCreate
is only on user_id
and preference_key
.
But the problem is when value == ""
.
Let's consider two cases:
Case 1: When a record does not exist for the given user and key.
value
. If value
is an empty string, then the record will have an empty string for PreferenceValue
. This is acceptable.Case 2: When a record already exists for the given user and key.
FirstOrCreate
which will find the existing record and then ... what? The Assign
method is used to set the PreferenceValue
to the new value (which is empty string). Then, what does FirstOrCreate
do? Actually, the FirstOrCreate
method in GORM does not update the found record by default. It only creates if not found.Looking at the GORM documentation for FirstOrCreate
:
It returns the first record that matches the condition. If no record is found, it creates a new record with the given struct.
But note: we are also using Assign
? The Assign
method is used to assign attributes for the update in the same query? Actually, the Assign
method in GORM is used in the context of updates. But in the FirstOrCreate
method, the behavior is:
Assign
attributes are ignored? Because FirstOrCreate
doesn't update the found record.Wait, let me check the GORM documentation:
The method FirstOrCreate
is used to find the first record or create it if it doesn't exist. It does not update the found record. The Assign
method in this context is actually not doing what we expect.
In the given code, the Assign
is called before FirstOrCreate
. The Assign
method in GORM is used to set attributes for an update. But in the FirstOrCreate
method, if the record is found, then the Assign
attributes are not used. Only when creating a new record, the struct we pass is used.
So, the issue is that when the record already exists, we are not updating it. The Assign
method in the chain is not triggering an update.
Therefore, the current code only updates the record when we are creating a new one? Actually, no: the code does not update the existing record at all.
So, what we intended is to update the existing record with the new value? Then we should use Save
or Updates
instead.
But the code uses FirstOrCreate
and then what? It doesn't update the found record.
So the problem is that the function does not update the existing record when value
is an empty string? Actually, it doesn't update at all.
Let me explain:
The code:
The Assign
method in GORM is used to assign attributes for the update, but it is typically used with Update
or Updates
. In the context of FirstOrCreate
, the Assign
method is not documented to do anything for the found record.
In fact, the FirstOrCreate
method will only create a record if not found, and then the Assign
method is ignored because we are not in an update operation.
So the current code only sets the value when creating a new record. For an existing record, it does nothing.
Therefore, the issue is that the existing record is not updated when the value is an empty string. But note: the problem might be that the user expects that when they set a value (even an empty string) to a preference, it should be updated. However, the current code does not update the existing record at all.
So the function is broken for updating existing records, regardless of the value being empty or not.
But why did we notice the problem when value
is empty? Maybe because when the value is non-empty, the user might not have an existing record? Or maybe the test case was with an existing record and an empty string? Or perhaps the existing record had a non-empty string and we want to update it to empty?
In any case, the function should update the existing record.
How to fix?
We can change the approach:
Option 1: Use Save
method.
But note: Save
will update the entire record. We can do:
Option 2: Use Updates
with FirstOrInit
and then create if not found? But that might be more complex.
Alternatively, we can use the Clauses(clause.OnConflict{DoUpdates: clause.Assignments})
with Create
to handle the update on conflict.
But let's stick to a clear method.
However, note the original code used FirstOrCreate
and then set the value only on create? And we want to update even if the value is empty.
But the problem in the original code is that it doesn't update the existing record. So we need to update the existing record.
But why did the original code not work for empty string? Because when the record exists and we try to set the value to empty, the function does nothing? So the value remains the old one? That is the issue.
So the fix is to update the record when it exists.
But note: the original code uses Assign
and then FirstOrCreate
. The Assign
method in GORM is for update, but we are not doing an update. We are doing a first or create.
So we can change the code to use the update method if the record exists.
Alternatively, we can use the Save
method on the struct after FirstOrCreate
? But that would update the record we just created? And also, if we found one, we have to update it.
But note: the FirstOrCreate
returns the record. We can then update it and save it? But that would require two operations.
Alternatively, we can use a single transaction and update if exists, or create if not.
But the original code was atomic? It was one query.
We can use:
But note: this will update all columns (except the primary key) on conflict. But we only want to update the preference_value
? And the updated_at
?
In the struct, we have:
ID
is primary key, so it won't be updated.UserID
and PreferenceKey
are used in the condition, so they are not updated.PreferenceValue
and UpdatedAt
will be updated? But note: the Create
method will set the CreatedAt
and UpdatedAt
if defined. But in the case of conflict, we are updating, so the UpdatedAt
will be updated? And the PreferenceValue
will be updated.But what if we don't want to update the CreatedAt
? Actually, the CreatedAt
is set only on create, and the UpdatedAt
is set on create and update.
But the above method will update the entire row? Actually, the UpdateAll
updates all columns excluding the primary key and the columns in the condition? But the condition is on user_id
and preference_key
? Actually, the condition is implied by the conflict index? We have to set the conflict index.
Alternatively, we can specify the conflict condition:
But note: the updated_at
is automatically set by the database via the trigger? Actually, we have the UpdatedAt
field in the struct and GORM will set it. But in the Create
method, it will set the UpdatedAt
to the current time. And when updating, it will also set the UpdatedAt
. So we don't need to specify the updated_at
in the update.
But the above method will set the preference_value
and the updated_at
? Actually, the DoUpdates
can be set to update specific columns.
Alternatively, we can use:
But note: we don't have the value
in the struct? Actually, we are passing the struct to Create
, so the preference
struct has the value
. But the Assignments
requires a map.
Alternatively, we can use:
This will update the preference_value
and also the updated_at
? Because GORM automatically updates the UpdatedAt
when defined? Yes, if we have the UpdatedAt
field and we are using GORM's automatic timestamp, then it will be updated.
But note: the Create
method with OnConflict
and DoUpdates
might not trigger the automatic UpdatedAt
? Because we are doing an insert and then on conflict update. We can set the UpdatedAt
in the struct to the current time? But we are passing a struct with the current time? Actually, we set:
But in the update, the UpdatedAt
will be set again? It will be the same as the one in the struct? But we want the update time to be the current time.
Alternatively, we can rely on GORM's hooks. The Create
method will set the UpdatedAt
to the current time even in the update? Actually, GORM will set the UpdatedAt
for the record when we use the Save
method, but in the Create
method with OnConflict
and update, it might not? Because it's a create and then update.
To be safe, we can set the UpdatedAt
in the Assignments
map.
But the original function did not set the UpdatedAt
explicitly? It was set by the database via the trigger? Actually, the UpdatedAt
is set by GORM in the struct before saving? But in the case of update, it will be set again?
Given the complexity, and since the original table has the UpdatedAt
with ON UPDATE CURRENT_TIMESTAMP
, we can rely on that? But note: the original table definition for updated_at
is:
So the database will update the updated_at
automatically on update.
Therefore, we don't need to set it in the application. We can remove the UpdatedAt
from the struct? But no, because GORM will try to set it and might override the database trigger? Actually, GORM by default will set the UpdatedAt
if the field is present. So if we want to rely on the database, we should remove the UpdatedAt
field from the struct? Or we can set the UpdatedAt
to nil and let the database handle it? But that might not be straightforward.
Alternatively, we can skip the UpdatedAt
in the struct and let the database handle it. But the original struct has the UpdatedAt
field. So we must keep it? And GORM will set it on every update? That would override the database trigger.
To avoid this, we can adjust the model:
But that would make it read only. Or we can set the UpdatedAt
to be handled by the database and not by GORM? We can use:
But then we lose the ability to read it?
Alternatively, we can set the UpdatedAt
in the struct to the current time just before saving? But that would be the same as the database? And then the database would set it again? So we would have two updates?
Given the complexity, and since the problem is about the empty string, let's go back to the original goal.
The original function did not update the existing record. So we need to update it.
We can do:
But note: the Save
method will update all fields. And the CreatedAt
will be set to the existing one? Because we are not changing it. And the UpdatedAt
will be updated by GORM? But we want the database to update the UpdatedAt
? Or we can let GORM update it?
Since the table has ON UPDATE CURRENT_TIMESTAMP
, we can rely on that. But if we use GORM's Save
, it will set the UpdatedAt
to the current time in the application and then the database might set it again? That would be two updates? And the time might be slightly different?
Alternatively, we can update only the PreferenceValue
and let the database update the UpdatedAt
:
But then we don't need to set the preference.ID
and use Save
.
So:
This way, we avoid setting the UpdatedAt
manually and let the database trigger handle it.
But note: the Update
method will not set the UpdatedAt
in the struct, but the database will update the updated_at
column. When we read the record later, we will see the updated value.
This should work for any value
, including empty string.
Therefore, we can change the function to this.
But let's consider performance: we are doing a select and then an update (or an insert). This is two operations. But it's acceptable.
Alternatively, we can use the Clauses
with OnConflict
as described earlier, but then we have to handle the timestamps carefully.
Given the above, I choose the transaction with select and then update or create.
But note: the original function used to set the entire struct and then do a FirstOrCreate
which might have created a new record with the same user_id
and preference_key
? But that would have a duplicate key error? Because we have a unique index on (user_id, preference_key)?
Looking at the original table:
So there is a unique index on (user_id, preference_key). Therefore, we cannot have two records for the same user and key.
So the OnConflict
approach is safe.
But to be consistent and clear, I will show the transaction approach.
But if we want to avoid the transaction and do it in one query, we can use:
This will work for both insert and update. And the database will update the updated_at
column automatically.
But note: the Create
method will set the CreatedAt
and UpdatedAt
in the struct to the current time. But when the conflict happens and we do an update, the CreatedAt
should remain the original one? But we are not updating the created_at
in the update. So the created_at
in the struct is set to the current time during the create, but then we are not using it in the update. And the database will not change the created_at
. So it's safe.
But the Create
method will set the CreatedAt
and UpdatedAt
in the struct and then the database will set them again? Actually, no: the Create
method is used to insert, and if there's a conflict, it becomes an update. In the update, we are not updating created_at
or updated_at
(because we only update preference_value
). But the database trigger will update the updated_at
? Yes.
And the created_at
remains the original one.
But what about the UpdatedAt
in the struct? We are not using it in the update, so it remains the time when we tried to create. But when we read the record later, the updated_at
from the database will be the time of the update. So the struct field UpdatedAt
is not in sync? That's okay because we are not using it in the update.
But if we later read the record, we will get the correct updated_at
from the database.
So this approach is acceptable.
And it works for the empty string because we are explicitly updating the preference_value
to the given value, even if it's empty.
Therefore, I will choose this approach.
But note: the Clauses
method might not be available in older GORM versions? But we are using GORM 2.0? I assume it's available.
So the fixed function is:
This will create a new record if it doesn't exist, or update the preference_value
if it does exist. And it will work for any value, including empty string.
But note: the map
in Assignments
must have the key as the column name. And we are using preference_value
(snake_case) because that's the column name.
Also, we are not updating any other fields.
This should fix the issue.
The issue arises because the original code using FirstOrCreate
does not update existing records—it only creates new ones. When value
is an empty string, the existing record remains unchanged. Here's why and how to fix it:
FirstOrCreate
Behavior:
user_id
and preference_key
), it returns the existing record without updating.Assign
method does nothing in this case because FirstOrCreate
doesn't perform updates.value
.Empty String Case:
value = ""
and a record exists, the function incorrectly assumes an update occurred, but the existing record's preference_value
remains unchanged.Use Clauses(clause.OnConflict{...})
with Create()
to perform an "upsert" (update or insert). This handles both cases:
preference_value
(even if it's an empty string).Explicit Update Logic:
OnConflict
with DoUpdates
ensures existing records are updated.Assignments
explicitly updates preference_value
(works for empty strings).Database Timestamps:
updated_at
trigger (with ON UPDATE CURRENT_TIMESTAMP
) automatically updates the timestamp on conflict. No manual timestamp handling needed.Efficiency:
value
is explicitly set in the update clause (clause.Assignments
), so existing records are updated to ""
when needed.value
as-is (including ""
).idx_user_key
unique index ensures conflict detection works.