[11.x] Add upsertUsing functionality
Overview
This branch is based on an idea in the discussions - https://github.com/laravel/framework/discussions/46589
It adds an upsertUsing method which is similar to the insertUsing method and allows developers to upsert data using a subquery.
I've implemented it only for MySql for now.
Benefits
Really it's an extension of insertUsing bringing the power of upserts. The alternative currently would be to either write a raw statement or to load records into memory and use the upsert method to enter them into the new table. Obviously, keeping it in MySQL is much faster and this method makes that easier to do for devs.
My use case is syncing data from a staging table to the live table.
Breaking Changes
None! It's a new method and doesn't change any existing functionality!
Notes
One thing to note - I don't use the laravel_upsert_alias because it's difficult to implement with a subquery. I could nest the subquery like so:
insert into `table1` (`foo`) select * from (select `bar` from `table2` where `foreign_id` = ?) as laravel_upsert_alias on duplicate key update `foo` = `laravel_upsert_alias`.`foo`
However, this would result in an error since the column foo doesn't exist in the subquery. If anyone has a good solution for this let me know! Although I'm not sure the alias would provide any benefit in this scenario anyway.
I guess I'm not fundmentally opposed to this but would need to work for other databases as well I think.
That makes sense and I thought that might be the case! I'll work on adding it for the other databases that support upserts.
@taylorotwell So SQLite warns about parsing ambiguity of on when doing an upsert using a subquery (the docs) but in my testing this is only an issue when nothing comes after a join in the subquery or that join has no on clause.
So this produces an error:
User::upsertUsing([
'name',
'email',
'password'
], function ($query) {
$query->selectRaw("'Test McTesterson' as name, email, password")
->from('users')
// notice no join conditions
->join('posts', fn ($join) => $join);
}, ['email'], [
'name'
]);
But this does not:
User::upsertUsing([
'name',
'email',
'password'
], function ($query) {
$query->selectRaw("'Test McTesterson' as name, email, password")
->from('users')
// notice no join conditions
->join('posts', fn ($join) => $join)
// but something comes between the join and the "on conflict" clause
->orderBy('users.id', 'desc');
}, ['email'], [
'name'
]);
This also does not produce errors:
User::upsertUsing([
'name',
'email',
'password'
], function ($query) {
$query->selectRaw("'Test McTesterson' as name, email, password")
->from('users')
// notice there are join conditions
->join('posts', 'users.id', 'posts.user_id');
}, ['email'], [
'name'
]);
So it seems that running into this issue would be quite rare and I'm not sure it should be the responsibility of the ORM to protect the developer from making this mistake. Not to mention that trying to automatically append a where clause to the subquery if there isn't one would be added complexity especially if just for SQLite. Thoughts? Do you think I should try to account for rare cases of parsing ambiguity or keep the straightforward approach?
Thanks for your pull request to Laravel!
Unfortunately, I'm going to delay merging this code for now. To preserve our ability to adequately maintain the framework, we need to be very careful regarding the amount of code we include.
If applicable, please consider releasing your code as a package so that the community can still take advantage of your contributions!