JPA - update before remove in one transaction not working - java

I have 2 tables in database:
ads - represent user defined advertisements
ad_categories - represent categories for advertisements
every advertisement must belong to exactly one category, so in ads table I defined a foreign key pointing to ad_categories with ON UPDATE NO ACTION ON DELETE NO ACTION.
In my application, user must be able to delete any category, but if that category contains advertisements, they must be moved to another category before category is deleted.
em.getTransaction().begin();
// get currentNode
AdCategories currentNode = em.find(AdCategories.class, currentNodeId);
// get ads
List<Ads> resultList = em.createQuery("SELECT a from Ads a WHERE a.adCategoryId = :categoryId").setParameter("categoryId", currentNode).getResultList();
// get their new location
AdCategories newLocation = em.find(AdCategories.class, newLocationId);
// set their new location
for(Ads a: resultList)
a.setAdCategoryId(newLocation);
em.remove(currentNode);
em.getTransaction().commit();
I expected, that affected advertisements will have ad_category_id changed and then the empty category will be removed. But affected advertisements are deleted too!!
I enabled logging in EclipseLink to FINEST level and found out, that when transaction is commited, firstly, UPDATE query is sent to database, which changes ad_category_id for affected advertisements and then category is deleted, but delete is cascaed to advertisements! I dont understand why, because advertisements should have updated ad_category_ids before remove occours.
I know, one simple workaround is to call em.flush() before removing the category, but I dont think it is optimal solution. I think, I need to understand this behaviour.
I am using EclipseLink with NetBeans and PostgreSQL.
Table definitions:
AdCategories
#Entity
#Table(name = "ad_categories")
#XmlRootElement
#NamedQueries({
#NamedQuery(name = "AdCategories.findAll", query = "SELECT a FROM AdCategories a"),
#NamedQuery(name = "AdCategories.findById", query = "SELECT a FROM AdCategories a WHERE a.id = :id"),
#NamedQuery(name = "AdCategories.findByParentId", query = "SELECT a FROM AdCategories a WHERE a.parentId = :parentId"),
#NamedQuery(name = "AdCategories.findByCategoryOrder", query = "SELECT a FROM AdCategories a WHERE a.categoryOrder = :categoryOrder"),
#NamedQuery(name = "AdCategories.findByCategoryDepth", query = "SELECT a FROM AdCategories a WHERE a.categoryDepth = :categoryDepth"),
#NamedQuery(name = "AdCategories.findByName", query = "SELECT a FROM AdCategories a WHERE a.name = :name"),
#NamedQuery(name = "AdCategories.findByGrandParentId", query = "SELECT a FROM AdCategories a WHERE a.grandParentId = :grandParentId"),
#NamedQuery(name = "AdCategories.findByParentName", query = "SELECT a FROM AdCategories a WHERE a.parentName = :parentName"),
#NamedQuery(name = "AdCategories.findByGrandParentName", query = "SELECT a FROM AdCategories a WHERE a.grandParentName = :grandParentName")})
public class AdCategories implements Serializable {
#OneToMany(cascade = CascadeType.ALL, mappedBy = "adCategoryId")
private Collection<Ads> adsCollection;
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "id")
private Integer id;
#Basic(optional = false)
#Column(name = "parent_id")
private int parentId;
#Basic(optional = false)
#Column(name = "category_order")
private short categoryOrder;
#Basic(optional = false)
#Column(name = "category_depth")
private short categoryDepth;
#Basic(optional = false)
#Column(name = "name")
private String name;
#Column(name = "grand_parent_id")
private Integer grandParentId;
#Column(name = "parent_name")
private String parentName;
#Column(name = "grand_parent_name")
private String grandParentName;
...
Ads
#Entity
#Table(name = "ads")
#XmlRootElement
#NamedQueries({
#NamedQuery(name = "Ads.findAll", query = "SELECT a FROM Ads a"),
#NamedQuery(name = "Ads.findByAdId", query = "SELECT a FROM Ads a WHERE a.adId = :adId"),
#NamedQuery(name = "Ads.findByName", query = "SELECT a FROM Ads a WHERE a.name = :name"),
#NamedQuery(name = "Ads.findByDescriptionShort", query = "SELECT a FROM Ads a WHERE a.descriptionShort = :descriptionShort"),
#NamedQuery(name = "Ads.findByDescriptionLong", query = "SELECT a FROM Ads a WHERE a.descriptionLong = :descriptionLong")})
public class Ads implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "ad_id")
private Integer adId;
#Basic(optional = false)
#Column(name = "name")
private String name;
#Basic(optional = false)
#Column(name = "description_short")
private String descriptionShort;
#Basic(optional = false)
#Column(name = "description_long")
private String descriptionLong;
#JoinColumn(name = "ad_category_id", referencedColumnName = "id")
#ManyToOne(optional = false)
private AdCategories adCategoryId;
...

The problem here is that you defined a bi-directional relationship, which needs to be manually managed (The JPA provider will not do it for you). In your calling code, you break the link between ads and their category, from the point of view of the ads.
for(Ads a: resultList)
a.setAdCategoryId(newLocation);
But, your category is still holding on to a collection of ads that it believes its related too, and when you delete it, those ads get deleted as well (because of the CascadeType.ALL annotation). There are two ways you can go about fixing this.
Keep the bidirectional relationship
If you really need to, you can leave the relationship bidirectional, but then you would have to properly disassociate the relationship on both sides, when you want to break it. It's normal to manage the relationship entirely from the 'owning' side, so I would do something like this:
public class Ads implements Serializable {
public void setAdCategoryId(AdCategories category) {
this.category.removeAd(this);
this.category = category;
this.category.addAd(this);
}
}
Very rough pseudocode, you will need to flesh it out
Remove the birectional relationship
Does a category really need to maintain a list of all ads that use it? Conceptually, I don't think it should. The list will get very large over time, and you could always query for it dynamically instead of storing it with each category. But that's a decision you have to make from a business point of view.

If you declare a cascade of type REMOVE (or ALL) on the collection of ads in AdCategory, you tell JPA: when I call remove() on an AdCategory, also call remove() on all the ads in this collection. So that's what JPA does.
You have a bidirectional association, it's your responsibility to make sure both sides of the association are in a coherent state. So if you change the category of an ad, you should also remove this ad from the set of ads in its category, and you should also add the ad to its new category. It's not absolutely mandatory in all the cases, but in yours, it is.
Also, your naming is really bad. An instance of AdCategories is a single category. So the entity should be named AdCategory. Same for Ads, which should be named Ad. The field adCategoryId doesn't contain a category ID, but a category. It should be named adCategory of category and not adCategoryId. Why name the field adId? It's the ID in the class Ad, so it's already obviously the ID of an Ad. It should thus be named id. descriptionLong should be named longDescription. That might seem like details, but those are the details that make code look good and be readable.

Related

I'm receiving one object using findAllBy in springBoot

I'm trying to fetch all rows that have the same patient_id, so I'm doing findAllByPatientId. But I'm always receiving one object in the Listinstead of all the rows.
#Entity
#Getter
#Setter
public class MedicalHistory extends BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#ManyToOne
#JoinColumn(name = "operator_id")
private MedicalOperator medicalOperatorId;
#ManyToOne
#JoinColumn(name = "illness_id")
private Illness illnessId;
#ManyToOne
#JoinColumn(name= "patientId")
private Patient patientId;
}
public List<MedicalHistory> getPatientMedicalRecords(PatientDto patientDto) {
Optional<Patient> getPatient = patientRepository.findByNin(patientDto.getNin());
Long patientId = getPatient.get().getPatientId();
return medicalHistoryRepository.findAllByPatientId(patientId);
}
I want to receive multiple rows using the patient_id but instead, I'm always getting one !!.
I tried native query and hibernate but nothing is working.
public interface MedicalHistoryRepository extends JpaRepository<MedicalHistory, Long> {
// List<MedicalHistory> findAllByPatientId(Long id);
ArrayList<MedicalHistory> findMedicalHistoriesByPatientId(Long id);
#Query(value = "SELECT * FROM medical_history WHERE patient_id = id",nativeQuery = true)
List<MedicalHistory> findAllByPatientId(Long id);
}
Now you are requesting "give me medical_history where id = patient_id" and getting only one result row.
You need to add a colon to the query to set a parameter to fix a result
value = "SELECT * FROM medical_history WHERE patient_id = :id"
Look for JPQL, it's java persistancy query language and spring is automatically turning your #query into it. Spring is also supporting spEL you can also have a look to it here : https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query.spel-expressions where you can see than you can grab your parameters ever with ?number or with :name or putting #Param("name") into your parameters definition. As said before there is multiple ways to receive a parameter in you query but certainly not like you previously did.
That's why you don't receive anything.

hibernate #Loader annotation on #oneToOne relation

I'm trying to implement a custom #loader using a namedQuery on a OneToOne - Relation of an entity.
However the lastDatalog field remains null at all given times
I've tested the named query befor on a simple integration test using a repositry, the result was exactly what I intend to have in the lastDestinationStatus
(I need the last updated record from the logs for this data and IREF combination)
when I query the Datalog entity with the id of the data I get the correct result so the Datalog entity seems to be persisted
maybe good to know : curent hibernate version on the project is 4.2.11.Final
this is en extract from entity 1
#Entity
#Table(name = "data")
#NamedQueries({
#NamedQuery(name = "LastLogQuery", query = "select log from DataLog log where log.data.id= ?1 and " +
"log.IREF = (select max(log2.IREF) from DataLog log2 where log2.data = log.data ) " +
"and log.tsUpdate = (select max(log3.tsUpdate) from DataLog log3 where log3.data = log.data and log3.IREF = log.IREF)")})
public class Data{
....
#OneToOne(targetEntity = DataLog.class)
#Loader(namedQuery = "LastLogQuery")
private DataLog lastDataLog;
}
extract from entity 2
#Entity
#Table(name ="log")
public class DataLog{
.......
#ManyToOne(fetch = FetchType.EAGER)
#org.hibernate.annotations.Fetch(value = org.hibernate.annotations.FetchMode.SELECT)
#JoinColumn(name = "DTA_IDN", nullable = false)
private Data data;
/** IREF */
#Column(name = "DSE_LOG_UID_FIL_REF_COD")
private String IREF;
#Column(name = "LST_UPD_TMS", nullable = false)
#Temporal(TemporalType.TIMESTAMP)
private Date tsUpdate;
}

How to ignore a #SQLDelete annotation in certain cases

