I devloppe a batch with juste main method in legacy project with : java 7, hibernate and Spring using mysql database.
In this batch I want to update a several lines in a table that have more than 50 millions lines.
When I start the batch each day I have to update at least 10000 lines.
So, what is the best way to update the line without lock the table in mysql ?
Juste do one query like this :
update table items set is_achive = true where id in (id1,id2,id3....id10000)
Or use a for loop like this :
for(item p : ItemsList){
update table item set is_achive = true where id = p.id
}
This depends how you determine the list of rows that need updating. If you query the database to determine the list, it's probably best just to use a DML statement like:
UPDATE Item i SET i.achive = true WHERE ...
If your concern is locking i.e. the amount of time rows are locked, you can use batching by using a cursor e.g. some id of the data source.
SELECT id FROM ... WHERE id >= :start AND ...
ORDER BY id
OFFSET 100 -- use a batch size that suites your needs
LIMIT 1 -- use a batch size that suites your needs
The limit and for update can be implemented by using a query
Integer end = entityManager.createQuery("SELECT id FROM ... WHERE id >= :start AND ... ORDER BY id")
.setParameter("start", previousEnd)
.setFirstResult(100) // batch size
.setMaxResults(1)
.getResultList().stream().findFirst().orElse(null);
Then do a query like this
UPDATE Item i SET i.achive = true WHERE i.id BETWEEN :start AND :end
or if the end is null i.e. the last batch use
UPDATE Item i SET i.achive = true WHERE i.id >= :start
Use Hibernate Criteria builder:
CriteriaBuilder cb = this.em.getCriteriaBuilder();
// create update
CriteriaUpdate<Order> update = cb.createCriteriaUpdate(Order.class);
// set the root class
Root e = update.from(Order.class);
// set update and where clause
update.set("amount", newAmount);
update.where(cb.greaterThanOrEqualTo(e.get("amount"), oldAmount));
// perform update
this.em.createQuery(update).executeUpdate();
https://thorben-janssen.com/criteria-updatedelete-easy-way-to/
It is best to try to get to the root if it like Chris B suggested, but if it's something you can't do, then you might also consider leveraging Spring JDBC Batch Update Operations as documented here. These have existed for some time, so find whichever documentation is appropriate for the version you're using.
https://docs.spring.io/spring-framework/docs/3.0.0.M4/reference/html/ch12s04.html
Related
I'm trying to set a Lock for the row I'm working on until the next commit:
entityManager.createQuery("SELECT value from Table where id=:id")
.setParameter("id", "123")
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.setHint("javax.persistence.lock.timeout", 10000)
.getSingleResult();
What I thought should happen is that if two threads will try to write to the db at the same time, one thread will reach the update operation before the other, the second thread should wait 10 seconds and then throw PessimisticLockException.
But instead the thread hangs until the other thread finishes, regardless of the timeout set.
Look at this example :
database.createTransaction(transaction -> {
// Execute the first request to the db, and lock the table
requestAndLock(transaction);
// open another transaction, and execute the second request in
// a different transaction
database.createTransaction(secondTransaction -> {
requestAndLock(secondTransaction);
});
transaction.commit();
});
I expected that in the second request the transaction will wait until the timeout set and then throw the PessimisticLockException, but instead it deadlocks forever.
Hibernate generates my request to the db this way :
SELECT value from Table where id=123 FOR UPDATE
In this answer I saw that Postgres allows only SELECT FOR UPDATE NO WAIT that sets the timeout to 0, but it isn't possible to set a timeout in that way.
Is there any other way that I can use with Hibernate / JPA?
Maybe this way is somehow recommended?
Hibernate supports a bunch of query hints. The one you're using sets the timeout for the query, not for the pessimistic lock. The query and the lock are independent of each other, and you need to use the hint shown below.
But before you do that, please be aware, that Hibernate doesn't handle the timeout itself. It only sends it to the database and it depends on the database, if and how it applies it.
To set a timeout for the pessimistic lock, you need to use the javax.persistence.lock.timeout hint instead. Here's an example:
entityManager.createQuery("SELECT value from Table where id=:id")
.setParameter("id", "123")
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.setHint("javax.persistence.lock.timeout", 10000)
.getSingleResult();
I think you could try
SET LOCAL lock_timeout = '10s';
SELECT ....;
I doubt Hibernate supports this out-of-box. You could try find a way to extend it, not sure if it worth it. Because I guess using locks on a postges database (which is mvcc) is not the smartest option.
You could also do NO WAIT and delay-retry several times from your code.
There is the lock_timeout parameter that does exactly what you want.
You can set it in postgresql.conf or with ALTER ROLE or ALTER DATABASE per user or per database.
The hint for lock timeout for PostgresSQL doesn't work on PostreSQL 9.6 (.setHint("javax.persistence.lock.timeout", 10000)
The only solution I found is uncommenting lock_timeout property in postgresql.conf:
lock_timeout = 10000 # in milliseconds, 0 is disabled
For anyone who's still looking for a data jpa solution, this is how i managed to do it
First i've created a function in postgres
CREATE function function_name (some_var bigint)
RETURNS TABLE (id BIGINT, counter bigint, organisation_id bigint) -- here you list all the columns you want to be returned in the select statement
LANGUAGE plpgsql
AS
$$
BEGIN
SET LOCAL lock_timeout = '5s';
return query SELECT * from some_table where some_table.id = some_var FOR UPDATE;
END;
$$;
then in the repository interface i've created a native query that calls the function. This will apply the lock timeout on that particular transaction
#Transactional
#Query(value = """
select * from function_name(:id);
""", nativeQuery = true)
Optional<SomeTableEntity> findById(Long id);
I have an update/insert SQL query that I created using a MERGE statement. Using either JdbcTemplate or NamedParameterJdbcTemplate, does Spring provide a method that I can use to update a single record, as opposed to a Batch Update?
Since this query will be used to persist data from a queue via a JMS listener, I'm only dequeuing one record at a time, and don't have need for the overhead of a batch update.
If a batch is the only way to do it through Spring JDBC, that's fine... I just want to make certain I'm not missing something simpler.
You can use a SQL MERGE statment using only a one row query containing your parameters.
For example if you have a table COMPANYcontaing IDas a key and NAMEas an attribute, the MERGE statement would be:
merge into company c
using (select ? id, ? name from dual) d
on (c.id = d.id)
when matched then update
set c.name = d.name
when not matched then insert (c.id, c.name)
values(d.id, d.name)
If your target table contains the parametrised key, the name will be updated, otherwise a new record will be inserted.
With JDBCTemplate you use the update method to call the MERGEstatement, as illustrated below (using Groovy script)
def id = 1
def name = 'NewName'
String mergeStmt = """merge into company c
using (select ? id, ? name from dual) d
on (c.id = d.id)
when matched then update
set c.name = d.name
when not matched then insert (c.id, c.name)
values(d.id, d.name)""";
def updCnt = jdbcTemplate.update(mergeStmt, id, name);
println "merging ${id}, name ${name}, merged rows ${updCnt}"
Just use one of update methods, for example this one: JdbcTemplate#update instead of BatchUpdate.
Update updates a single record, batchUpdate updates multiple records using JDBC batch
order item table
order_item_id
order_id
quantity
unit_price
shipping_price
business_id
workflow_id
delivery_id
item_id
Orders table
billing_address_id
shipping_address_id
payment_mode
total_price
shipping_price
customer_id
UPDATE `order_items` t1 INNER JOIN Orders t2 ON t2.order_id = t1.order_id SET t1.workflow_id = ? WHERE t1.order_item_id = ? and t2.order_id = ? and t2.customer_id = ? and t1.delivery_id = ?
UPDATE `order_items` t1 SET t1.workflow_id = ?
WHERE t1.order_item_id = ? and t1.business_id = ? and t1.delivery_id = ?
UPDATE `order_items` t1 INNER JOIN Orders t2 ON t2.order_id = t1.order_id SET t1.workflow_id = ? WHERE t1.order_item_id = ? and t2.order_id = ? and t1.delivery_id = ?"
These queries are fired on different scenarios from my java rest service. (at any point of time, only one query will be used).
Previously I didn't use the inner join in my update sql and it worked well.
Now after I modified the query, it throws the following exception and the query is stuck and doesn't return for a minute.
java.sql.SQLException: Lock wait timeout exceeded; try restarting transaction
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:996)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3887)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3823)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2435)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2582)
UPDATE
This was happening because we forgot to set the autocommit mode to true again in the finally block. After which we didnt see this error.
Point1: You should not update query with join in application instead get primary key and then update the table based on primary key.
Point2: Show your tables structure with index you can get details by "show create table mytable" command, so that it can be checked that your update query is optimized or not.
Point3: If still you want to update based on join due to any specific reason and also your query is optimized then need to change your wait_timeout variable. So check what value is set in this variable on your server. you can check by below command-
SHOW GLOBAL VARIABLES LIKE 'wait_timeout';
A good thing before running UPDATE query is to run same SELECT. I.e.
SELECT * FROM `order_items` t1 INNER JOIN Orders t2 ON t2.order_id = t1.order_id SET t1.workflow_id = ? WHERE t1.order_item_id = ? and t2.order_id = ? and t2.customer_id = ? and t1.delivery_id = ?
Just to make sure, you are updating the right row.
You can also run EXPLAIN on that query to find out how complicated it is for your DB.
I recently encountered the following problem with buiding queries in jooq (version 3.1.0):
I want to build delete statement with order and limit constraints. So, my aim is to build something like this:
DELETE FROM table ORDER BY field DESC LIMIT 1 (this is MySql syntax)
But i haven't found nesessary methods in result delete query object:
DSLContext context = createContext();
DeleteWhereStep delete = context.delete(createTable(table));
DeleteConditionStep whereStep = delete.where(condition);
whereStep.orderBy(...)//and no such method here
There are all nesessary methods in select statements and none for delete.
Is it possible to set order and limit for delete request in jooq?
As of jOOQ 3.2, these sorts of extensions are currently not implemented yet. Chances are, that #203 could be implemented in jOOQ 3.3, though.
In the mean time, you have two options:
Resort to plain SQL
i.e. write something like:
context.execute("DELETE FROM {0} ORDER BY {1} DESC LIMIT 1",
createTable(table),
field);
Manually transform your SQL statement into something equivalent
I suspect that the ORDER BY .. LIMIT extension to the MySQL DELETE statement is just sugar for:
DELETE FROM table t
WHERE t.id IN (
SELECT id FROM table
ORDER BY field LIMIT 1
)
Or with jOOQ:
context.delete(TABLE)
.where(TABLE.ID.in(
select(TABLE.ID)
.from(TABLE)
.orderBy(TABLE.FIELD)
.limit(1)
))
I hope it is the appropriate section, I have a problem with this code
Transaction transaction = session.beginTransaction();
Query query = session.createQuery("update database set floop= :ctrl1" +" where ctrl= :ctrl2 ").setMaxResults(2);
query.setMaxResults(2);
query.setParameter("ctrl1",3);
query.setParameter("ctrl2", 5);
I ask through setMaxResults(2) to do the update only on the first two and he makes the update of all records as I do what is wrong?? thanks for any help
I thought to use session.createSQLQuery, but I do not know how to do.
This answer is posting delay but it can be helpful for others user who is looking update number of rows in DB with limit using HQL
Unfortunatly setMaxResults() do not work update and delete hibernate
query. It works only select criteria.
As per HQL there is no specific solution is available and you want to update rows with number of limit then follow below steps
Write a HQL to select all rows with condition or range with
setMaxResults. It will return you a List object with limit.
Then update the specific property (Property you want to update) and
store Objects of these rows again by session.update() method.
I'm assuming tablename with map Database class and there are two variable ctrl and floop with getter and setter(as per your question)
List<Database> list = new ArrayList<>();
Transaction transaction = session.beginTransaction();
//Fetching record with limit 2 using setMaxResults()
int setlimit = 2;
String hql_query = "from Database where ctrl = :ctrl2";
Query select_query = session.createQuery(hql_query).setMaxResults(setlimit);
select_query.setParameter("ctrl2", 5);
list = select_query.list();
//iterating list and setting new value to particuler column or property
int result;
if (list != null) {
for (Database element : list) {
element.setFloop(ctrl1);
//Here is updating data in database
session.update(element);
}
result = list.size();
} else {
result = 0;
}
System.out.println("Rows affected: " + result);
transaction.commit();
setMaxResults limits the number of results which are returned by the query, not the number of affected rows.
When you only want to update a limited set of rows, you should specify these rows within the where condition. Setting a hard limit on the number of updated rows wouldn't make much sense, because there would be no way to tell which rows would be updated.
query.setMaxResults(2); will be used for selection queries and will be ignored for insertion/updation. If you use it for selection queries, then you will get 2 records in result.
setMaxResults only applies to select. For your problem I would perform a select query and then use the query.setMaxResults(2), this will return a list of max 2 elements. Then loop the list returned and use session.update for the one or two elements returned.
I can see a number of perfectly valid use-cases where you want to update only a limited number of rows and as other have already answered, the Hibernate Query cannot deal with this so you need to resort to native SQL.
You don't specify in the question which type of database you are using so this answer will only apply to MySql:
Transaction transaction = session.beginTransaction();
Query query = session.createSQLQuery("UPDATE database SET floop= :ctrl1 WHERE ctrl= :ctrl2 LIMIT :max");
query.setParameter("ctrl1",3);
query.setParameter("ctrl2", 5);
query.setParameter("max", 2);
Please note that the sql query above needs to use the native table and column names and not the ones in your ORM model.