Why name derived JPA query method works so? - java

I have a Repository interface and name derived query method in it:
int deleteAllBySpaceIdAndUserId(UUID spaceId, UUID userId);
Calling this method results in Select query first and then Delete query.
What is the point of issuing select? Why isn't it evaluated as single query?
Why do i need it to work this (weirdly) way?

Spring Data doesn't execute direct SQL queries to delete an entity, but uses the EntityManager and it's remove method.
As an example you can look in the class org.springframework.data.jpa.repository.support.SimpleJpaRepository, which provides implementations for the default methods (e.g. deleteById etc.):
public void delete(T entity) {
Assert.notNull(entity, "Entity must not be null!");
if (entityInformation.isNew(entity)) {
return;
}
Class<?> type = ProxyUtils.getUserClass(entity);
T existing = (T) em.find(type, entityInformation.getId(entity));
// if the entity to be deleted doesn't exist, delete is a NOOP
if (existing == null) {
return;
}
em.remove(em.contains(entity) ? entity : em.merge(entity));
}
Since the remove method of the EntityManager takes the entity object itself, and not just an ID value or similiar, Spring Data has to execute the find method with the chosen parameters (either ID, or in your case 2 values), to get the actual entity object and then to remove it from the EntityManager.

Related

CrudRepository - Hibernate - Overwrite existing model with manual set id

I have a problem which I try to figure out since many hours now.
I must save a model with manual set id in the database using CrudRepository and Hibernate.
But the manual set of the id is ignored always.
Is it somehow possible, to force
CrudRepository.save(Model m)
to persist the given Model with UPDATE?
The queries always results in INSERT statements, without using the id.
The reason I must do this manually is, that the identifier is not the database ID - it is a ID generated outside as UUID which is unique over multiple databases with this model-entry. This model is shared as serialized objects via hazelcast-cluster.
Following an example:
The database already contains a Model-Entry with the id 1:
id identifier_field_with_unique_constraint a_changing_number
1 THIS_IS_THE_UNIQUE_STRING 10
Now I need to update it. I create a new Model version
Model m = new Model();
m.setIdentifierFieldWithUniqueConstraint(THIS_IS_THE_UNIQUE_STRING);
m.setAChangingNumberField(20);
saveMe(m);
void saveMe(Model m) {
Optional<Model> presentModalOpt = modelCrudRepo.findByIdentField(THIS_IS_THE_UNIQUE_STRING)
if(presentModalOpt.isPresent()) {
// The unique value in my identifier field exists in the database already
// so use that id for the new model, so it will be overwritten
m.setId(modalOpt.get().getId());
} else {
m.setId(null);
}
// This call will now do an INSERT, instead of UPDATE,
// even though the id is set in the model AND the id exists in the database!
modelCrudRepo.save(m);
// ConstraintViolationException for the unique identifier field.
// It would be a duplicate now, which is not allowed, because it uses INSERT instead of UPDATE
}
The id Field is tagged with #Id and #GeneratedValue annotation (for the case that the id is null and the id should be generated)
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
I even tried to changed this field only to an #Id field without #GeneratedValue and generate the ID always on my own. It had no effect, it always used INSERT statements, never UPDATE.
What am I doing wrong?
Is there another identifier for the CrudRepository that declares the model as an existing one, other than the id?
I'm happy for any help.
CrudRepository has only save method but it acts for both insert as well as update.
When you do save on entity with empty id it will do a save.
When you do save on entity with existing id it will do an update
that means that after you used findById for example and changed
something in your object, you can call save on this object and it
will actually do an update because after findById you get an object
with populated id that exist in your DB.
In your case you are fetching the records based on a field (unique) But records will update only when the model object has a existing primary key value
In your code there should be presentModalOpt instead of modalOpt
void saveMe(Model m) {
Optional<Model> presentModalOpt = modelCrudRepo.findByIdentField(THIS_IS_THE_UNIQUE_STRING)
if(presentModalOpt.isPresent()) { // should be presentModalOpt instead of modalOpt
} else {
m.setId(null);
}
modelCrudRepo.save(m);
}
See the default implementation -
/*
* (non-Javadoc)
* #see org.springframework.data.repository.CrudRepository#save(java.lang.Object)
*/
#Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}

Spring Security PermissionEvaluator: how to implement hasPermission method with object ID?

