Validation not fired if I just clear an #OneToMany relation - java

I'm facing a problem when updating an Entity using JPA 2.0 and Hibernate (I didn't test with other providers). Here Is my entity (cutted down for brevity):
#Entity
public class CriterioDefinicaoImpFederais implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#SequenceGenerator(name="criterio_definicao_imp_federais", sequenceName="criterio_definicao_imp_federais")
#GeneratedValue(generator="criterio_definicao_imp_federais", strategy=GenerationType.AUTO)
private Long id;
#Column(length=100)
#NotNull
#TextoValido(min=1, max=100)
private String descricao = "";
//Other fields ommited
#NotEmpty
#OneToMany(cascade=CascadeType.ALL, orphanRemoval=true, mappedBy="criterio")
//Bidirectional association
private List<GrupoCriterioImpFederais> grupos = new ArrayList<>();
public Long getId() {
return id;
}
public void addGrupo(GrupoCriterioImpFederais grupo) {
grupo.setCriterio(this);
this.grupos.add(grupo);
}
public void removerGrupo(GrupoCriterioImpFederais grupo) {
grupos.remove(grupo);
grupo.setCriterio(null);
}
//Other methods ommited
}
Supose I try to persist one new CriterioDefinicaoImpFederais instance. Validation works well, including the #NotEmpty on field grupos.
Then I load the instance persisted, clear the grupos list (calling removerGrupo) and try to update (using the JPA 2.0 merge) the instance.
At this point, the validation for grupos (#NotEmpty) is not fired. BUT, if I change another field of CriterioDefinicaoImpFederais (like descricao for example), all validations are fired including the validations for grupos.
Is this the correct behavior? Or what am I missing? Is there a way to fire the validations?
Ps: I've tried to call flush after merge, without success.
Code to load and update objects:
To load I use the following hql:
//This is critRepo.porId
String sql = "select distinct c from CriterioDefinicaoImpFederais c "
+ " join fetch c.licenca "
+ " join fetch c.grupos g "
+ "where "
+ " c.id = :id ";
This is the code executed after the object is persisted:
em.getTransaction().begin();
CriterioDefinicaoImpFederais outro = critRepo.porId(criterio.getId());
em.getTransaction().commit();
em.clear();
outro.removerGrupo(outro.getGrupos().get(0));
outro.removerGrupo(outro.getGrupos().get(0));
em.getTransaction().begin();
//This method calls merge
critRepo.salvar(outro);
em.getTransaction().commit();
Thanks!

I haven't verified this, but it might be because you aren't accessing the value through an setter that adheres to the Java Bean Specification.
Try if you set it to a new empty collection through a normal setter.

Related

JPA DB2 Native Query with Join Column

Here is my situation: I have a PrimeFaces application that is connecting to a DB2 database, and currently I am switching over JPQL queries to native SQL queries, because I need to be able to swap a schema programmatically for the same objects. With different native queries used to fetch from the DB2 database, a different schema can be used. However, I am running into an issue where one of the objects being fetched has a member with a #JoinColumn annotation.
The fetch of the main object seems to work fine, but when it tries to fetch the #JoinColumn member it fails, saying the table cannot be found using what I assume is some default schema. Consider the below classes:
#Entity
#Table(name = "MYOBJ1")
#NamedNativeQueries({
#NamedNativeQuery(name="MyObj1.findAll", query="SELECT * FROM SCHEMA1.MYOBJ1"),
#NamedNativeQuery(name="MyObj1.findAllAlt", query="SELECT * FROM SCHEMA2.MYOBJ1")
})
public class MyObj1 implements Serializable {
private static final long serialVersionUID = 1L;
#Id
private Integer id;
#OneToOne
#JoinColumn(name="obj2id", referencedColumnName = "id", insertable = false, updatable = false)
private MyObj2 myObj2;
// getters and setters
...
}
#Entity
#Table(name = "MYOBJ2")
#NamedNativeQueries({
#NamedNativeQuery(name="MyObj2.findAll", query="SELECT * FROM SCHEMA1.MYOBJ2"),
#NamedNativeQuery(name="MyObj2.findAllAlt", query="SELECT * FROM SCHEMA2.MYOBJ2")
})
public class MyObj2 implements Serializable {
private static final long serialVersionUID = 1L;
#Id
private Integer id;
/// getters and setters
...
}
Running a native query like the below:
public MyObj1 getMyObj1(Integer id) {
Query query = em.createNativeQuery("SELECT * FROM " + ServerUtilities.getSchema() + ".MYOBJ1 WHERE " + ServerUtilities.getSchema() + ".MYOBJ1.id = '" + id + "'", MyObj1.class);
MyObj1 results = new MyObj1();
try {
results = (MyObj1) query.getSingleResult();
} catch (NoResultException e) {
results = null;
}
return results;
}
Yields this exception: java.sql.SQLException: [SQL0204] MYOBJ2 in MYAPPL type *FILE not found.
One option is to remove the #JoinColumn's, just store the foreign key and look up MyObj2 manually with separate SQL statements, but I am wondering if there is a better way to tell JPA which schema to use for #JoinColumn fetch statements at runtime.

