I'm trying to insert the user object to Oracle with Hibernate. Object is loaded with values entered in the user registration form.
id is #GeneratedValue
pass is #Transient
These are the properties of User and UserType classes:
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(length = 50, nullable = false)
#NotBlank(message = "Boş bırakılamaz.")
#Size(min=2, max = 50)
private String firstName;
#Column(length = 50, nullable = false)
#NotBlank(message = "Boş bırakılamaz.")
#Size(min=2, max = 50)
private String lastName;
#Column(length = 50, nullable = false)
#NotBlank(message = "Boş bırakılamaz.")
#Size(min=2, max = 50)
private String userName;
#Column(columnDefinition = "char(128)")
private String passHash;
#Column(columnDefinition = "char(32)")
private String salt;
#ManyToOne
#NotNull(message = "Boş bırakılamaz.")
private UserType userType;
#Transient
#NotBlank(message = "Boş bırakılamaz.")
#Size(min=4)
private String pass;
}
#Entity
public class UserType {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(length = 50, nullable = false)
#NotBlank(message = "Boş bırakılamaz.")
private String name;
}
This is the Oracle DDL:
create table DH_USER
(
ID NUMBER(19) generated as identity
primary key,
FIRSTNAME VARCHAR2(50 char) not null,
LASTNAME VARCHAR2(50 char) not null,
PASSHASH CHAR(128),
SALT CHAR(32),
USERNAME VARCHAR2(50 char) not null,
USERTYPE_ID NUMBER(19) not null
constraint FKO3DS41MXQLO527MM8H8J7F0FL
references DH_USERTYPE
)
create table DH_USERTYPE
(
ID NUMBER(19) generated as identity
primary key,
NAME VARCHAR2(50 char) not null
)
After adding logging.level.org.hibernate.SQL=DEBUG and logging.level.org.hibernate.type=TRACE to application.properties file, critical part of the log is now like this:
2020-12-08 15:41:59.256 INFO 6676 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
2020-12-08 15:41:59.356 DEBUG 6676 --- [nio-8080-exec-2] org.hibernate.SQL : select usertype0_.id as id1_1_0_, usertype0_.name as name2_1_0_ from DH_UserType usertype0_ where usertype0_.id=?
Hibernate: select usertype0_.id as id1_1_0_, usertype0_.name as name2_1_0_ from DH_UserType usertype0_ where usertype0_.id=?
2020-12-08 15:41:59.379 TRACE 6676 --- [nio-8080-exec-2] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [2]
2020-12-08 15:41:59.481 TRACE 6676 --- [nio-8080-exec-2] o.h.type.descriptor.sql.BasicExtractor : extracted value ([name2_1_0_] : [VARCHAR]) - [Yönetici]
2020-12-08 15:41:59.830 DEBUG 6676 --- [nio-8080-exec-2] org.hibernate.SQL : insert into DH_User (id, firstName, lastName, passHash, salt, userName, userType_id) values (default, ?, ?, ?, ?, ?, ?)
Hibernate: insert into DH_User (id, firstName, lastName, passHash, salt, userName, userType_id) values (default, ?, ?, ?, ?, ?, ?)
2020-12-08 15:41:59.834 WARN 6676 --- [nio-8080-exec-2] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 17068, SQLState: 99999
2020-12-08 15:41:59.834 ERROR 6676 --- [nio-8080-exec-2] o.h.engine.jdbc.spi.SqlExceptionHelper : Invalid arguments in call
2020-12-08 15:41:59.863 ERROR 6676 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.orm.jpa.JpaSystemException: could not prepare statement; nested exception is org.hibernate.exception.GenericJDBCException: could not prepare statement] with root cause
Why does Hibernate add the id field into the insert statement?
id field is "generated as identity" so doesn't need to be involved in the query.
And why does it try to insert "default" into id column? It should use null instead of default, as null is the value of user.id at that point in the code.
DH_User (id, firstName, lastName, passHash, salt, userName, userType_id) values (default, ?, ?, ?, ?, ?, ?)
Oracle should support the DEFAULT syntax, I've seen it being used like here: https://www.techrepublic.com/article/oracle-tip-how-to-use-default-values-with-database-columns/
Maybe there are different editions of Oracle and yours does not have support for this? Or maybe you need to use GENERATED BY DEFAULT AS IDENTITY? Anyway, you can override this by subclassing org.hibernate.dialect.identity.Oracle12cIdentityColumnSupport and org.hibernate.dialect.Oracle12cDialect which then has to return that subclass instance in getIdentityColumnSupport().
In a custom Oracle12cIdentityColumnSupport class you can also use null for getIdentityInsertString instead of default, but then make sure you also change getIdentityColumnString to use generated by default on null as identity.
By the way, I would recommend using sequences whenever possible for performance reasons. The use of sequences and sequence caching allows Hibernate to defer/reorder and batch inserts which will improve performance drastically when inserting a lot.
Changing the annotation of id field as
#GeneratedValue(strategy = GenerationType.SEQUENCE)
worked. But I don't understand why.
And I don't want to use a sequence. I want to use an auto-generated ID and get a value back post insertion.
I need an annotation resulting exactly:
GENERATED BY DEFAULT ON NULL AS IDENTITY
I tried to use the columnDefinition. You can see that "not null" is appended automatically even I use nullable = true.
As Christian suggested, I tried Oracle12cIdentityColumnSupport method and successfully changed Hibernate's way of generating IDENTITY ddl. But I got the same error, Invalid arguments in call :(
Seems like the only way for me is GenerationType.SEQUENCE
AFAIK, generated by default on null as identity is the least restrictive way of defining id columns and allowing null id's while inserting.
Related
I was playing with Spring-Data-JDBC and encountered 2 issues. I have following entities with 1:N relationship.
------
DROP TABLE IF EXISTS product;
CREATE TABLE product (
product_id int AUTO_INCREMENT PRIMARY KEY,
name varchar(250) not null,
description varchar(512) not null
);
DROP TABLE IF EXISTS product_line;
CREATE TABLE product_line (
product_id int constraint fk_product_line_product references product(product_id),
label varchar(250) not null
);
----------
#Data
#Builder
public class Product {
#Id
private Long productId;
private String name;
private String description;
#Singular
#MappedCollection(idColumn = "product_id", keyColumn = "product_id")
private Set<ProductLine> lines;
}
#Data
#Builder
public class ProductLine {
private Long productId;
private String label;
}
Problem 1: Following test case fails because I was expecting to have the productId populated in the ProductLine object but it is not. Is this the expected behavior of Spring Data JDBC?
#SpringBootTest
class SpringDataJdbcApplicationTests {
#Autowired
private ProductRepository productRepository;
#Test
void saveTest() {
Product product = Product.builder()
.name("Product-1")
.description("Description")
.line(ProductLine
.builder()
.label("Line-label")
.build())
.build();
this.productRepository.save(product);
assertThat(product.getProductId()).isNotNull();
assertThat(product.getLines()).isNotNull().isNotEmpty().hasSize(1);
assertThat(product.getLines().stream().findFirst()).isPresent();
assertThat(product.getLines().stream().findFirst().get().getProductId()).isNotNull().isEqualTo(product.getProductId()); // -----> Fails here.
}
}
Problem 2: If I change Set<ProductLine> to List<ProductLine>, it fails due to JdbcSQLIntegrityConstraintViolationException, which means the product id set to 0 as seen in the log snippet below.
2022-09-10 22:33:12.393 DEBUG 18460 --- [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [INSERT INTO "PRODUCT_LINE" ("LABEL", "PRODUCT_ID") VALUES (?, ?)]
2022-09-10 22:33:12.393 TRACE 18460 --- [ main] o.s.jdbc.core.StatementCreatorUtils : Setting SQL statement parameter value: column index 1, parameter value [Line-label], value class [java.lang.String], SQL type 12
2022-09-10 22:33:12.393 TRACE 18460 --- [ main] o.s.jdbc.core.StatementCreatorUtils : Setting SQL statement parameter value: column index 2, parameter value [0], value class [java.lang.Integer], SQL type 4
Following test case fails because I was expecting to have the productId populated in the ProductLine object but it is not. Is this the expected behavior of Spring Data JDBC?
Yes, if you want a productId you have to (and can easily) populate it yourself using plain Java code.
But you really shouldn't need the productId in the first place since if you follow Domain Driven Design, you will access a ProductLine exclusively from a Product which already has the id at hand.
The article https://spring.io/blog/2021/09/22/spring-data-jdbc-how-do-i-make-bidirectional-relationships might be helpful.
If I change Set<ProductLine> to List<ProductLine>, it fails due to JdbcSQLIntegrityConstraintViolationException, which means the product id set to 0 as seen in the log snippet below.
You have two problems here:
You already have two sources for the product_id field: The relation from the aggregate root and the simple field, which may cause problems.
You mapped both the back reference to the aggregate root idColumn and the index of the list keyColumn to the same database column. Together with the simple field from above these are three values all mapped to the same column. Not good.
The value that seems to win is the list index, resulting in the exception.
In order to fix that, create an additional column in the product_line table and map the list index to it.
So when im trying to save data on a in memory H2 database with unit tests i get this following error. Saving works fine on normal postgres database. But im trying to make it work with H2 for unit tests.
> could not execute statement; SQL [n/a]; constraint [null]; nested
> exception is org.hibernate.exception.ConstraintViolationException:
> could not execute statement
This it the data class
#Entity
#Table(name = "delivery_type")
data class DeliveryType (
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", updatable = false, nullable = false)
val id: Long = 0,
#Column(name = "type")
val type: String,
)
Im trying to run a simple test
#SpringBootTest
#Transactional
internal class CarQueueServiceTest {
#Autowired
lateinit var deliveryTypeRepo: DeliveryTypeRepo
#Test
fun addDeliveryType() {
val deliveryType = DeliveryType(type = "Truck")
println(deliveryTypeRepo.save(deliveryType))
}
}
I will add some of the hibernate logs
Hibernate: create table delivery_type (id bigint generated by default as identity, type varchar(255), primary key (id))
Hibernate: insert into delivery_type (id, type) values (null, ?)
2022-03-21 11:36:00.656 TRACE 17340 --- [ Test worker] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [Truck]
2022-03-21 11:36:00.660 WARN 17340 --- [ Test worker] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 23502, SQLState: 23502
2022-03-21 11:36:00.660 ERROR 17340 --- [ Test worker] o.h.engine.jdbc.spi.SqlExceptionHelper : NULL not allowed for column "ID"; SQL statement:
insert into delivery_type (id, type) values (null, ?) [23502-210]
H2 setup
spring.h2.console.enabled=true
spring.h2.console.path=/h2_console
spring.datasource.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1
spring.datasource.driverClassName=org.h2.Driver
spring.jpa.hibernate.ddl-auto = create
spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
Is there a better way to update a versioned entity other than to catch exception and retrying ?
Below is the code
#Entity
#AllArgsConstructor
#Builder
#NoArgsConstructor
#Data
#IdClass(com.test.domain.EmployeeKey.class)
public class Employee {
#Id
private Integer employeeId;
private String name;
private Double commission;
#Id
private LocalDate inputDate;
#Version
private Integer version;
}
public class EmployeeKey implements Serializable {
private Integer employeeId;
private java.time.LocalDate inputDate;
}
#SpringBootApplication
#Slf4j
public class SampleApplication {
public static void main(final String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
#Component
public class AppStartupRunner implements ApplicationRunner {
#Autowired
EmployeeRepo employeeRepo;
#Override
public void run(ApplicationArguments args) throws Exception {
log.info("Your application started with option names : {}", args.getOptionNames());
List<Employee> empList = new ArrayList();
Employee employeeComm1 = Employee.builder().employeeId(1).name("Sam").commission(100.45).inputDate(LocalDate.of(2019,10,01)).build();
Employee employeeComm2 = Employee.builder().employeeId(1).name("Sam").commission(87.54).inputDate(LocalDate.of(2019,10,01)).build();
Employee employeeComm3 = Employee.builder().employeeId(2).name("John").commission(56.78).inputDate(LocalDate.of(2019,10,01)).build();
Employee employeeComm4 = Employee.builder().employeeId(3).name("Katie").commission(65.23).inputDate(LocalDate.of(2019,10,01)).build();
empList.add(employeeComm1);
empList.add(employeeComm3);
empList.add(employeeComm4);
empList.add(employeeComm2);
// employeeRepo.saveAll(empList);
for (Employee emp: empList) {
try {
employeeRepo.save(emp);
}
catch(Exception e) {
if (employeeRepo.findByEmployeeIdAndInputDate(emp.getEmployeeId(), emp.getInputDate()).isPresent()) {
Employee empFromDb = employeeRepo.findByEmployeeIdAndInputDate(emp.getEmployeeId(), emp.getInputDate()).get();
empFromDb.setCommission(emp.getCommission());
empFromDb.setName(emp.getName());
employeeRepo.save(empFromDb);
}
}
}
}
}
}
Output shown below
Hibernate: insert into employee (commission, name, version, employee_id, input_date) values (?, ?, ?, ?, ?)
Hibernate: insert into employee (commission, name, version, employee_id, input_date) values (?, ?, ?, ?, ?)
Hibernate: insert into employee (commission, name, version, employee_id, input_date) values (?, ?, ?, ?, ?)
Hibernate: insert into employee (commission, name, version, employee_id, input_date) values (?, ?, ?, ?, ?)
2019-10-15 16:43:10.372 WARN 17216 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1, SQLState: 23000
2019-10-15 16:43:10.372 ERROR 17216 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : ORA-00001: unique constraint (I706446.SYS_C008493) violated
Hibernate: select employee0_.employee_id as employee_id1_0_, employee0_.input_date as input_date2_0_, employee0_.commission as commission3_0_, employee0_.name as name4_0_, employee0_.version as version5_0_ from employee employee0_ where employee0_.employee_id=? and employee0_.input_date=?
Hibernate: select employee0_.employee_id as employee_id1_0_, employee0_.input_date as input_date2_0_, employee0_.commission as commission3_0_, employee0_.name as name4_0_, employee0_.version as version5_0_ from employee employee0_ where employee0_.employee_id=? and employee0_.input_date=?
Hibernate: select employee0_.employee_id as employee_id1_0_0_, employee0_.input_date as input_date2_0_0_, employee0_.commission as commission3_0_0_, employee0_.name as name4_0_0_, employee0_.version as version5_0_0_ from employee employee0_ where employee0_.employee_id=? and employee0_.input_date=?
Hibernate: update employee set commission=?, name=?, version=? where employee_id=? and input_date=? and version=?
I expect an hibernate to issue an update with Version column
incrementing
Your expectation is incorrect. You use version not as it is intended to be used. The purpose of versioning is to provide optimistic locking and not simply indicate how many times has the entity changed. The intended usage scenario is following: before updating an entity you should read it from the database, then modify, then save. Hibernate checks if the version you are trying to save is the same as in the database.
A) If the same, it will automatically increment version and save your entity, i.e. it will update the existing record in the database.
B) If the version in the entity you are trying to save is less than the version in the database, it means you are trying to save an outdated version. It means you are not aware of changes that were done in the meanwhile. In such case Hibernate will throw an exception.
I am pretty new in Hibernate and I have the following problem: I have to perform a massive insert operation (I need to insert a lot of the model object and each of these object contains a byte[] field mapping a blob field on the DB).
I am using Spring Boot (so I need to configure it on Spring Boot) and Spring Data JPA.
So I have the following situation:
1) RoomMedia my model object:
#Entity
#Table(name = "room_media")
public class RoomMedia {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
//#Column(name = "id_room")
//private Long idRoom;
#ManyToOne
#JoinColumn(name = "id_room", nullable = false) // da rinominare anche sul DB in room_fk
private Room room;
#Lob
#Column(name = "media")
private byte[] media;
private String description;
private Date time_stamp;
public RoomMedia() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
........................................................................
........................................................................
GETTER AND SETTER METHODS
........................................................................
........................................................................
}
Then into another class I have this code that persist a list of RoomMedia objects:
roomMediaDAO.save(mediaListToBeInserted);
where mediaListToBeInserted is a List.
The problm is that doing in this way it is very slow because this statment generate insertion queries like these:
Hibernate: insert into room_media (description, media, id_room, time_stamp) values (?, ?, ?, ?)
Hibernate: insert into room_media (description, media, id_room, time_stamp) values (?, ?, ?, ?)
Hibernate: insert into room_media (description, media, id_room, time_stamp) values (?, ?, ?, ?)
Hibernate: insert into room_media (description, media, id_room, time_stamp) values (?, ?, ?, ?)
Hibernate: insert into room_media (description, media, id_room, time_stamp) values (?, ?, ?, ?)
............................................................................
............................................................................
............................................................................
So the records are inserted one by one.
I need to insert these records massively, genereting a code like this:
INSERT INTO MyTable ( Column1, Column2 ) VALUES
( Value1, Value2 ),
( Value1, Value2 ),
( Value1, Value2 ),
.................................
.................................
.................................
( Value1, Value2 )
Reading this documentation:
https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/batch.html
I know that I can set Hibernate to perform Batch Insert that should do something like this.
But I have some doubts:
1) Reading the previous documentation link it says that:
Hibernate disables insert batching at the JDBC level transparently if
you use an identity identifier generator.
and I have that my primary key of my model object is annoted with:
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
So what it means? That I can't do massive insert of a list of these objects? It seem me strange that there is no way to do it.
In the previous article it explain:
You can also do this kind of work in a process where interaction with
the second-level cache is completely disabled:
hibernate.cache.use_second_level_cache false
However, this is not absolutely necessary, since we can explicitly set
the CacheMode to disable interaction with the second-level cache.
So it seem to me that I can do it in some way but I don't know what is this second-level cache and why it have to be disabled to perform batch insert of object having an identity identifier generator setted.
2) In my Spring Boot project I have this application.properties file containing the following settings related to the database connection and Hibernate configuration. So I think that this should be the point where to add the set to handle the batch massive insert. But what have I to set?
#No auth protected
endpoints.shutdown.sensitive=true
#Enable shutdown endpoint
endpoints.shutdown.enabled=true
spring.datasource.url = jdbc:mysql://XX.YY.ZZZ.WW:3306/BeTriviusTest?useUnicode=true&characterEncoding=UTF-8
spring.datasource.username = MyUserName
spring.datasource.password = MyPswd
spring.datasource.driverClassName=com.mysql.jdbc.Driver
# Keep the connection alive if idle for a long time (needed in production)
spring.datasource.testWhileIdle = true
spring.datasource.validationQuery = SELECT 1
# Show or not log for each sql query
spring.jpa.show-sql = true
spring.jpa.database = mysql
# Hibernate ddl auto (create, create-drop, update)
spring.jpa.hibernate.ddl-auto = validate
# Naming strategy
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy
# Use spring.jpa.properties.* for Hibernate native properties (the prefix is
# stripped before adding them to the entity manager)
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.spatial.dialect.mysql.MySQLSpatial5InnoDBDialect
spring.jpa.database-platform = org.hibernate.spatial.dialect.mysql.MySQLSpatial5InnoDBDialect
i use spring save
HighWay highWay = new HighWay();
highWay.setId("000");
HighWayRepository hRepository = (HighWayRepository) context
.getBean("highWayRepository");
hRepository.save(highWay);
hRepository.flush();
public interface HighWayRepository extends JpaRepository<HighWay, String> {
}
the table is like f_id varchar(256) NOT NULL,
public class HighWay {
#Id
#Column(name="f_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private String id;}
but throws exception
Caused by: java.sql.SQLException: Field 'f_id' doesn't have a default
value at
com.mysql.jdbc.SQLError.createSQLException(SQLError.java:946) at
com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2870) at
com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1573) at
com.mysql.jdbc.ServerPreparedStatement.serverExecute(ServerPreparedStatement.java:1169)
at
com.mysql.jdbc.ServerPreparedStatement.executeInternal(ServerPreparedStatement.java:693)
at
com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1404)
at
com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1318)
at
com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1303)
at
com.alibaba.druid.pool.DruidPooledPreparedStatement.executeUpdate(DruidPooledPreparedStatement.java:253)
at
org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:208)
... 51 more
i check the document,
Id-Property inspection (default) By default Spring Data JPA inspects the Id-Property of the given Entity. If the Id-Property is null, then the entity will be assumed as new, otherwise as not new.
the sql is like Hibernate: insert into us_highway (blockTime, blockType, endNum, predictTime, publishTime, roadName, situation, startNum, tips) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
not id is insert!
because i want to assign id manually,the id-property is not null if new ,how to configure to save ?
If you are assigning Id manually.Remove below annotation on id.
#GeneratedValue(strategy = GenerationType.IDENTITY)