I have a Counter entity as below : for each prefix (current year and month), I am maintaining a counter that I need to increment.
#Entity
#Table(name = "T_COUNTER")
#SequenceGenerator(allocationSize = 1, name = "S_COUNTER", sequenceName = "S_COUNTER")
public class CodeCounter implements Serializable {
private static final long serialVersionUID = 6431190800245592165L;
#Id
#GeneratedValue(strategy = SEQUENCE, generator = "S_COUNTER")
#Column(name = "ID")
private Long id;
#Column(name = "PREFIX", nullable = false,unique = true)
private String prefix;
#Column(name = "COUNTER", nullable = false)
private Integer counter;
This is my very simple JPA repository, using Spring data :
public interface CodeCounterRepository extends JpaRepository<CodeCounter, Long> {
#Transactional
CodeCounter findByPrefix(String prefix);
}
Now, whenever my service is called, I need to get the right counter thanks to the prefix (and create it if it doesn't exist yet, the first time of the month), increment and save back. This is how I have implemented it so far :
//entry point in the service
public String generateUniqueRequestCode() {
System.out.println("Generating new Request Code for Consolidated Request");
Integer counter = getUniqueCodeAndUpdateCounter(calendarProvider);
String requestCode = format("%s_%04d_%02d_%06d", REQUEST_CODE_PREFIX, calendarProvider.getCurrentYear(), calendarProvider.getCurrentMonth(),
counter);
System.out.println("New Request Code for Consolidated Request:: " + requestCode);
return requestCode;
}
#Transactional
private synchronized Integer getUniqueCodeAndUpdateCounter(CalendarProvider calendarProvider) {
System.out.println("entering..");
String prefix = format("%04d_%02d", calendarProvider.getCurrentYear(), calendarProvider.getCurrentMonth());
CodeCounter codeCounter = codeCounterRepository.findByPrefix(prefix);
if (codeCounter != null) {
codeCounter.setCounter(codeCounter.getCounter() + 1);
} else {
codeCounter = new CodeCounter();
codeCounter.setPrefix(prefix);
codeCounter.setCounter(1);
}
CodeCounter counter=codeCounterRepository.save(codeCounter);
int result=counter.getCounter();
System.out.println("..exiting");
return result;
}
I've added a multithreaded unit test (using tempus fugit library ) with H2 DB, that shows it's working when 2 threads try to generate a unique code at the same time, but I'm not too happy with my code : I would like to get rid of that synchronized method and rely solely on proper transaction configuration.
If I remove the synchronized keyword, then both threads execute the method at same time and it fails because they generate the same prefix, which shouldn't happen (Unique index or primary key violation). Here's the log :
Generating new Request Code for Consolidated Request
Generating new Request Code for Consolidated Request
entering..
entering..
Hibernate: select codecounte0_.id as id1_4_, codecounte0_.counter as counter2_4_, codecounte0_.prefix as prefix3_4_ from t_counter codecounte0_ where codecounte0_.prefix=?
Hibernate: select codecounte0_.id as id1_4_, codecounte0_.counter as counter2_4_, codecounte0_.prefix as prefix3_4_ from t_counter codecounte0_ where codecounte0_.prefix=?
Hibernate: call next value for S_COUNTER Hibernate: call next value for S_COUNTER
Hibernate: insert into t_counter (counter, prefix, id) values (?, ?, ?)
Hibernate: insert into t_counter (counter, prefix, id) values (?, ?, ?)
..exiting
New Request Code for Consolidated Request:: CR_2016_09_000001
17:02:39.853 WARN SqlExceptionHelper - SQL Error: 23505, SQLState: 23505
17:02:39.854 ERROR SqlExceptionHelper - Unique index or primary key violation...
Any idea of how to implement this without synchronizing myself in the code ?
Is writing a method in your entity that is annotated with #PrePersist or #PreUpdate something that you are looking for?
Related
Something very bizarre have been happening. I have a very simple Entity recipe like so
#Data
#Entity
#Table(name = "recipe", schema = "public")
#AllArgsConstructor
#NoArgsConstructor
public class Recipe {
#Id
#GeneratedValue(
strategy = GenerationType.IDENTITY
)
#Column(name = "id", updatable = false, nullable = false)
private long id;
#Column(name = "name")
private String name;
#Column(name = "instructions")
private String instructions;
#Column(name = "date_added", nullable = false)
private String dateAdded;
#Column(name = "last_edited", nullable = false)
private String lastEdited;
}
and I have this post service that should post the 4 string attribute to the database
public void postRecipe(Recipe recipe){
var sql = """
INSERT INTO public.recipe ("name","instructions","date_added","last_edited")
VALUES (?, ?, ?, ?)
""";
jdbcTemplate.update(
sql,
recipe.getName(),
recipe.getInstructions(),
recipe.getDateAdded(),
recipe.getLastEdited()
);
}
However when the following jason is sent using postman, I get the null value error.
{
"name":"test",
"instructions":"don't eat",
"date_added":"03/04/2017",
"last_edited":"03/04/2017"
}
ERROR: null value in column \"date_added\" of relation \"recipe\" violates not-null constraint\n Detail: Failing row contains (3, null, don't eat, null, test)
The strangest thing is that only the "name" and "instruction" columns receive their data and not the other columns. I have tried adding another String attribute to the Entity class and it also cannot get data from the jason.
Edit 1:
I have tried adding the data directly through pgadmin and it worked fine
INSERT INTO recipe (name, instructions, date_added, last_edited)
VALUES ('test', 'test instruction', '2020/03/05', '2020/05/08');
It looks like your deserialization is broken - transforming your JSON into the Java entity, which results in some null values present. Most likely because date_added != dateAdded (field name), and Jackson cannot properly set a value.
I recommend having a look at Jackson guide: https://www.baeldung.com/jackson-annotations, #JsonProperty specifically. And overall do not mix entities and DTOs
After many trials and errors I was able to come up with a solution but still have no clue as to why this is happening. It turns out the under score in the annotation is the problem.
//won't work
#Column(name = date_added)
//works
#Column(name = dateadded)
This is pretty strange because I am fairly certain that the under score is generated by hibernate.
if anyone know why this is happening please let me know... for now I will just stay away from the under scrolls.
hope you all doing well. I'm continuously getting "ORA_02201 - Sequence is not allowed here" SQL syntax error when I'm trying to post new entry to Database using JSON. I'm using Oracle Autonomous Database
In Oracle Database, folks use GenerationType.SEQUENCE strategy for ID (Primary Key) increment.
I've tried something beforehand, let me share with you. It seems root of the issue is that Sequence is not working with JSON, or not set up to be working. It gives "Sequence is not allowed here" when my JSON request doesn't have id, so when Sequence is supposed to work.
Note that, I have this app as MVC web application where I save entity into database through getting data from form input in jsp pages. It works flawlessly and generate ID correctly. Problem is with JSON I believe.
{
"firstName" : "Mark",
"lastName" : "Heiberg",
"email" : "heibergmark#dot.com"
}
I made request by specifying the Id in JSON which id exists in database, then it updated the entry. So I get assured that root of the cause is id generation, when it should make new id, it doesn't work. I couldn't find anything about JSON-Oracle DB relationship in Internet. I wanted to ask here, maybe you know can I or should I do something to enable support for Sequence generation type on the moment of JSON request. Thanks in advance.
#PostMapping("/customers")
Customer addCustomer(#RequestBody Customer customer){
customer.setId(0); //set id to 0 to force it to add customer to DB, instead of updating current.
customerService.saveCustomer(customer);
return customer;
}
This is Table Definition SQL: (I've built it using Oracle APEX environtment, don't mind that 9999999999999999, it's by default)
CREATE TABLE "CUSTOMER"
( "ID" NUMBER(3,0) GENERATED BY DEFAULT AS IDENTITY MINVALUE 1 MAXVALUE 9999999999999999999999999999 INCREMENT BY 1 START WITH 1 CACHE 20 NOORDER NOCYCLE NOKEEP NOSCALE NOT NULL ENABLE,
"FIRST_NAME" VARCHAR2(15) COLLATE "USING_NLS_COMP",
"LAST_NAME" VARCHAR2(15) COLLATE "USING_NLS_COMP",
"EMAIL" VARCHAR2(30) COLLATE "USING_NLS_COMP",
CONSTRAINT "CUSTOMER_PK" PRIMARY KEY ("ID")
USING INDEX ENABLE
) DEFAULT COLLATION "USING_NLS_COMP"
/
CREATE OR REPLACE EDITIONABLE TRIGGER "BI_CUSTOMER"
before insert on "CUSTOMER"
for each row
begin
if :NEW."ID" is null then
select "CUSTOMER_SEQ".nextval into :NEW."ID" from sys.dual;
end if;
end;
/
ALTER TRIGGER "BI_CUSTOMER" ENABLE
/
This is my Entity class where Database annotations is implemented:
package com.customer_tracker.entity;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
#Entity
#Table(name = "CUSTOMER")
public class Customer {
public Customer() {
}
public Customer(String firstName, String lastName, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "Customer_SEQ")
#SequenceGenerator(name = "Customer_SEQ", sequenceName = "CUSTOMER_SEQ", allocationSize = 1)
#Column(name = "ID")
private int id;
#NotNull(message = "This field is required!")
#Size(min = 3, max = 15, message = "Write something!")
#Column(name = "FIRST_NAME")
private String firstName;
#NotNull(message = "This field is required!")
#Size(min = 3, max = 15, message = "Write something!")
#Column(name = "LAST_NAME")
private String lastName;
#NotNull(message = "This field is required!")
#Size(min = 3, max = 30, message = "Write something!")
#Column(name = "EMAIL")
private String email;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName.trim().replaceAll("\\s", "");
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName.trim().replaceAll("\\s", "");
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email.trim().replaceAll("\\s", "");
}
#Override
public String toString() {
return "id = " + id +
", firstName = " + firstName + ", lastName = " + lastName + ", email = " + email;
}
}
This is the code snippet where Hibernate going to save entry to Database:
package com.customer_tracker.dao;
import com.customer_tracker.entity.Customer;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.query.Query;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.List;
#Repository
public class CustomerList implements CustomerDAO{
#Autowired
private SessionFactory sessionFactory;
#Override
public void saveCustomer(Customer customer) {
Session session = sessionFactory.getCurrentSession();
session.saveOrUpdate(customer);
}
}
... GET, PUT, DELETE methods work as desired, only POST doesn't work as database should generate id behind the scenes.
The problem is that you are using an identity column with the option generated always, but at the same time you are using a trigger to populate the column ID with a sequence. You can't do that, either you use a sequence and a trigger, or you use identity columns. If you are in Oracle 12c or higher, I strongly recommend you to use identity columns, unless some of the restrictions they inherit may affect the way your application behaves.
See this example
SQL> create table test_col ( c1 number generated always as identity start with 1 increment by 1 , c2 varchar2(1) ) ;
Table created.
SQL> insert into test_col ( c2 ) values ( 'A' ) ;
1 row created.
SQL> select * from test_col ;
C1 C
---------- -
1 A
SQL> insert into test_col ( c1,c2 ) values ( 2,'B' ) ;
insert into test_col ( c1,c2 ) values ( 2,'B' )
*
ERROR at line 1:
ORA-32795: cannot insert into a generated always identity column
You have to drop the trigger and the sequence as they are useless in this scenario. The error is surely raised when the trigger is fired. When you use identity columns, Oracle handles automatically for you the sequence and the insert.
The identity columns are subject to the following restrictions:
Each table has one and only one identity column.
The data type of the identity column must be a numeric data type. the user-defined data type is not allowed to use with the identity clause.
The identity column is not inherited by the CREATE TABLE AS SELECTstatement.
The identity column cannot have another DEFAULT constraint.
The encryption algorithm for encrypted identity columns can be inferred therefore you should use a strong encryption algorithm.
The inline constraint of the identity column must not conflict with the NOT NULL and NOT DEFERRABLE constraint stated by the identity clause.
generated always is one of the options for Identity Columns. Read more about that in the link below
Identity Columns
if you are using JPA you should have #Id and #GeneratedValue (with proper strategy) annotations over id field and it will be auto-generated.
Example: https://spring.io/guides/gs/accessing-data-jpa/
I learn Spring Boot reading book Spring in Action 5. I try to insert data to H2 embedded database.I use SimpleJdbcInsert and executeAndReturnKey method.
Here is a Constructor:
#Autowired
public JdbcOrderRepository(JdbcTemplate jdbc) {
this.orderInserter = new SimpleJdbcInsert(jdbc).withTableName("Taco_Order").usingGeneratedKeyColumns("id");
this.orderTacoInserter = new SimpleJdbcInsert(jdbc).withTableName("Taco_Order_Tacos");
this.objectMapper = new ObjectMapper();
}
Here is methods to insert data:
#Override
public Order save(Order order) {
order.setPlacedAt(new Date());
long orderId = saveOrderDetails(order);
order.setId(orderId);
List<Taco> tacos = order.getTacos();
for (Taco taco : tacos)
saveTacoToOrder(taco, orderId);
return order;
}
private long saveOrderDetails(Order order) {
Map<String, Object> values = objectMapper.convertValue(order, Map.class);
values.put("placedAt", order.getPlacedAt());
long orderId = orderInserter.executeAndReturnKey(values).longValue();
return orderId;
}
The error is on this string:
long orderId = orderInserter.executeAndReturnKey(values).longValue();
Error text:
There was an unexpected error (type=Internal Server Error, status=500).
PreparedStatementCallback; ???????? NULL ?? ????????? ??? ????
"DELIVERYNAME" NULL not allowed for column "DELIVERYNAME"; SQL statement:
INSERT INTO Taco_Order (DELIVERYNAME, DELIVERYSTREET, DELIVERYCITY,
DELIVERYSTATE, DELIVERYZIP, CCNUMBER, CCEXPIRATION, CCCVV, PLACEDAT)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?) [23502-197]; nested exception is
org.h2.jdbc.JdbcSQLException: ???????? NULL ?? ????????? ??? ????
"DELIVERYNAME" NULL not allowed for column "DELIVERYNAME"; SQL statement:
INSERT INTO Taco_Order (DELIVERYNAME, DELIVERYSTREET, DELIVERYCITY,
DELIVERYSTATE, DELIVERYZIP, CCNUMBER, CCEXPIRATION, CCCVV, PLACEDAT)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?) [23502-197]
Bur in debugger evrything is alright:
value parameter is fully loaded by values.
idea screensot
Class Order:
#Data
public class Order {
#NotBlank(message = "Name is required")
private String name;
#NotBlank(message = "Name is required")
private String street;
#NotBlank(message = "Name is required")
private String city;
#NotBlank(message = "Name is required")
private String state;
#NotBlank(message = "Name is required")
private String zip;
#CreditCardNumber(message = "Not valid cc")
private String ccNumber;
#Pattern(regexp = "^(0[1-9]|1[0-2])([\\/])([1-9][0-9])$", message = "Must be formatted MM/YY")
private String ccExpiration;
#Digits(integer = 3, fraction = 0, message = "Invalid CVV")
private String ccCVV;
private Long id;
private Date placedAt;
List<Taco> tacos = new ArrayList<>();
public void addDesign(Taco design) {
this.tacos.add(design);
}
}
How to fix this problem and insert data to H2 database using SimpleJdbcInsert?
I faced same issue today. I was able to resolve this.
To fix this remove, go to schema.sql(file in which you are creating tables) and remove delivery from fields in Taco_Order table.
From book
SimpleJdbcInsert has a couple of useful methods for executing the insert: execute() and executeAndReturnKey(). Both accept a Map, where the map keys correspond to the column names in the table the data is inserted into. The map values are inserted into those columns.
Keys name are fields from Order.java
I went through the screen shot and i do not see any value for column DELIVERYNAME.
The issue is because of the table in which you are running the insert operation doesn't allow null values for the column DELIVERYNAME.
Ways to solve this:-
Remove the NOT NULL constraints from the column in the table definition.
Remove the #NotBlank annotation from the members of the object if you are not sure whether the data is going to be blank or not.
Use proper mapping between the column names in table and those with the member name in Class/Entity Definition. You can use #Column annotation to easily do that.
As much I can understand following your code and screenshot, that the issue is because of improper mapping between column names and that of the member of the object.
It is recommended that rather than relying on the auto mapping of Spring Boot coder should manually do the mapping using #Column annotation or mapping through hibernate configuration file.
The issue is with attributes name in Order class. These should mirror the coloumn name of respective table.
Eg:
private String deliveryName; -> would be mapped to deliveryName field in db.
The issue has arisen as in schema.sql the field is named as "deliveryName" while in ingredient class the attirbute is named "name".
This answer is for those who follow the examples from book "Spring in action". You won't do such thing in real life, but if you're a reader of this book it is reasonable to replace this:
Map<String, Object> values = objectMapper.convertValue(order, Map.class);
values.put("placedAt", order.getPlacedAt());
with this:
Map<String,Object> values = new HashMap<>();
values.put("ID", order.getId());
values.put("PLACEDAT", order.getPlacedAt());
values.put("DELIVERYNAME", order.getName());
values.put("DELIVERYSTREET", order.getStreet());
values.put("DELIVERYCITY", order.getCity());
values.put("DELIVERYSTATE", order.getState());
values.put("DELIVERYZIP", order.getZip());
values.put("CCNUMBER", order.getCcNumber());
values.put("CCEXPIRATION", order.getCcExpiration());
values.put("CCCVV", order.getCcCVV());
Previous answers mentioned changing schema which will cause problems if you will follow examples from next chapters or using jpa annotations to perform mapping, but according to the author of the book you don't know about them because they are introduced in the next chapters. Besides you won't use JdbcOrderRepository class from the next chapter.
Names of fields in Order class must be the same as columns names in table Taco_Order.
#Data
public class Order {
private Long id;
private Date placedAt;
#NotBlank(message = "Podanie imienia i nazwiska jest obowiazkowe")
private String deliveryName;
#NotBlank(message = "Podanie ulicy jest obowiązkowe")
private String deliveryStreet;
#NotBlank(message = "Podanie miasta jest obowiązkowe")
private String deliveryCity;
#NotBlank(message = "Podanie województwa jest obowiązkowe")
private String deliveryState;
#NotBlank(message = "Podanie kodu pocztowego jest obowiązkowe")
private String deliveryZip;
#CreditCardNumber(message = "To nie jest prawidłowy numer karty kredytowej")
private String ccNumber;
#Pattern(regexp = "^(0[1-9]|1[0-2])([\\/])([1-9][0-9])$", message = "Wartość musi być w formacie MM/RR")
private String ccExpiration;
#Digits(integer = 3, fraction = 0, message = "Nieprawidłowy kod CVV")
private String ccCVV;
public List<Taco> tacos = new ArrayList<>();
public void addDesign(Taco design) {
this.tacos.add(design);
}
public List<Taco> getTacos() {
return tacos;
}
}
I encountered the same problem today and all I had forgotten to do was to update the fields in the order.html file to deliveryName, deliveryCity etc.
I have a problem with JPA querying a MySQL table that has a column of type geometry. It contains polygons having sets of latitude and longitude as the coordinates. While executing the nativequery to select from the table, I am getting the following error
Exception Description: The primary key read from the row [ArrayRecord(
=> POLYGON((102.642944444444 2.9757087270706,102.642944444444 2.79805447470818,....
=> 16.0
=> 325990)] during the execution of the query was detected to be null. Primary keys must not contain null.
However the table has no row with primary key as null. This specific row has a very large polygon with 66 coordinates. Not sure if the problem is because of this.
Following are the table column names and types
geomarea - geometry
riskvalue - double
id - int (Autoincrement, Primary Key)
Following is the code in my EJB to read the table.
Query query = em.createNativeQuery("select astext(geomarea) geomarea,riskvalue,id from earthquakeRisk where Contains(geomarea,GeomFromText('POINT(" + node.getLongitude() + " "+node.getLatitude()+")'))",Earthquakerisk.class);
geomList.addAll(query.getResultList());
And here is how the fields are declared in the entity class
public class Earthquakerisk implements Serializable {
private static final long serialVersionUID = 1L;
#Basic(optional = false)
#NotNull
#Lob
#Column(name = "geomArea")
private byte[] geomArea;
#Column(name = "riskvalue")
private Double riskvalue;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "id")
private Integer id;
Any idea how to solve this?
I found a solution to the problem. Adding it here in case someone finds it useful.
Solved it by removing the Earthquakerisk.class from the query, and changing my List to List. So the working code is now as follows.
List<Object[]> geomList = new ArrayList<>();
Query query = em.createNativeQuery("select astext(geomarea) geomarea,riskvalue,id from earthquakeRisk where Contains(geomarea,GeomFromText('POINT(" + node.getLongitude() + " "+node.getLatitude()+")'))");
geomList.addAll(query.getResultList());
I've mapped my class as follow (omitted other fields as only ID matters):
#Entity
#Table(name = "MODEL_GROUP")
#Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
public class SettlementModelGroup implements Serializable
{
#Id
#GeneratedValue(generator = "MODEL_GROUP_SEQ", strategy = GenerationType.SEQUENCE)
#GenericGenerator(name = "MODEL_GROUP_SEQ",
strategy = "sequence",
parameters = #Parameter(name = "sequence", value = "SEQ_MODEL_GROUP_MODEL_GROUP_ID"))
#Column(name = "MODEL_GROUP_ID", nullable = false)
private Integer modelId;
}
when I'm saving new object:
Integer modelGroupId = sessionFactory.getCurrentSession().save( modelGroup );
System.out.println( modelGroupId );
ID is set as for example 23, but when I look at the database it is actually 24. This is leading to many problems, as I'm using this ID later on. Any idea why it is making this gap?
SQL logs show that everything is fine (I thinks so):
Hibernate:
select
SEQ_MODEL_GROUP_MODEL_GROUP_ID.nextval
from
dual
Hibernate:
insert
into
MODEL_GROUP
(DOMAIN_ID, DESCRIPTION, NAME, PERIOD_TYPE_ID, MODEL_GROUP_TYPE_ID, STATUS_ID, OWNER_ID, MODEL_GROUP_ID)
values
(?, ?, ?, ?, ?, ?, ?, ?)
Trigger and Sequence:
CREATE SEQUENCE "SEQ_MODEL_GROUP_MODEL_GROUP_ID"
INCREMENT BY 1
START WITH 1
NOMAXVALUE
MINVALUE 1
NOCYCLE
NOCACHE
NOORDER
;
CREATE OR REPLACE TRIGGER "TRG_MODEL_GROUP_MODEL_GROUP_ID"
BEFORE INSERT
ON "MODEL_GROUP"
FOR EACH ROW
WHEN (NEW."MODEL_GROUP_ID" is NULL)
BEGIN
SELECT "SEQ_MODEL_GROUP_MODEL_GROUP_ID".NEXTVAL
INTO :NEW."MODEL_GROUP_ID"
FROM DUAL;
END;
Apparently, when Hibernate ask your database for nextValue of ID, it fires also Trigger. So when I ask for ID, I've got number 23 but when actually saving to database by commiting transaction, it is increased again so I've got 24. Solution is described here:
HIbernate issue with Oracle Trigger for generating id from a sequence
To make it work correctly, I changed Trigger:
CREATE OR REPLACE TRIGGER "TRG_MODEL_GROUP_MODEL_GROUP_ID"
BEFORE INSERT
ON "MODEL_GROUP"
FOR EACH ROW
WHEN (NEW."MODEL_GROUP_ID" is NULL)
BEGIN
SELECT "SEQ_MODEL_GROUP_MODEL_GROUP_ID".NEXTVAL
INTO :NEW."MODEL_GROUP_ID"
FROM DUAL;
END;