I want to implement soft deletion, but still be able to delete permanently. Is there any way to ignore a declared #SQLDelete() annotation, or maybe say:
#SQLDelete("IF expression THEN UPDATE statement ELSE delete statement")
EDIT:
This is the entity in question.
#Indexed
#Entity
#DynamicUpdate
#FilterDefs({
#FilterDef(name = "tenantFilter", parameters = {#ParamDef(name = "tenantId", type = "string")}),
#FilterDef(name = "deleteFilter")
})
#Filters({
#Filter(name = "tenantFilter", condition = "tenant_id = :tenantId"),
#Filter(name = "deleteFilter", condition = "deleted = false")
})
#SQLDelete(sql ="UPDATE Antwort SET deleted = true, date_modified = NOW() WHERE ID = ?; DELETE FROM Antwort WHERE deleted = true AND kundenentwurf_id IS NOT NULL", check = ResultCheckStyle.NONE)
public class Antwort implements TenantSupport, ISoftDeleteModel{
#Id
#Column(name = "ID")
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String tenantId;
#Column(nullable = false)
private Boolean deleted;
private Date dateModified;
#ManyToOne(fetch = FetchType.LAZY)
private Organisation organisation;
#ManyToOne(fetch = FetchType.LAZY)
private Projekt projekt;
#ManyToOne(fetch = FetchType.LAZY)
private Kundenentwurf kundenentwurf;
private Integer nummer;
private String frage;
private String antwort;
//getters, setters and contructors...
}
I explained this in more details in one of my Hibernate Tips. Here is the short version of it:
When you call the remove method on your EntityManager, Hibernate will execute the SQL statement defined in the #SQLDelete operation.
You can’t deactivate the #SQLDelete annotation. So, if you want to remove the record from the database permanently, you can’t use the remove method of your EntityManager. You need to execute a SQL DELETE statement using a JPQL, Criteria or native query.
Here is an example of a JPQL DELETE statement:
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
// do something ...
// add this if you fetched the Book entity in this session
em.flush();
em.clear();
Query query = em.createQuery("DELETE Book b WHERE id = :id");
query.setParameter("id", 1L);
query.executeUpdate();
em.getTransaction().commit();
em.close();
In case you fetched the Book entity you want to remove within your current Hibernate Session, you need to call the flush and clear methods on your EntityManager before you execute the DELETE statement. This ensures that all pending changes are written to the database before you remove the record.

Hibernate/JPA JPQL to wrong SQL when querying Map<String,String> field

This is my Entity configuration
#Entity
#NamedQuery(name = "Payment.findByEmail", query = "SELECT p FROM Payment p JOIN p.additionalAuthData a " +
"WHERE KEY(a) = 'email' AND VALUE(a) = ?1 AND (p.paymentType = 4 OR p.paymentType = 10)")
public class Payment {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
#Column(name = "payment_type")
private Integer paymentType;
/** other properties, getters and setters */
#ElementCollection
#CollectionTable(name = "additional_auth_data")
#MapKeyJoinColumn(name = "id", referencedColumnName = "id")
#MapKeyColumn(name = "field")
#Column(name = "data_value")
private Map<String, String> additionalAuthData;
}
The NamedQuery findByEmail("test#example.com") generates the following SQL
select -- all fields ...
from payment payment0_ inner join additional_auth_data additional1_ on payment0_.id=additional1_.id
where
additional1_.field='email' and (select additional1_.data_value from additional_auth_data additional1_ where payment0_.id=additional1_.id)='test#example.com' and (payment0_.payment_type=4 or payment0_.payment_type=10)
which is wrong: it may work if you have only one row but it blows up otherwise. H2 complains Scalar subquery contains more than one row and PostgreSQL more than one row returned by a subquery used as an expression. In fact, query's where condition compares a scalar value ('test#example.com') with a subquery.
The correct SQL should be:
select -- all fields
from payment payment0_ inner join additional_auth_data additional1_ on payment0_.id=additional1_.id
where additional1_.field='payerEmail' and additional1_.data_value='test#example.com' and (payment0_.payment_type=4 or payment0_.payment_type=10)
Is the HSQL correct? Is there a way to instruct Hibernate to generates a clever, better SQL? Is this a Hibernate bug?
Note: Hibernate shipped with Spring Boot Starter 1.3.7.RELEASE
Edit:
Using an #Embeddable class
#ElementCollection
#JoinTable(name = "additional_auth_data", joinColumns = #JoinColumn(name = "id"))
#MapKeyColumn(name = "field")
#Column(name = "data_value")
private Set<AdditionalData> additionalAuthData;
#Embeddable
public static class AdditionalData {
#Column(name = "field", nullable = false)
private String field;
#Column(name = "data_value")
private String dataValue;
protected AdditionalData() {
}
public AdditionalData(String field, String dataValue) {
this.field = field;
this.dataValue = dataValue;
}
/** Getters, setters; equals and hashCode on "field" */
}
#NamedQuery(name = "Payment.findByEmail", query = "SELECT p FROM Payment p JOIN p.additionalAuthData a " +
"WHERE a.field = 'email' AND a.dataValue = ?1 AND (p.paymentType = 4 OR p.paymentType = 10)")
solves the problem, and the SQL is correct, but it looks just plain wrong, like shooting a fly with a bazooka...
It generates correct SQL without value().
Use just a=?1
But I would expect is should generate it simple also with it.