Get collections within an Entity when mapping it to DTO using Transformers

I have an Entity called Student
#Entity
#Table(name = "students")
public class Student implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "STUDENT_ID")
private Integer studentId;
#Column(name = "STUDENT_NAME", nullable = false, length = 100)
private String studentName;
#OneToMany(fetch = FetchType.EAGER, mappedBy = "student", cascade = CascadeType.ALL)
private List<Note> studentNotes;
// Some other instance variables that are not relevant to this question
/* Getters and Setters */
}
and an entity called as Note
#Entity
#Table(name = "notes")
public class Note implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "NOTE_ID")
private Integer noteId;
#Column(name = "NOTE_CONTENT")
private String noteText;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "STUDENT_ID")
private Student student;
/* Getters and Setters */
}
As you can see the relationship dictates that a Student can have multiple number of notes.
For displaying some information about the student on a particular page I need only the studentName, count of notes and all the notes.
I created a StudentDTO for that and it looks something like this:
public class StudentDTO {
private Long count;
private String name;
private List<Note> notes;
/* Getters and setters */
}
And I am using the following code to map the Student and Notes returned from the DB to the StudentDTO
private static void testDTO() {
Session session = getSessionFactory().openSession();
String queryString = "SELECT count(n) as count, s.studentName as name, s.studentNotes as notes " +
"from Student s join s.studentNotes n where s.id = 3";
Query query = session.createQuery(queryString);
List<StudentDTO> list = query.setResultTransformer(Transformers.aliasToBean(StudentDTO.class)).list();
for (StudentDTO u : list) {
System.out.println(u.getName());
System.out.println(u.getCount());
System.out.println(u.getNotes().size());
}
}
The above code fails when there are notes fetched in the query but if I remove the notes and get only name and count it works fine.
When notes is included in the query, this is the error that is fired by Hibernate:
select
count(studentnot2_.NOTE_ID) as col_0_0_,
. as col_3_0_,
studentnot3_.NOTE_ID as NOTE_ID1_2_,
studentnot3_.NOTE_CONTENT as NOTE_CON2_2_,
studentnot3_.STUDENT_ID as STUDENT_3_2_
from
students studentx0_
inner join
notes studentnot2_
on studentx0_.STUDENT_ID=studentnot2_.STUDENT_ID
inner join
notes studentnot3_
on studentx0_.STUDENT_ID=studentnot3_.STUDENT_ID
where
studentx0_.STUDENT_ID=3;
And this is the error message that I get:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'as col_3_0_, studentnot3_.NOTE_ID as NOTE_ID1_2_, studentnot3_.NOTE_CONTENT as N' at line 1
Now I can see where the query is wrong but it is generated by Hibernate, not something that I have control on. Is there something that I need to change in my queryString to acheive the result that I need.
I do not want to manually map the results to my DTO, is there a way that I can directly map my studentNotes in Student.java to notes in StudentDTO.java
Looks like this query is wrong. The better way is to get just the student. You can always get collection of notes from a student.
Session session = getSessionFactory().openSession();
String queryString = from Student s where s.studentId = 3;
Query query = session.createQuery(queryString);
Student student = query.getSingleResult();
sysout(student.getNotes().size())
Also, I never retrieved collection this way in SELECT clause; so, not sure but do you really need
join s.studentNotes
in your query? Not sure if my answer is helpful.
Your query is wrong as you would need two joins to also select the count of notes, but that's not even necessary, as you could determine the count by just using the size of the notes collection.
I created Blaze-Persistence Entity Views for exactly that use case. You essentially define DTOs for JPA entities as interfaces and apply them on a query. It supports mapping nested DTOs, collection etc., essentially everything you'd expect and on top of that, it will improve your query performance as it will generate queries fetching just the data that you actually require for the DTOs.
The entity views for your example could look like this
#EntityView(Student.class)
interface StudentDTO {
#Mapping("studentName")
String getName();
#Mapping("studentNotes")
List<NoteDTO> getNotes();
default int getCount() { return getNotes().size(); }
}
#EntityView(Note.class)
interface NoteDTO {
// attributes of Note that you need
#IdMapping Integer getId();
String getNoteText();
}
Querying could look like this
StudentDTO student = entityViewManager.find(entityManager, StudentDTO.class, studentId);