Spring's Security "hasPermission" method has an implementation, which (as I get) is intended for passing class name (targetType) and Object Id (Serializable).
So could you please explain (at least in general) how to do this implementation right?
I've searched for example of passing object ID and found no any (even at Spring's doc).
In my situation I want to check for User's DELETE permission on some of my classes (for instance, "Goal"). All of these classes has universal methods and fields, so I can have universal logic for checking permission inside a PermissionEvaluator.
For doing this I'm intended to pass an Object's ID and Object's class name into PermissionEvaluator and do the check here like this:
#PreAuthorize("hasPermission(#id, 'Goal','DELETE')")
It sounds pretty good till it not comes to the implementation, because I don't really understand how can I get Object's instance by Class name and Id inside Permission evaluator.
#Component
public class CustomPermissionEvaluator implements PermissionEvaluator
#Override
public boolean hasPermission(Authentication authentication, Serializable serializable, String targetType,
Object permission) {
Yes, I can instantiate object by Class.forName(targetType), but how can I get it's instance by Id (serializable) from appropriate Repository then? (I have different repository for every object).
#Autowiring all of my 30 repositories would be the madness.
Implemented my service, which takes Object ID and Object Type and then sends back Object, which I can later unbox. I used dynamic HQL, so no need in 30+ JPA repositories autowiring (my bad, I missed this possibility at the beginning).
#PersistenceContext
EntityManager entityManager;
static String entityClassPath="com.platform.entity.";
public Object getEntity(String className, Long id) {
String classToQuery = capitalize(className);
/* Check if Entity class exists to decide whether to query DB or not */
try {
Class cls = Class.forName(entityClassPath + className);
} catch (Exception e) {
return null;
}
/* Query DB if Entity class exist */
Query query;
try {
query = entityManager.createQuery("SELECT Q FROM " + classToQuery + " Q WHERE Q.id=?1");
query.setParameter(1, id);
return query.getSingleResult();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

Why does not delete method of spring data (JPA) have any return values?

I have used a delete method of Spring Data JPA in the service layer, but I wonder why neither the deleteById method nor delete method has any return values.
If we inspect the implementation of the delete method carefully, there is an if statement that when the entity to be deleted doesn't exist returns nothing.
public void delete(T entity) {
Assert.notNull(entity, "Entity must not be null!");
if (entityInformation.isNew(entity)) {
return;
}
Class<?> type = ProxyUtils.getUserClass(entity);
T existing = (T) em.find(type, entityInformation.getId(entity));
// if the entity to be deleted doesn't exist, delete is a NOOP
if (existing == null) {
return;
}
em.remove(em.contains(entity) ? entity : em.merge(entity));
}
Personally, I think returning a Boolean value could be an adequate approach in this case because the controller layer will know about the deletion status, and the view layer can be provided with the far more reliable alert message.
Spring Data JPA design some build-in methods that way they think and give us the option to use the other way also.
You can easily get deleted records and their count using derived delete query supported By Spring Data JPA (Reference)
#Repository
public interface FruitRepository extends JpaRepository<Fruit, Long> {
Fruit deleteById(Long id); // To get deleted record
}
#Repository
public interface FruitRepository extends JpaRepository<Fruit, Long> {
Long deleteById(Long id); // To get deleted record count
}
use #Modifying and #Query ant it will return number of deleted rows.
#Repository
public interface FruitRepository extends JpaRepository<Fruit, Long> {
#Modifying
#Query(value = "DELETE FROM Fruit f where f.id = ?1")
int costumDeleteById(Long id);
}
Another option would be to follow the suggestion from this answer and check if the number of affected entities is 1 (in case of a deleteById method).

Entity must be managed to call remove?

public Person deletePerson(Person entity) {
EntityManager ems = emf.createEntityManager();
try {
ems.getTransaction().begin();
ems.merge(entity);
ems.remove(entity);
ems.getTransaction().commit();
} finally {
ems.close();
}
return entity;
}
it doesnt work I don't know why? Gives me java.lang.IllegalArgumentException
It doesn't work because remove operation requires managed entity to be passed to it. You could modify your code like this to make it work:
entity = ems.merge(entity);
ems.remove(entity);
Because merge returns managed entity instance, you can call remove with the object it returns, because it is managed by JPA (the object you pass to merge is not affected, which is why your code fails).

JPA ID Generation Strategy

I defined a generator for a JPA class:
<sequence-generator name="MY_SEQ" allocation-size="-1"
sequence-name="MY_SEQ"
initial-value="100000000" />
There are cases where I already have an ID for an entity but when I insert the Entity the Id gets generated using the generator.
Is it possible to define a generator that will only generate an Id when one does not exist?
I am using Hibernate as a JPA Provider.
Thank you
I couldn't find a way to do this in JPA so I used Hibernate EJB3 event listeners. I over rode the saveWithGeneratedId to use reflection to check the entity for an #Id annotation and then to check that field for a value. If it has a value then I call saveWithRequestedId instead. Other wise I let it generate the Id. This worked well because I can still use the sequence for Hibernate that is set up if I need an Id. The reflection might add overhead so I might change it a little. I was thinking of having a getId() or getPK() method in all entities so I don't have to search for which field is the #Id.
Before I used reflection I tried calling session.getIdentifier(entity) to check but I was getting TransientObjectException( "The instance was not associated with this session" ). I couldn;t figure out how to get the Entity into the session without saving it first so I gave up. Below is the listener code I wrote.
public class MergeListener extends org.hibernate.ejb.event.EJB3MergeEventListener
{
#Override
protected Serializable saveWithGeneratedId(Object entity, String entityName, Object anything, EventSource source, boolean requiresImmediateIdAccess) {
Integer id = null;
Field[] declaredFields = entity.getClass().getDeclaredFields();
for (Field field : declaredFields) {
Id annotation = field.getAnnotation(javax.persistence.Id.class);
if(annotation!=null) {
try {
Method method = entity.getClass().getMethod("get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1));
Object invoke = method.invoke(entity);
id = (Integer)invoke;
} catch (Exception ex) {
//something failed (method not found..etc) , keep going anyway
}
break;
}
}
if(id == null ||
id == 0) {
return super.saveWithGeneratedId(entity, entityName, anything, source, requiresImmediateIdAccess);
} else {
return super.saveWithRequestedId(entity, id, entityName, anything, source);
}
}
}
I then had to add the listener to my persistence.xml
<property name="hibernate.ejb.event.merge" value="my.package.MergeListener"/>
it's not a good Idea, sequences are used for surrogate keys, are meaningless in the business sense but assures you, there won't be duplicates thus no error at inserting time.

Categories