-
Notifications
You must be signed in to change notification settings - Fork 694
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
Support upsert functionality #167
Comments
Should be fixed in pair with #186 |
Workaround is to implement this locally with help of "ON DUPLICATE KEY UPDATE": class BatchInsertUpdateOnDuplicate(table: Table, val onDupUpdate: List<Column<*>>): BatchInsertStatement(table, false) {
override fun prepareSQL(transaction: Transaction): String {
val onUpdateSQL = if(onDupUpdate.isNotEmpty()) {
" ON DUPLICATE KEY UPDATE " + onDupUpdate.joinToString { "${transaction.identity(it)}=VALUES(${transaction.identity(it)})" }
} else ""
return super.prepareSQL(transaction) + onUpdateSQL
}
}
fun <T: Table, E> T.batchInsertOnDuplicateKeyUpdate(data: List<E>, onDupUpdateColumns: List<Column<*>>, body: T.(BatchInsertUpdateOnDuplicate, E) -> Unit): List<Int> {
return data.takeIf { it.isNotEmpty() }?.let {
val insert = BatchInsertUpdateOnDuplicate(this, onDupUpdateColumns)
data.forEach {
insert.addBatch()
body(insert, it)
}
TransactionManager.current().exec(insert)
columns.firstOrNull { it.columnType.isAutoInc }?.let { idCol ->
insert.generatedKey?.mapNotNull {
val value = it[idCol]
when (value) {
is Long -> value.toInt()
is Int -> value
null -> null
else -> error("can't find primary key of type Int or Long; map['$idCol']='$value' (where map='$it')")
}
}
}
}.orEmpty()
}
// Usage sample
FooTable.batchInsertOnDuplicateKeyUpdate(listOf(fooObject), listOf(FooTable.barField)) { batch, foo ->
batch[FooTable.id] = foo.id
batch[FooTable.barField] = foo.bar
} |
Here is another method that seems to work for PostgreSQL's upsert: fun <T : Table> T.insertOrUpdate(key: Column<*>, body: T.(InsertStatement<Number>) -> Unit) =
InsertOrUpdate<Number>(key, this).apply {
body(this)
execute(TransactionManager.current())
}
class InsertOrUpdate<Key : Any>(private val key: Column<*>,
table: Table,
isIgnore: Boolean = false) : InsertStatement<Key>(table, isIgnore) {
override fun prepareSQL(transaction: Transaction): String {
val updateSetter = table.columns.joinToString { "${it.name} = EXCLUDED.${it.name}" }
val onConflict = "ON CONFLICT (${key.name}) DO UPDATE SET $updateSetter"
return "${super.prepareSQL(transaction)} $onConflict"
}
}
|
That upsert method didn't work for me. Instead of "insert or update" it would "insert or replace", meaning that unrelated columns got changed. The following fix seemed to work (the difference is that we only alter keys that are in the transaction):
|
This is for a single insert on update rather than a batch - tested on MySQL. fun <T : Table> T.insertOrUpdate(vararg onDuplicateUpdateKeys: Column<*>, body: T.(InsertStatement<Number>) -> Unit) =
InsertOrUpdate<Number>(onDuplicateUpdateKeys,this).apply {
body(this)
execute(TransactionManager.current())
}
class InsertOrUpdate<Key : Any>(
private val onDuplicateUpdateKeys: Array< out Column<*>>,
table: Table,
isIgnore: Boolean = false
) : InsertStatement<Key>(table, isIgnore) {
override fun prepareSQL(transaction: Transaction): String {
val onUpdateSQL = if(onDuplicateUpdateKeys.isNotEmpty()) {
" ON DUPLICATE KEY UPDATE " + onDuplicateUpdateKeys.joinToString { "${transaction.identity(it)}=VALUES(${transaction.identity(it)})" }
} else ""
return super.prepareSQL(transaction) + onUpdateSQL
}
}
// Example:
// CustomerTable.insertOrUpdate(
// CustomerTable.favouriteColour
// ) {
// it[id] = customer.id
// it[favouriteColour] = customerFavouriteColour
// } |
I made something similar to @carolosf 's code, however, it takes an index or column that is used as a constraint. Example usage:
Do not call Code:
|
Hey, is there any plans to support upsert out of the box in Exposed? |
I modified @AllanWang's code to quote column names via fun <T : Table> T.insertOrUpdate(vararg keys: Column<*>, body: T.(InsertStatement<Number>) -> Unit) =
InsertOrUpdate<Number>(this, keys = *keys).apply {
body(this)
execute(TransactionManager.current())
}
class InsertOrUpdate<Key : Any>(
table: Table,
isIgnore: Boolean = false,
private vararg val keys: Column<*>
) : InsertStatement<Key>(table, isIgnore) {
override fun prepareSQL(transaction: Transaction): String {
val tm = TransactionManager.current()
val updateSetter = table.columns.joinToString { "${tm.identity(it)} = EXCLUDED.${tm.identity(it)}" }
val onConflict = "ON CONFLICT (${keys.joinToString { tm.identity(it) }}) DO UPDATE SET $updateSetter"
return "${super.prepareSQL(transaction)} $onConflict"
}
} Would love to see official support in Exposed, though. |
Used solution from @Dico200 , but had to modify it to work with index (only tested on Postgres). import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Index
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.transactions.TransactionManager
class UpsertStatement<Key : Any>(table: Table, conflictColumn: Column<*>? = null, conflictIndex: Index? = null) :
InsertStatement<Key>(table, false) {
private val indexName: String
private val indexColumns: List<Column<*>>
private val index: Boolean
init {
when {
conflictIndex != null -> {
index = true
indexName = conflictIndex.indexName
indexColumns = conflictIndex.columns
}
conflictColumn != null -> {
index = false
indexName = conflictColumn.name
indexColumns = listOf(conflictColumn)
}
else -> throw IllegalArgumentException()
}
}
override fun prepareSQL(transaction: Transaction) = buildString {
append(super.prepareSQL(transaction))
val dialect = transaction.db.vendor
if (dialect == "postgresql") {
if (index) {
append(" ON CONFLICT ON CONSTRAINT ")
append(indexName)
} else {
append(" ON CONFLICT(")
append(indexName)
append(")")
}
append(" DO UPDATE SET ")
values.keys.filter { it !in indexColumns }
.joinTo(this) { "${transaction.identity(it)}=EXCLUDED.${transaction.identity(it)}" }
} else {
append(" ON DUPLICATE KEY UPDATE ")
values.keys.filter { it !in indexColumns }
.joinTo(this) { "${transaction.identity(it)}=VALUES(${transaction.identity(it)})" }
}
}
}
inline fun <T : Table> T.upsert(
conflictColumn: Column<*>? = null,
conflictIndex: Index? = null,
body: T.(UpsertStatement<Number>) -> Unit
) =
UpsertStatement<Number>(this, conflictColumn, conflictIndex).apply {
body(this)
execute(TransactionManager.current())
}
fun Table.indexR(customIndexName: String? = null, isUnique: Boolean = false, vararg columns: Column<*>): Index {
val index = Index(columns.toList(), isUnique, customIndexName)
indices.add(index)
return index
}
fun Table.uniqueIndexR(customIndexName: String? = null, vararg columns: Column<*>): Index =
indexR(customIndexName, true, *columns) |
+1 for official support |
+1 |
Had to provide different values in insert and update blocks so I wrote my own implementation based on @carolosf version. Thought it might be useful. Tested with MySQL and MariaDB. /**
* Insert or update a-ka upsert implementation
*
* Sample usage:
*
* class SampleTable: IntIdTable("whatever"){
* val identifier = varchar("identifier", 32").uniqueIndex()
* val value = varchar("value", 32)
* }
*
* transaction {
* SampleTable.insertOrUpdate({
* it[SampleTable.identifier] = "some identifier"
* it[SampleTable.value] = "inserted"
* }){
* it[SampleTable.value] = "updated"
* }
* }
*
* Which is equivalent of:
*
* INSERT INTO whatever(identifier, value) VALUES('some identifier', 'inserted')
* ON DUPLICATE KEY UPDATE value = 'updated'
*/
fun <T : Table> T.insertOrUpdate(insert: T.(InsertStatement<Number>) -> Unit, update: T.(UpdateBuilder<Int>) -> Unit) {
val updateStatement = UpsertUpdateBuilder(this).apply { update(this) }
InsertOrUpdate<Number>(updateStatement,this).apply {
insert(this)
execute(TransactionManager.current())
}
}
private class UpsertUpdateBuilder(table: Table) : UpdateBuilder<Int>(StatementType.OTHER, listOf(table)){
val firstDataSet: List<Pair<Column<*>, Any?>> get() = values.toList()
override fun arguments(): List<List<Pair<IColumnType, Any?>>> = QueryBuilder(true).run {
values.forEach {
registerArgument(it.key, it.value)
}
if (args.isNotEmpty()) listOf(args) else emptyList()
}
override fun prepareSQL(transaction: Transaction): String {
throw IllegalStateException("prepareSQL in UpsertUpdateBuilder is not supposed to be used")
}
override fun PreparedStatementApi.executeInternal(transaction: Transaction): Int {
throw IllegalStateException("executeInternal in UpsertUpdateBuilder is not supposed to be used")
}
}
private class InsertOrUpdate<Key : Any>(
val update: UpsertUpdateBuilder,
table: Table,
isIgnore: Boolean = false
) : InsertStatement<Key>(table, isIgnore) {
override fun arguments(): List<List<Pair<IColumnType, Any?>>> {
val updateArgs = update.arguments()
return super.arguments().mapIndexed { index, list -> list + (updateArgs.getOrNull(index) ?: return@mapIndexed list) }
}
override fun prepareSQL(transaction: Transaction): String {
val values = update.firstDataSet
if(values.isEmpty())
return super.prepareSQL(transaction)
val originalStatement = super.prepareSQL(transaction)
val updateStm = with(QueryBuilder(true)){
values.appendTo(this) { (col, value) ->
append("${transaction.identity(col)}=")
registerArgument(col, value)
}
toString()
}
return "$originalStatement ON DUPLICATE KEY UPDATE $updateStm"
}
} |
I'd love to see this functionality natively in Exposed, too. |
Unfortunately this does not work with PostgreSQL as it uses a different syntax ('ON CONFLICT'). Would love to see this covered by Exposed directly, also to keep your app code more independent. |
Here's my implementation of upsert and batch upsert. I tried to take the best of everything above. I use this w/ PostgreSQL in production and has been tested against MySQL.
And you'll need this helper:
Rocket science. |
In September we can celebrate 4 years since this issue was open and still hasn't been resolved. |
When updating a column by calculating a new value from an old value, "balance" will always be the default value |
Kinda sad to see such an issue unresolved for 4 years by the framework that pretends to be an alternative for some other popular solutions :( |
For those using the Postgres - a year or so ago I created super simple library based on the comments here -https://github.com/LukasForst/exposed-upsert no need to copy paste code, the library is now available on the Maven Central |
@jnfeinstein - any plans to make your solution into a library or create a PR to exposed? Would you mind attaching a license to that code so I can borrow it? Thanks! |
If anyone still interested in a library based solution, here is the implementation that covers all dialects that support queries with upsert functionality: https://github.com/dzikoysk/exposed-upsert
The motivation behind this implementation is that nobody at this moment covered both MySQL and PostgreSQL based solutions at once with expressions support and separated insert and update body. The LukasForst's library supports only PostgreSQL and the issue related to MySQL just didn't receive any feedback. I hope it'll help someone who needs this feature. |
Getting this officially supported would be great! |
Please check the |
Works as expected for at least PostgreSQL. But it would be nice if the columns could be specified, not just the primary key. |
I created an upsert for the DAO interface: // Upsert.kt
import org.jetbrains.exposed.dao.UUIDEntity
import org.jetbrains.exposed.dao.UUIDEntityClass
import org.jetbrains.exposed.sql.Column
fun <T : UUIDEntity, A : Any> UUIDEntityClass<T>.upsert(column: Column<A>, value: A, init: T.() -> Unit): T {
val existing = this.findOneByCol(column, value)
if (existing == null) {
return this.new(init)
} else {
existing.apply(init)
return existing
}
} // FindOneByCol.kt
import org.jetbrains.exposed.dao.UUIDEntity
import org.jetbrains.exposed.dao.UUIDEntityClass
import org.jetbrains.exposed.sql.*
fun <T : UUIDEntity, A : Any> UUIDEntityClass<T>.findOneByCol(column: Column<A>, value: A): T? {
return this.find { column eq value }.firstOrNull()
} Example: object InvoicesTable : UUIDTable("invoices") {
val vendor = text("vendor")
val date = date("date")
val number = text("number").uniqueIndex()
override val primaryKey = PrimaryKey(id)
}
class Invoice(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<Invoice>(InvoicesTable)
var vendor by InvoicesTable.vendor
var date by InvoicesTable.date
var number by InvoicesTable.number
}
Invoice.upsert(InvoicesTable.number, "123") {
vendor = "Fake Vendor"
date = LocalDate.of(2021, 1, 1)
number = "ABC123"
}.id It probably needs adaptation to work for anything other than |
@shcallaway Nice simple workaround, I don't care much about performance and your solution is simple enough, which leave me away from things like "ON CONFLICT DO UPDATE SET" or "ON DUPLICATE KEY UPDATE". But in my case the duplicated key itself won't change its value, so I modified your solution: fun <T : IntEntity, A : Any?> IntEntityClass<T>.upsert(column: Column<A>, kProperty: KMutableProperty1<T, A>, value: A, init: T.() -> Unit): T {
val dao = this.find { column eq value }.firstOrNull()
return dao?.apply(init) ?: this.new {
kProperty.set(this, value)
init.invoke(this)
}
} Example (assume InvoicesTable is an IntIdTable): Invoice.upsert(InvoicesTable.number, Invoice::number, "123") {
vendor = "Fake Vendor"
date = LocalDate.of(2021, 1, 1)
//number = "ABC123" // this also works as original
} |
I made a small adjustment to @Maxr1998 solution to reduce the amount of NB!: This solution works only on Postgres import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.statements.BatchInsertStatement
import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.transactions.TransactionManager
/**
* Example:
* val item = ...
* MyTable.upsert {
* it[id] = item.id
* it[value1] = item.value1
* }
*/
fun <T : Table> T.upsert(
vararg keys: Column<*> = (primaryKey ?: throw IllegalArgumentException("primary key is missing")).columns,
body: T.(InsertStatement<Number>) -> Unit
) =
InsertOrUpdate<Number>(this, keys = keys).apply {
body(this)
execute(TransactionManager.current())
}
class InsertOrUpdate<Key : Any>(
table: Table,
isIgnore: Boolean = false,
private vararg val keys: Column<*>
) : InsertStatement<Key>(table, isIgnore) {
override fun prepareSQL(transaction: Transaction): String {
val tm = TransactionManager.current()
val updateSetter = (table.columns - keys).joinToString { "${tm.identity(it)} = EXCLUDED.${tm.identity(it)}" }
val onConflict = "ON CONFLICT (${keys.joinToString { tm.identity(it) }}) DO UPDATE SET $updateSetter"
return "${super.prepareSQL(transaction)} $onConflict"
}
}
/**
* Example:
* val items = listOf(...)
* MyTable.batchUpsert(items) { table, item ->
* table[id] = item.id
* table[value1] = item.value1
* }
*/
fun <T : Table, E> T.batchUpsert(
data: Collection<E>,
vararg keys: Column<*> = (primaryKey ?: throw IllegalArgumentException("primary key is missing")).columns,
body: T.(BatchInsertStatement, E) -> Unit
) =
BatchInsertOrUpdate(this, keys = keys).apply {
data.forEach {
addBatch()
body(this, it)
}
execute(TransactionManager.current())
}
class BatchInsertOrUpdate(
table: Table,
isIgnore: Boolean = false,
private vararg val keys: Column<*>
) : BatchInsertStatement(table, isIgnore) {
override fun prepareSQL(transaction: Transaction): String {
val tm = TransactionManager.current()
val updateSetter = (table.columns - keys).joinToString { "${tm.identity(it)} = EXCLUDED.${tm.identity(it)}" }
val onConflict = "ON CONFLICT (${keys.joinToString { tm.identity(it) }}) DO UPDATE SET $updateSetter"
return "${super.prepareSQL(transaction)} $onConflict"
}
} |
Next small adjustment.
So with import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.SqlExpressionBuilder
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.statements.BatchInsertStatement
import org.jetbrains.exposed.sql.statements.InsertStatement
import org.jetbrains.exposed.sql.transactions.TransactionManager
/**
* Example:
* ```
* val item = ...
* MyTable.upsert {
* it[id] = item.id
* it[value1] = item.value1
* }
*```
*/
fun <T : Table> T.upsert(
where: (SqlExpressionBuilder.() -> Op<Boolean>)? = null,
vararg keys: Column<*> = (primaryKey ?: throw IllegalArgumentException("primary key is missing")).columns,
body: T.(InsertStatement<Number>) -> Unit
) = InsertOrUpdate<Number>(this, keys = keys, where = where?.let { SqlExpressionBuilder.it() }).apply {
body(this)
execute(TransactionManager.current())
}
class InsertOrUpdate<Key : Any>(
table: Table,
isIgnore: Boolean = false,
private val where: Op<Boolean>? = null,
private vararg val keys: Column<*>
) : InsertStatement<Key>(table, isIgnore) {
override fun prepareSQL(transaction: Transaction): String {
val onConflict = buildOnConflict(table, transaction, where, keys = keys)
return "${super.prepareSQL(transaction)} $onConflict"
}
}
/**
* Example:
* ```
* val items = listOf(...)
* MyTable.batchUpsert(items) { table, item ->
* table[id] = item.id
* table[value1] = item.value1
* }
* ```
*/
fun <T : Table, E> T.batchUpsert(
data: Collection<E>,
where: (SqlExpressionBuilder.() -> Op<Boolean>)? = null,
vararg keys: Column<*> = (primaryKey ?: throw IllegalArgumentException("primary key is missing")).columns,
body: T.(BatchInsertStatement, E) -> Unit
) = BatchInsertOrUpdate(this, keys = keys, where = where?.let { SqlExpressionBuilder.it() }).apply {
data.forEach {
addBatch()
body(this, it)
}
execute(TransactionManager.current())
}
class BatchInsertOrUpdate(
table: Table,
isIgnore: Boolean = false,
private val where: Op<Boolean>? = null,
private vararg val keys: Column<*>
) : BatchInsertStatement(table, isIgnore) {
override fun prepareSQL(transaction: Transaction): String {
val onConflict = buildOnConflict(table, transaction, where, keys = keys)
return "${super.prepareSQL(transaction)} $onConflict"
}
}
fun buildOnConflict(
table: Table,
transaction: Transaction,
where: Op<Boolean>? = null,
vararg keys: Column<*>
): String {
var updateSetter = (table.columns - keys).joinToString(", ") {
"${transaction.identity(it)} = EXCLUDED.${transaction.identity(it)}"
}
where?.let {
updateSetter += " WHERE $it"
}
return "ON CONFLICT (${keys.joinToString { transaction.identity(it) }}) DO UPDATE SET $updateSetter"
} |
Yet another small adjustment, if you are batch upserting a lot of rows, set class BatchInsertOrUpdate(
table: Table,
isIgnore: Boolean = false,
private vararg val keys: Column<*>
) : BatchInsertStatement(table, isIgnore, shouldReturnGeneratedValues = false) {
override fun prepareSQL(transaction: Transaction): String {
val tm = TransactionManager.current()
val updateSetter = (table.columns - keys).joinToString { "${tm.identity(it)} = EXCLUDED.${tm.identity(it)}" }
val onConflict = "ON CONFLICT (${keys.joinToString { tm.identity(it) }}) DO UPDATE SET $updateSetter"
return "${super.prepareSQL(transaction)} $onConflict"
}
} |
Hello together, this issue is full of solutions. Which one is the best for a quick hack? Maybe it makes sense to add the best solution to the FAQ. |
incredibly looking forward to have this feature in lib |
@hennihaus this is what I use on my project and it works well enough for my needs: https://github.com/LorittaBot/Loritta/blob/5061942ab13809f882b5eeacfb14c6543a0856f3/pudding/client/src/main/kotlin/net/perfectdreams/loritta/cinnamon/pudding/utils/exposed/BatchUpsert.kt I've also published the |
Thats by far the best solution:) Thanks a lot |
Hello together, I have implemented the batchUpsert like shown in @red-avtovo and @MrPowerGamerBR solutions. While looking into the logs of the JDBC PostgreSQL driver I can see that there are multiple batch statements and not one as expected. I already set With best regards |
This is how the PostgreSQL JDBC driver works, IIRC it tries splitting up the statements in batch sizes of power of 2, up to 128. |
No description provided.
The text was updated successfully, but these errors were encountered: