I have a server running Spring boot + JPA + Hibernate. I am using MySQL database (Using InnoDb engine by default).
The implementation draws inspiration from many articles I had search on Internet.
I have implemented REST API to facilitate building a website dynamically.
I wanted to log all the API requests into a log (audit log). So when the API is called,
I store the request method name and few parameters into auditlog table in MySql.
Just before I return from the API, I store the response as well by updating the same record.
I was reviewing the code logs of Hibernate when I make API requests using the web application client as well as Postman.
I noticed that for every API, it takes on an average 150ms - 200ms for inserts and updates.
This is proving to be costly for APIs which fetch very less information.
So I want to know how I can speed up the inserts so that my inserts/updates take less than 10 -20 ms.
My Auditlog entity is
#Entity
#Table(name="auditlog")
public class AuditLog{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(nullable = false, updatable = false)
#Temporal(TemporalType.TIMESTAMP)
#CreatedDate
private Date created_at;
#Column(nullable = false)
#Temporal(TemporalType.TIMESTAMP)
#LastModifiedDate
private Date updated_at;
#NotBlank
private String methodName;
private String param1;
// Text field with private information like password masked
#Column(length = 65535, columnDefinition = "text")
private String request;
// Text field
#Column(length = 65535, columnDefinition = "text")
private String response;
private Integer result;
... // getters and setters
}
My AuditLogRepository is :
public interface AuditLogRepository extends JpaRepository<AuditLog, Long>{
}
In my REST API controller I am doing the following
...
AuditLog logEntry = new AuditLog();
// set all the values except generated ones like id, created_at and updated_at
logEntry.setMethodName(...);
logEntry.setParam1(...);
logEntry.setRequest(...);
// Save into the table using autowired repoitory
auditLogRepoitory.saveAndFlush(logEntry);
// ... do the operation of the API
// Update the logEntry
logEntry.setResult(...);
logEntry.setResponse(...);
auditLogRepoitory.saveAndFlush(logEntry);
...
Please help me in improving the insert and updates to the table.
Or please help in improving the code so that I can make APIs response faster.
Thanks,
Sri Prad
First tips
if you want to speed up insert/update don't user JpaRepository.save method (notice that saveAndFlush() internally calls save method).
Because JpaRepository.save internal select the entity in order to know if the entity is new or if it exists in database.
Here is the default implementation of jpaRepository.save :
#Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (this.entityInformation.isNew(entity)) {
this.em.persist(entity);
return entity;
} else {
return this.em.merge(entity);
}
}
I think using jdbcTemplate is the best option.
Second tips
when thinking about optimizing the inserts, it is probably useful to think about doing bulk inserts. According to mysql documentation website , The time required for inserting a row is determined by the following factors, where the numbers indicate approximate proportions:
Connecting: (3)
Sending query to server: (2)
Parsing query: (2)
Inserting row: (1 × size of row)
Inserting indexes: (1 × number of indexes)
Closing: (1)
So you can easily see how bulk insert can help you improve insert speed.
Third tips
You probably need to tune your mysql instance settings as explained in this stackeroverflow anwser
Others options
Make sur you have selected the right ID generation strategy as explained here https://dzone.com/articles/spring-boot-boost-jpa-bulk-insert-performance-by-100x
If your framework allows for it, do
START TRANSACTION
at the beginning of building the page and storing the auditing. And
COMMIT
at the end.
Related
I'm trying to make a query to retrieve some data which has been created between two dates (represented as Instant).
Here below an extract from the Entity I'm using:
#Entity
public class HistoricalData {
#Id
#GeneratedValue
private Long id;
#Column
private String name;
#CreationTimestamp
private Instant timestamp;
#Column
private Double price;
}
And the query I've written to retrieve the data between the two Instants;
#Query("select h from HistoricalData h where h.timestamp between :timestampStart and :timestampEnd and upper(name) = upper(:name)")
List<HistoricalData> findHistoricalDataBetween(#NonNull Instant timestampStart, #NonNull Instant timestampEnd, #NonNull String name);
Which produces this SQL query:
select historical0_.id as id1_5_, historical0_.price as price2_5_, historical0_.timestamp as timestam3_5_ from historical_data historical0_ where (historical0_.timestamp between ? and ?) and upper(historical0_.name)=upper(?)
Also I wrote the "hibernate JPA" query just to try but no success:
List<HistoricalData> findHistoricalDataByTimestampAfterAndTimestampBeforeAndName(#NonNull Instant timestampStart, #NonNull Instant timestampEnd, #NonNull String name);
Keep in mind that all the above queries compile correctly and do not throw any exception, they just retrieve nothing from the database
The database I'm using is a latest version of MariaDB and the connector version is the 2.7.2
Also the SpringBoot version I'm using is the 2.5.3
Here is DDL from the table definition (automatically generated from Hibernate):
create table historical_data
(
id bigint not null primary key,
price double null,
timestamp datetime not null,
name varchar not null
);
An this is how the timestamp looks like in the database:
Even though records between those two Instants are present in the database I'm still getting nothing as a result from the query.
Looks like the reason is a time zone.
MySQL driver uses incorrect time zone transformations, using a default local time zone in place of a connection time zone (or vice versa).
Just debug this query inside MySQL driver to have fun and figure out what happens.
You can add parameters to the database URL to see which actual values are passed for the prepare statement
jdbc:mysql://<DATABASE_URL>?logger=com.mysql.cj.log.Slf4JLogger&profileSQL=true
I have a table called 'otp', I need to delete records that is older than a certain period, let say 5 minutes for now.
I use Spring Framework with hibernate with MySql. I tried all possible ways, writing a '#Query' in the DTO interface, trying the Spring Data query way, I even tried to do a 'Select' on the records. When running in the code it does not select any records nor does it delete any records older than the period asked for. When I copy the 'Query" statement into the MySQL workbench it do work, I only need to put the DB name in front of the table name to get it to work. Below is snippets of the code. I removed the "#Query" statement and kept the Spring Data query.
Thank you in advance for your help.
The Entity or model.
'
#Getter
#Setter
#NoArgsConstructor
#Entity
#Table(name = "otp")
public class Otp {
#Id
#Column(name="phoneNumber")
private String phoneNumber;
#Column(name="otp")
private String otp;
#Column(name="createdOn", nullable = false, updatable = false)
#CreationTimestamp
private Timestamp createdOn;
}
`
Code that must delete the rows.
'
#Repository
public interface OTPdto extends CrudRepository<Otp, String> {
#Modifying
#Transactional
void deleteByCreatedOnLessThan(Object now);
}
'
Code that build the date for the query.
'
Date now = Date.from(Instant.now());
Object param = new java.sql.Timestamp(now.getTime());
otPdto.deleteByCreatedOnLessThan(param);
'
Hard to tell by provided info, but maybe I can help.
First of all, I would make sure that CreatedOn column gets filled in with correct information. Also I would try something like deleteByCreatedOnBefore(java.sql.Timestamp time) instead of deleteByCreatedOnLessThan(Object now). java.sql.Timestamp is not raw timestamp value, so maybe comparasing "LessThan" doesn't work well with that.
I will go another route and just clear the otp field. Seems I need to keep the phone number in the Db. Will use the phone number in future to remove the record.
I have one very annoying issue, i have read all existing documentation online and read all stackoverflow questions and answers related to this topic, but simply can not make this to work!
I am really desperate and i do not know what i am missing so i will try to give you all what i have so far. Basically what i am trying to do is to save a lot of data with one query instead of multiple queries for each object. As you can suspect i am using Spring Boot, Hibernate and MySql.
So basic facts that i have learned so far based on what i read related to "batch insert using mysql + hibernate" is next:
Mysql does not support Sequence ID, so i can not use it, like i could use it for PostgreSql
Hibernate does not support batch insert out of the box, there are couple of app properties that needs to be added
And this is what i have so far:
Application properties that i added:
spring.datasource.url=jdbc:mysql://localhost:32803/db?rewriteBatchedStatements=true
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.batch_versioned_data=true
spring.jpa.properties.hibernate.id.new_generator_mappings=false
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.type=trace
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
spring.jpa.properties.hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext
#Entity
data class Person (
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
val id: Long?,
var firstName: String,
var lastName: String,
var country: String,
var org: Int
)
What i want is to save a lot of Persons at once, as you can see i added batch size 50, if i understood correctly that means i will do one database hit per 50 persons while saving. (correct me if i am wrong)
And at the end i have Repository where i execute that batch insert:
#Repository
class PersonRepositoryCustomImpl : PersonRepositoryCustom {
#PersistenceContext
private val entityManager: EntityManager? = null
#Transactional
override fun batchSave2(persons: Set<Person>) {
val session = entityManager!!.unwrap(Session::class.java)
persons.forEachIndexed { index, person ->
if ( index % 50 == 0 ) {
session!!.flush()
session.clear()
}
session!!.save(person)
}
session.close()
}
#Transactional
override fun <T : Person?> batchSave(entities: Collection<T>): Collection<T>? {
val savedEntities: MutableList<T> = ArrayList(entities.size)
var i = 0
for (t in entities) {
savedEntities.add(persistOrMerge(t))
i++
if (i % 50 == 0) { // Flush a batch of inserts and release memory.
entityManager!!.flush()
entityManager.clear()
}
}
return savedEntities
}
private fun <T : Configuration?> persistOrMerge(t: T): T {
return if (t!!.id == null) {
entityManager!!.persist(t)
t
} else {
entityManager!!.merge(t)
}
}
}
So here you can see that i have tried to make this works on 2 almost the same ways, but of course both of them seems not to work.
In order to confirm that i am actually doing batch insert i am looking at this:
https://tableplus.com/blog/2018/10/how-to-show-queries-log-in-mysql.html
so basically that should show me queries that are being executed on DB, and there i can see that for each person object i have one insert statement.
Basically that result of this query:
SELECT
*
FROM
mysql.general_log;
And there i can clearly see that i have multiple insert statements that do one query per object (person).
Edit:
https://blog.arnoldgalovics.com/configuring-a-datasource-proxy-in-spring-boot/
I have also implemented datasource proxy, which proved me that i am not doing batch insert:
Name:, Time:1, Success:True, Type:Prepared, Batch:False, QuerySize:1, BatchSize:0, Query:["insert into person(firstName, lastName, country, org) values (?, ?, ?, ?)"], Params:[(10,John,Johny,USA,ORG)]
i have multiple of records like this one.
Thanks in advance for any kind of help!
Just to give an answer in case someone needs it:
So long story short i was not able to make MySql + hibernate batch processing work, for sake of testing i actually was able to make it work with PostgreSQL.
But anyway if anyone needs this with MySql there is a way using JDBC batch processing, and code more or less is very straight forward:
private String INSERT_SQL_PARAMS = "INSERT INTO item_params(p_key, p_value, item_id) values (?,?,?)"
override fun saveParams(configParams: Set<ItemParam>) {
jdbcTemplate!!.batchUpdate(INSERT_SQL_PARAMS , configParams, 3000) { ps, argument ->
ps.setLong(1, argument.pKey)
ps.setString(2, argument.pValue)
ps.setString(3, argument.itemId)
}
}
mysql doesn't support sequences so you can generate one using this strategy in your entity class:
#Id
#GeneratedValue(generator = "voucher_sequence")
#GenericGenerator(name = "voucher_sequence",strategy = "increment")
and also in your mysql url enable batching:
jdbc:mysql://localhost:3306/db?rewriteBatchedStatements=true
and in your application.properties:
spring.jpa.properties.hibernate.jdbc.batch_size=20
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
then use saveAll() method to persist list of entities
I am having a user database table as:
CREATE TABLE IF NOT EXISTS `user` (
`user_id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`first_ name` VARCHAR(45) NOT NULL,
`active_status` ENUM('ACTIVE', 'PENDING', 'DEACTIVATED', 'BLOCKED', 'SPAM', 'DELETED') NOT NULL ,
UNIQUE INDEX `unique_id_UNIQUE` (`unique_id` ASC),
UNIQUE INDEX `email_UNIQUE` (`email` ASC),
PRIMARY KEY (`user_id`))
ENGINE = InnoDB;
I mapped it to a corresponding JPA entity class as:
#Entity
public class User implements OfloyEntity {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(name = "user_id", unique = true, nullable = false)
private int userId;
//other fields
#Enumerated(EnumType.STRING)
#Column(name = "active_status", nullable = false, length = 11)
private UserStatus activeStatus;
As you can see, I have mapped activeStatus to a enum UserStatus to restrict the entires from persistence layer itself.
public enum UserStatus {
ACTIVE,
PENDING,
DEACTIVATED,
BLOCKED,
DELETED,
SPAM
}
I want to know is there any drawback of using this approach for implementing a DB enum in persistence layer? I gone throw multiple articles which recommend using AttributeConverter but since the the values in my enum are very limited and have less chances of modification, I am unable to relate all those articles with my requirement.
Is there something I am missing, or any improvement can be done in my design?
Articles I gone throw:
vladmihalcea
thorban and some other stackoverflow questions.
Update: After reading the answer from Jens, I decided to implement AttributeConverter(for user's gender). And that confused me a little:
Why I decided to use enum as MYSQL column type : as it restrict the values and require less space. Because MYSQL stores the ordinal value of it's enum behind the scene and when asked for the value it represents the String value of that, it saves space.
My implementation of gender:
public enum UserGender {
MALE('M'),
FEMALE('F'),
OTHER('O');
private Character shortName;
private UserGender(Character shortName) {
this.shortName = shortName;
}
public Character getShortName() {
return shortName;
}
public static UserGender fromShortName(Character shortName) {
switch (shortName) {
case 'M': return UserGender.MALE;
case 'F' : return UserGender.FEMALE;
case 'O' : return UserGender.OTHER;
default:
throw new UserGenderNotSupportedException("user gender with shortName : " + shortName + " not supported");
}
}
}
converter class:
#Converter(autoApply = true)
public class UserGenderConverter implements AttributeConverter<UserGender, Character> {
#Override
public Character convertToDatabaseColumn(UserGender userGender) {
return userGender.getShortName();
}
#Override
public UserGender convertToEntityAttribute(Character dbGender) {
return UserGender.fromShortName(dbGender);
}
}
Now, the major doubts:
1. As per blogs, using MYSQL enum is evil in DB, because someday if I need to add extra values to the enum column and that would require a table ALTER, but isn't it the same case with using AttributeConverter? Because there also we use a java enum, which would need to be change if someday new genders are required?
2. If I use AttributeConverter, I would have to document java enum(UserGender here) explaination somewhere so that DBA can understand what F,M,O stands for. Am I right here?
The articles gave you a rich selection of potential drawbacks:
Using #Enumerated(EnumType.STRING) has the following:
It uses lots of space compared to other options. Note that means more data needs to be loaded and transferred over the wire this has an effect on performance as well. We have no idea if this is a problem for you and you won't know either until you made some performance tests.
Ties the name of the enum values hard to the column values. Which can be risky since developers are used to renaming stuff quickly and you would need tests with actual legacy data to catch this.
If you don't work with really huge amounts of data for which updating the column for all rows is an actual problem, I wouldn't sweat it. It's easy enough to introduce an AttributeConverter and update the data when the simple solution actually becomes a problem.
Update regarding the updated question:
I don't buy into the argument that anything is "evil" because it might require an ALTER TABLE statement. By this argument, we should abolish relational databases completely because using them requires DDL and evolution of an application will require more of it. Of course, the necessity of a DDL statement makes a deployment a little more complex. But you need to be able to handle this thing anyway.
But it is true that with an AttributeConverter you wouldn't need any DDL in this case, because you'd just put another value in the same column which doesn't have any special constraints except the maximum length of the value. This assumes you don't have a check constraint on the column to limit the legal values.
Do you have to document the relationship between Enum and the value stored in the DB? Depends on your team. Does the DBA even care about the meaning of the data? Does the DBA have access and the skills to understand the Java code? If the DBA needs or wants to know and can't or won't get the information from the source code you have to document it. True.
I'm developing a J2EE 6 Web Application, using a MySql 5.02 DataBase. I'm trying to generate a hash digest of the ID, every time I create a new Entity. This is set to a column on the Table.
But well, I'm stuck on something that is apparently easy, and according with what I found googling, possible. Basically I want to retrieve the ID (Primary Key) of a newly persisted object, but whatever I try, it returns null.
The steps are follow are:
Create the Instance of the Entity -> userCard = new Users();
Setting the corresponding fields with some values.
Calling the persist() method of the EntityManager.
After reading some forums, I understood I had to either call flush() after persist(), or use merge() and use the returned entity to retrieve the id.
public void createAndFlush(Users users) {
em.persist(users);
em.flush();
}
public Integer edit(Users users) {
return ((Users)em.merge(users)).getIdvcards();
}
None of them (among some other combinations) work, well, the Entity is successfully persisted, but the ID field returns null.
These are the correspinding annotations of the id column I want to retrieve:
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "idvcards")
private Integer idvcards;
And obviously, the column idvcards of my table is set to Auto-Increment and Primary Key.
Maybe I just need to clarify my Peristence-Management concepts, but I'd appreciate if I can get some hint to solve this basic issue.
Maybe is not the most elegant solution ever, but finally I succeded on retrieving the ID of the new Entity:
public Integer create(User user) {
em.persist(users);
em.flush();
return (Integer) em.getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(users);
}
And well, althought is not related to the functionality, I changed the entity name to the singular form as #Bohemian suggested.
Try this, it works for me:
#Id
#GeneratedValue
#Column(name = "id", unique = true, nullable = false)
Also, I don't use the value returned from merge. I just persist then flush and the entity object magically gets the new key value.
p.s. Tables should never be named in the plural, and especially not the entity class. Call the class User (and the table if you can). Otherwise it's just confusing: Users sounds like a collection of User.
It will work fine
em.persist(usmApproveTransaction);
em.flush();
System.out.println("++++++e++++++++"+usmApproveTransaction.getUatApproveTransIdPk());
return usmApproveTransaction;
I'm getting id using em.flush(); after persist
++++++e++++++++51472