Generate non-conflicting non-primary-key unique "composite ids" in Hibernate - java

I'm not sure if my question title is correct, if not, please correct it.
Anyway, long story short, I have sellers, each seller belongs to a company, each seller has an ID as a primary key which is auto-incrementing and a seller-number which is unique per company.
id seller-number company-id
0 0 1
1 1 1
2 2 1
3 0 2
4 1 2
4 2 2
Here's my Seller entity:
#Entity
#Configurable
#Table(name="Seller", uniqueConstraints = {#UniqueConstraint(columnNames= {"company", "sellerNumber"})})
public class Seller implements Serializable {
#PersistenceContext
transient EntityManager entityManager;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private Long id;
#Version
#Column(name = "version")
private Integer version;
#ManyToOne
private Company company;
private Long sellerNumber;
...
Now when creating a seller, I do the following:
#Transactional
private void createSeller(SellerRequest request, SellerResponse response, Session session) {
Seller seller = new Seller();
// generate seller number
TypedQuery<Long> query = Seller.entityManager().createQuery("SELECT max(o.sellerNumber) + 1 FROM Seller AS o WHERE o.company=:company", Long.class);
query.setParameter("company", session.getCompany());
Long sellerNumber = query.getSingleResult();
seller.setSellerNumber(sellerNumber == null ? 1 : sellerNumber);
...
seller.setCompany(session.getCompany());
// persist
seller.persist();
...
The seller numbers I'm getting back is fine, until I start doing a lot of concurrent creates. If two creates happen at the exact same moment, I get a org.hibernate.exception.ConstraintViolationException
The requirements are that I only use an ID as a primary key, no composite primary keys. So taking these constraints into account, how should I be creating these entities so that they have unique seller numbers inside their companies and avoid ConstraintViolationExceptions ?
Is using max(o.sellerNumber) + 1 the right way to go or is there a better way to do this?

The hackish way to accomplish this was to simply catch the ConstraintViolationException and recursively retry the create, increment a retry counter in the request so that it's possible to bail if the number of recursive retries becomes too much.
try {
createSeller(...);
} catch(org.springframework.orm.jpa.JpaSystemException e){
if (e.contains(org.hibernate.exception.ConstraintViolationException.class)){
return thisMethodThatCallsCreateSeller(...);
} else {
throw e;
}
}
On small loads, there's almost no contention, when throwing heavy load at it, there's a little bit of a slowdown as contention causes multiple calls to try and create the seller with multiple ConstraintViolations being caught. Gets the job done though.

Related

org.hibernate.AssertionFailure Issue when getting sequence value

I am trying to insert multiple records in a table using loop and getting sequence number for that using below method. It is getting sequence number for very first time alone and during next iteration below exception is coming.Please help in resolving this
14:03:51.928 [http-nio-8080-exec-5] ERROR org.hibernate.AssertionFailure - HHH000099: an assertion failure occured (this may indicate a bug in Hibernate, but is more likely due to unsafe use of the session): org.hibernate.AssertionFailure: possible non-threadsafe access to session
14:03:51.938 [http-nio-8080-exec-5] ERROR u.s.m.e.p.o.b.c.ORBudgetController - 2020/08/26_14:03:51.938|1|pa23690|bearer 6d7417d8-6835-485e-956d-c362cb7bce2b|createRecord|possible non-threadsafe access to session
#Override
public int getNextSequenceNumber(String seqName) {
int nextValue = 0;
String strQuery = "SELECT " + seqName + ".NEXTVAL FROM DUAL";
Query q = entityManager.createNativeQuery(strQuery);
BigDecimal bd = (BigDecimal) q.getSingleResult();
nextValue = bd.intValue();
return nextValue;
}
You need to generate the sequence automatically, do it manually is a bad practice and can bring you problems in the future. There are several JPA strategies to automatically generate the sequence, this, for example, is The Sequence Strategy
#Entity
// Define a sequence - might also be in another class:
#SequenceGenerator(name="seq", initialValue=1, allocationSize=100)
public class EntityWithSequenceId {
// Use the sequence that is defined above:
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="seq")
#Id long id;
}
You can also use The Auto Strategy
#Entity
public class EntityWithAutoId1 {
#Id #GeneratedValue(strategy=GenerationType.AUTO) long id;
}

Hibernate - Many To Many wants persist data again

I have User class, where I want ManyToMany relation referencing on itself - particalury I am solving that user could follows/be followed another user. There is no need for other attributes, so it should be one table with composite primary key. This is how it is annotated:
#ManyToMany(cascade = {CascadeType.ALL, CascadeType.MERGE})
#JoinTable(name = "user_followers", joinColumns = { #JoinColumn(name = "follower") }, inverseJoinColumns = { #JoinColumn(name = "following") })
private Set<User> following = new HashSet<User>();
#ManyToMany(mappedBy = "following")
private Set<User> followers = new HashSet<User>();
Everything looks fine - table is generated, PK is composite follower_following. When I persist data on first try, it works - insert statement is executed. BUT on the second time, it is still trying to insert previously executed relation (obviously not the same relation) and I get:
Unexpected RuntimeException
Last cause: Duplicate entry 'follower-following' for key 'PRIMARY'
// EDIT: There are correct FK keys instead of follower-following
This is how I add follower and persist user:
if (!user.following.contains(toFollowUser)) {
user.following.add(toFollowUser);
}
if (session.contains(user)) {
session.persist(user);
} else {
session.merge(user)
}
session.getTransaction().commit(); // HERE I get exception
From my point of view, it looks like Hibernate doesn't see that previous relation was peristed and written to database and still try to insert new record that of course falls on duplicated key. Do you have an idea what am I doing wrong?

Hibernate save returns wrong generated id

I am having trouble using Hibernate with MSSQL Server 2012. No matter what I do when I try to insert a value in a certain table using Hibernate I get generated id=0.
Here is the model.
#Entity
#Table(name = "tbl_ClientInfo")
public class ClientInfo {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column (name = "auto_Client_ID", unique=true, nullable=false)
private int auto_Client_ID;
...
Here is the write.
public boolean addNewClient(Client client) {
// there is a class that wraps SessionFactory as singleton
Session session = getSessionFactory().openSession();
Transaction tx = null;
Integer clientFamId; //client family info id
Integer clientId; // actual client id
try {
// create fam info first with some data - need id for ClientInfo
tx = session.beginTransaction();
ClientFam clientFam = new ClientFam();
clientFamId = (Integer) session.save(clientFam);
clientFamId = (Integer) session.getIdentifier(clientFam); // this returns the right id
session.flush();
ClientInfo clientInfo = new ClientInfo();
clientInfo.setABunchOfFields(withStuff); //multiple methods
session.save(clientInfo);
clientInfoId = (Integer) session.getIdentifier(clientInfo); // this is always 0
session.flush();
tx.commit();
} catch (HibernateException e) {
if (tx!=null) tx.rollback();
e.printStackTrace();
return false;
} finally {
session.close();
}
return true;
}
In the database the PK auto_Client_ID is clustered, set to IDENTITY(1,1). Both ClientInfo and ClientFam records are created in the db, but hibernate returns 0. I also tried catching the value from save, but it's also 0.
I don't want to commit in-between separate insert: the transaction is when all inserts are fine (there are more after this, but I can't get to them because of this id issue yet).
The model for ClientFam is almost the same: the id field is #GeneratedValue(strategy=GenerationType.IDENTITY) as well.
I also tried specifying this for ClientInfo
#GeneratedValue(generator="increment", strategy=GenerationType.IDENTITY)
#GenericGenerator(name = "increment", strategy = "increment")
The first time I ran it it returned the correct value. However, the second time I ran it I got an error:
Cannot insert explicit value for identity column in table 'Report' when IDENTITY_INSERT is set to OFF
And that was the end of trying that. Everywhere I looked the recommendation is to use GenerationType.IDENTITY for auto incremented field in the db. That's supposed to return the right values. What might I be doing wrong?
I also tried getting the id from the ClientInfo object itself (I thought it should get written into it) after the right, but it's was also 0. Makes me think something is wrong with my ClientInfo model and/or annotations in it.
I found the problem with my situation - has nothing to do with Hibernate. There is a instead of insert trigger that wasn't returning id and hence messing up what save() returns.
This is just an educated guess, but you might want to remove the "unique=true" clause from the #Column definition. Hibernate may be handling the column as a unique constraint as opposed to a primary key.

Spring Data, JPA #ManyToOne lazy initialization not working

I know there are many similar questions about this trouble but nothing works for me.
I have #ManyToOne relationship between Aim and User.
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "user_id", nullable = false, updatable = false)
private User user;
and
#OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
private Collection<Aim> userAims;
respectively.
#Override
#Transactional(propagation = Propagation.REQUIRED)
#PreAuthorize("isAuthenticated() and principal.user.isEnabled() == true")
public Aim findById(String aimId) throws NumberFormatException, EntityNotFoundException {
Aim aim = null;
try {
aim = aimRepository.findOne(Long.parseLong(aimId));
} catch (NumberFormatException e) {
throw new InvalidDataAccessApiUsageException(e.getMessage(), e);
}
if (aim == null) throw new EntityNotFoundException("Aim with id: " + aimId + " not found!");
return aim;
}
#OneToMany associations work fine with lazy fetching. Method isn't nested to another #Transactional method so #Transactional works fine.
So the record exists.
Classes User and Aim aren't final and implement
Serializable
Some sources advice to put annotations on getters. It also doesn't
work.
#Fetch(FetchMode.SELECT) the same situation =\
Query via Hibernate results the same, but HQL query with left join
fetch works fine
My FK is ON UPDATE CASCADE ON INSERT CASCADE
optional = false also tried...
Pay attention that I haven't the LazyInitException
Thanks in advance!
I'm guessing from the code in your findById method, and by the reference to "lazy initialization not working" in the title, that you are wanting to find an Aim object by it's numeric Id, along with the associated User object.
In order to do this with lazy-loading, you need to 'get' the associated object, and (most importantly) you need to 'get' one of the associated entity's fields.
So the code inside the try block should be:
aim = aimRepository.findOne(Long.parseLong(aimId));
if (aim != null && aim.getUser() != null) {
aim.getUser().getUserId(); // doesn't need to be assigned to anything
}
Alternatively, if you have a logger available you can use the userId in a debug or trace log message:
if (aim != null && aim.getUser() != null) {
logger.debug("Lazy-loaded User " + aim.getUser().getUserId());
}
This has the added benefit that you can debug how things are lazy-loaded.
By the way, we found out the hard way that making a find routine throw an Exception when it doesn't find something is a bad idea. This is because you might want to use the find routine to find out if an Entity does NOT exist. If that is happening within a transaction, your exception may trigger an unwanted rollback (unless you specifically ignore it). Better to return null and check for that instead of using a try ... catch.