Can't make #Formula attributes work with CriteriaBuilder

I have an entity like the following were I use #Formula to populate clientId from other tables.
#Entity
public class Failure {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public int id;
public String name;
#ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH} )
public PVPlant pvPlant;
#Formula("(SELECT cl.id from failure f " +
"INNER JOIN pvplant p ON f.pv_plant_id = p.id " +
"INNER JOIN company co ON p.company_id = co.id "+
"INNER JOIN client cl ON co.client_id = cl.id "+
"WHERE f.id = id) ")
public Integer clientId;
}
while CrudRepository<Failure,Integer> JPA method getByClientId works fine I am trying to make something more dynamic for filtering using a Map of keys and values with Specification and CriteriaBuilder.
public MySpecifications {
public static Specification<Failure> equalToEachColumn(HashMap<String,Object> map) {
return new Specification<Failure>() {
#Override
public Predicate toPredicate(Root<Failure> root, CriteriaQuery<?> cq, CriteriaBuilder builder) {
return builder.and(root.getModel().getAttributes().stream().map(a ->
{
if (map.containsKey(a.getName())) {
Object val = map.get(a.getName());
return builder.equal(root.<Integer>get(a.getName()), Integer.parseInt(val.toString()));
}
return builder.disjunction();
}
).toArray(Predicate[]::new)
);
}
};
}
}
When I am passing id in the HashMap it works fine but when I have clientId it doesn't send anything back. It is interesting that getAttributes() actually returns clientId but it seems that builder.equal(root.<Integer>get(a.getName()), Integer.parseInt(val.toString())) is false and not true
This is how I am using the Specification:
failureRepository.findAll(Specifications.where(MySpecifications.equalToEachColumn(map)));
Am I missing something?
Thanks in advance!
I wouldn't expect this to work however you could make it work by using a database view as an alternative to #Formula and mapping the entity across the table and view using #SecondaryTable.
//failures_client_vw is a 2 column db view: failure_id, client_id
#Table(name = "failures")
#SecondaryTable(name = "failures_client_vw",
pkJoinColumns = #PrimaryKeyJoinColumn(name = "failure_id"))
#Entity
public class Failure {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public int id;
public String name;
#ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH} )
public PVPlant pvPlant;
#Column(name = "client_id", table = "failures_client_vw")
public Integer clientId;
}
You can then query clientId as you would any other property.
https://en.wikibooks.org/wiki/Java_Persistence/Tables#Multiple_tables
The actual problem was that I was using
builder.disjunction()
in the and() which creates 0=1 false predicates.
When I replaced it with
builder.conjunction() (which creates 1=1 true predicates)
in my code it worked fine. So #Formula properties behave as native ones to the table and it seems there is no need for SecondaryTable and a new View. Apparently in my earlier tests I used an entity that had just an id in its class and when I added clientId it misled me to believe that #Formula properties don't work, while it was the disjunction from the id that broke clientId

How to Map Result Set of Native Query to Variable in Entity