why i can't persist object by openjpa? - incorrect validation

In my web applicaton I use OpenJPA on Apache Tomcat (TomEE)/7.0.37 server. I use Netbeans to auto generate class ("Entity Class from database..." and "Session Beans From Entity Class...").
my User.class:
#Entity
#Table(name = "USER")
#XmlRootElement
#NamedQueries({
#NamedQuery(name = "User.findAll", query = "SELECT u FROM User u"),
#NamedQuery(name = "User.findByIdUser", query = "SELECT u FROM User u WHERE u.idUser = :idUser"),
#NamedQuery(name = "User.findByLogin", query = "SELECT u FROM User u WHERE u.login = :login"),
#NamedQuery(name = "User.findByPassword", query = "SELECT u FROM User u WHERE u.password = :password")})
public class User implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#NotNull
#Column(name = "id_user")
private Short idUser;
#Size(max = 8)
#Column(name = "login")
private String login;
#Size(max = 64)
#Column(name = "password")
private String password;
#JoinTable(name = "USER_has_ROLES", joinColumns = {
#JoinColumn(name = "USER_id", referencedColumnName = "id_user")}, inverseJoinColumns = {
#JoinColumn(name = "ROLES_id", referencedColumnName = "id_roles")})
#ManyToMany
private List<Roles> rolesList;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "user")
private List<Lecturer> lecturerList;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "user")
private List<Student> studentList;
//constructors, getters, setters
}
when I create new user by ManagedBean:
private void addUser() {
User user = new User();
user.setLogin(registerLog);
user.setPassword(registerPass);
Roles r = new Roles();
r.setIdRoles(new Short("2"));
List<Roles> roleList = new ArrayList<Roles>();
roleList.add(r);
user.setRolesList(roleList);
userFacade.create(user); //<------here i create by abstract facade by em.persist(user);
}
i get exception:
javax.el.ELException: javax.ejb.EJBException: The bean encountered a non-application exception; nested exception is: javax.validation.ConstraintViolationException: A validation constraint failure occurred for class "model.entity.User".
viewId=/pages/register.xhtml
location=/home/jakub/Projekty/Collv2/build/web/pages/register.xhtml
phaseId=INVOKE_APPLICATION(5)
Caused by:
javax.validation.ConstraintViolationException - A validation constraint failure occurred for class "model.entity.User".
at org.apache.openjpa.persistence.validation.ValidatorImpl.validate(ValidatorImpl.java:282)
/pages/register.xhtml at line 26 and column 104 action="#{registerController.register}"
it's look like I my user id is not correct. What is wrong ?
I think your problem is your id generation type - GenerationType.IDENTITY. Usually when using identity a special database column is used to generate the id. The id is not generated until the data is inserted into the database and the id itself is not available to the entity until after commit. However, Bean Validation occurs on the pre-persist callback using the current state of the entity. This will fail, because the id is still null.
I probably would just change the generation type.
I won't do this:
Roles r = new Roles();
r.setIdRoles(new Short("2"));
List<Roles> roleList = new ArrayList<Roles>();
roleList.add(r);
This basically is a misused, if this Role is exist and able to link to User you're creating, you should retrieve it and set; otherwise, if you want to create a new Role, don't set #Id for it, or it will cause error as OpenJPA will look for an existing item instead of creating new one.
And, yes, it's possible the cause of your error. The fix is retrieve Role or just don't set #Id attribute. Plus, .setIdRoles() naming is quite strange naming to me.

Categories

Resources