Hibernate: same generated value in two properties

I want the first to be generated:
#Id
#Column(name = "PRODUCT_ID", unique = true, nullable = false, precision = 12,
scale = 0)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "PROD_GEN")
#BusinessKey
public Long getAId() {
return this.aId;
}
I want the bId to be initially exactly as the aId. One approach is to insert the entity, then get the aId generated by the DB (2nd query) and then update the entity, setting the bId to be equal to aId (3rd query). Is there a way to get the bId to get the same generated value as aId?
Note that afterwards, I want to be able to update bId from my gui.
If the solution is JPA, even better.
Choose your poison:
Option #1
you could annotate bId as org.hibernate.annotations.Generated and use a database trigger on insert (I'm assuming the nextval has already been assigned to AID so we'll assign the curval to BID):
CREATE OR REPLACE TRIGGER "MY_TRIGGER"
before insert on "MYENTITY"
for each row
begin
select "MYENTITY_SEQ".curval into :NEW.BID from dual;
end;
I'm not a big fan of triggers and things that happen behind the scene but this seems to be the easiest option (not the best one for portability though).
Option #2
Create a new entity, persist it, flush the entity manager to get the id assigned, set the aId on bId, merge the entity.
em.getTransaction().begin();
MyEntity e = new MyEntity();
...
em.persist(e);
em.flush();
e.setBId(e.getAId());
em.merge(e);
...
em.getTransaction().commit();
Ugly, but it works.
Option #3
Use callback annotations to set the bId in-memory (until it gets written to the database):
#PostPersist
#PostLoad
public void initialiazeBId() {
if (this.bId == null) {
this.bId = aId;
}
}
This should work if you don't need the id to be written on insert (but in that case, see Option #4).
Option #4
You could actually add some logic in the getter of bId instead of using callbacks:
public Long getBId() {
if (this.bId == null) {
return this.aId;
}
return this.bId;
}
Again, this will work if you don't need the id to be persisted in the database on insert.
If you use JPA, after inserting the new A the id should be set to the generated value, i tought (maybe it depends on which jpa provider you use), so no 2nd query needed. then set bld to ald value in your DAO?

Categories

Resources