I have two entities, Users and Annotations, and I want to add a set of results from a query on the Annotations table to a transient variable in the Users entity.
Entities:
Users
#Entity
public class Users {
#Id
#GeneratedValue
#Column(name="user_id")
private Long userId;
#Column(name="username", unique=true, nullable=false)
private String username;
#Transient
private Set<Annotation> annotations;
.....
Annotations
#Entity
#Table(name="Annotation")
public class Annotation {
#Id
#GeneratedValue
#Column(name="anno_id")
private Long annoId;
#Column(name="user_id", nullable=false)
private Long userId;
#Enumerated(EnumType.STRING)
#Column(name="access_control", nullable=false)
private Access accessControl;
#Column(name="group_id", nullable=true)
private Long groupId;
.....
So I want the Set<Annotation> annotations variable to hold results from a query on the Annotations table. This can't simply be a one-to-many mapping, because I have to limit the results in a specific way. In fact, the query is this:
SELECT anno_id, a.user_id, timestamp, is_redacted, access_control, a.group_id, vocabulary_id, key_, value, target_type, target_id, root_type, root_id FROM Annotation AS a
LEFT JOIN group_membership g ON g.user_id = ?#{ principal?.getId() }
WHERE a.user_id = :id
AND (a.access_control='PUBLIC'
OR (a.access_control='GROUP' AND a.group_id = g.group_id
OR (a.access_control='PRIVATE' AND g.user_id = a.user_id))
GROUP BY a.anno_id
I think this is possible through SQLResultSetMapping, however, it seems as though the results are always mapped to another, distinct entity. Is it possible to extract the set as collection and store it in the way I want?
You cannot use SQLResultSetMapping in this scenario as the results will only be mapped to be distinct entity. What you can do is execute as native query and then get the result as a list of object array. You can then construct the desired object that you need.
Query query = entityManager
.createNativeQuery("SELECT anno_id, a.user_id FROM Annotation AS a"
+ " LEFT JOIN group_membership g ON g.user_id = ?"
+ " WHERE a.user_id = ?"
+ " AND (a.access_control='PUBLIC'"
+ " OR (a.access_control='GROUP' AND a.group_id = g.group_id)"
+ " OR (a.access_control='PRIVATE' AND g.user_id = a.user_id))"
+ " GROUP BY a.anno_id");
query.setParameter(1, new Long(1));
query.setParameter(2, new Long(1));
List<Object[]> list = query.getResultList();
return list;

Mapping Custom Named/Native Queries to Entities

It's possible mapping custom native/named queries to entities? I have something like this
NamedQueries({
NamedQuery(name = "StateBo.findByCountry", query = "SELECT state FROM StateBo state WHERE state.country.id = ?"),
NamedQuery(name = "StateBo.showIdfindByCountry", query = "SELECT state.id FROM StateBo state WHERE state.country.id = ?")
})
#Table(name = "STATE")
#Entity(name = "StateBo")
public class StateBo extends BaseNamedBo {
private static final long serialVersionUID = 3687061742742506831L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "STATE_ID")
private Long id;
#Column(name = "ISO_CODE")
private String isoCode;
#ManyToOne
#JoinColumn(name = "COUNTRY_ID")
private CountryBo country;
// getters and setters ...
}
I have my method to call Native/Named queries like this.
#Override
public List<E> executeQuery(String queryName, List<Object> criteria) {
TypedQuery<E> query = entityManager.createNamedQuery(queryName, entityClass);
Integer argumentPosition = 1;
if ( (criteria != null) && (criteria.size() > 0) ){
for(Object object : criteria) {
query.setParameter(argumentPosition, object);
argumentPosition++;
}
}
return (List<E>) query.getResultList();
}
When I call the StateBo.findByCountry the result is mapped to StateBo, but if I call StateBo.showIdfindByCountry the result is not mapped to StateBo because I'm only selected on the query the state.id instead of the fields on the table.
I don't want to select all the fields of the STATE table, I only want in this case the state.id, but when I customize my native query, the result is not mapped to StateBo instead of this, the result is a Long type.
My question is, Is possible map to an Entity the result of StateBo.showIdfindByCountry? I case that I have more fields like state.isoCode, is possible map to StateBo, the custom query? or only is possible if I return all the fields from the query, like the first query StateBo.findByCountry
It is possible, but as JB Nizet said - "your collegues will suffer from such a design decision".
Anyway, in order to do that you should create custom constructor in your entity class. This constructor should accept Long argument and assign it to id field of your entity class.
Then you should change your query to include NEW keyword followed by full qualified entity class name as below:
SELECT NEW your.package.StateBo(sb.id)
FROM StateBo sb
WHERE state.country.id = ?
Please note that all entities retreived from database in such a way will not be managed by persistence context.

Categories

Resources