Hibernate Filter being ignored - postgresql

My application uses Hibernate 5.02 and Wildfly 10 with a PostgreSQL 9.5 database. I'm trying to enable a filter on a #OneToMany collection held within an entity that is constructed via a NamedQuery. Unfortunately, it seems as if the filter is just ignored. Here are the different components, redacted for ease of reading.
#NamedNativeQueries({
#NamedNativeQuery(
name = "getAnalystProcess",
query = "SELECT * FROM analysis.analystprocess WHERE id = :processId",
resultClass = AnalystProcessEntity.class
)})
#FilterDef(
name = "analystProcessUnanalyzedMsgsFilter",
parameters = { #ParamDef(name = "processIds", type = "integer"), #ParamDef(name = "analystIds", type = "integer") })
#Filter(name = "analystProcessUnanalyzedMsgsFilter", condition = "analystprocess_id IN (:processIds) AND id NOT IN (SELECT msg_id FROM analysis.analyzedmsg WHERE analyst_id IN (:analystIds) AND analystprocess_id IN (:processIds)) ORDER BY process_msg_id")
#Entity
#Table(name = "analystprocess", schema = "analyst")
public class AnalystProcessEntity implements JPAEntity {
public static final String GET_PROCESS = "getAnalystProcess";
public static final String MSG_FILTER = "analystProcessUnanalyzedMsgsFilter";
public static final String MSG_FILTER_PROC_ID_PARAM = "processIds";
public static final String MSG_FILTER_ANALYST_ID_PARAM = "analystIds";
private static final long serialVersionUID = 1L;
...
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "process")
#OrderColumn(name = "process_msg_id")
#LazyCollection(LazyCollectionOption.EXTRA)
private List<MsgEntity> msgList;
#Entity
#Table(name = "msg", schema = "analyst")
public class MsgEntity implements JPAEntity {
...
#ManyToOne(cascade = CascadeType.ALL, optional = false)
#JoinColumn(name = "analystprocess_id", referencedColumnName = "id")
private AnalystProcessEntity process;
#Column(name = "process_msg_id")
private Integer processMsgId;
private void buildAnalystProcess() {
LOG.info("Building AnalystProcessEntity");
analystUser.getJdbcSession().enableFilter(AnalystProcessEntity.MSG_FILTER)
.setParameter(AnalystProcessEntity.MSG_FILTER_PROC_ID_PARAM, analystProcessId)
.setParameter(AnalystProcessEntity.MSG_FILTER_ANALYST_ID_PARAM, analystUser.getId());
Query query = analystUser.getJdbcSession().getNamedQuery(AnalystProcessEntity.GET_PROCESS)
.setParameter("processId", analystProcessId);
// Query query = analystUser.getJdbcSession().createNativeQuery("SELECT * FROM analysis.analystprocess WHERE id = :processId")
// .setParameter("processId", analystProcessId)
// .addEntity(AnalystProcessEntity.class);
analystProcess = (AnalystProcessEntity) query.getSingleResult();
CREATE TABLE analysis.analystprocess (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_date TIMESTAMP NOT NULL DEFAULT now(),
...
);
CREATE TABLE analysis.msg (
id SERIAL PRIMARY KEY,
analystprocess_id INTEGER NOT NULL REFERENCES analysis.analystprocess(id) ON DELETE CASCADE ON UPDATE CASCADE,
process_msg_id INTEGER NOT NULL,
constraint tbl_statusid_analystprocessid unique(status_id, analystprocess_id)
);
As seen above, I have also tried the filter on constructing the AnalystProcessEntity class via createNativeQuery instead of getNamedQuery and no luck.
I also added a defaultCondition with hardcoded values into the #FilterDef just to see if it would execute the default condition and it still didn't.
I've tried the #Filter above the entity definition as well as above the class definition. I even came across a blog post which made it sound like the condition references entity fields (variable names) and not table fields (column names). Trying to stick to Java naming conventions in the Entity and Postgres naming conventions in the table, so I tried switching the references in the condition and to no avail.
I have sql logging turned on in Hibernate and the condition doesn't show up anywhere, as if it's just simply being ignored.
Any help would be greatly appreciated!

So, the problem was that I had the #FilterDef applied to the wrong class. It was my presumption that because I was constructing the AnalystProcessEntity which holds the MsgEntity collection (which I am trying to filter), that the #FilterDef would be applied to the AnalystProcessEntity class. Instead, it needs to be applied to the entity that it's actually filtering (hindsight being 20/20, that's pretty obvious).
Also, the actual condition needed to be modified to use complete references within the sub-select query.
I hope this helps someone at some point...
#NamedNativeQueries({
#NamedNativeQuery(
name = "getAnalystProcess",
query = "SELECT * FROM analysis.analystprocess WHERE id = :processId",
resultClass = AnalystProcessEntity.class
)})
#Filter(name = "analystProcessUnanalyzedMsgsFilter", condition = "id NOT IN (SELECT amsg.msg_id FROM analysis.analyzedmsg amsg WHERE amsg.analyst_id IN (:analystIds) AND amsg.analystprocess_id IN (:processIds))")
#Entity
#Table(name = "analystprocess", schema = "analyst")
public class AnalystProcessEntity implements JPAEntity {
public static final String GET_PROCESS = "getAnalystProcess";
public static final String MSG_FILTER = "analystProcessUnanalyzedMsgsFilter";
public static final String MSG_FILTER_PROC_ID_PARAM = "processIds";
public static final String MSG_FILTER_ANALYST_ID_PARAM = "analystIds";
private static final long serialVersionUID = 1L;
...
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "process")
#OrderColumn(name = "process_msg_id")
#LazyCollection(LazyCollectionOption.EXTRA)
private List<MsgEntity> msgList;
#FilterDef(
name = "analystProcessUnanalyzedMsgsFilter",
parameters = { #ParamDef(name = "processIds", type = "integer"), #ParamDef(name = "analystIds", type = "integer") })
#Entity
#Table(name = "msg", schema = "analyst")
public class MsgEntity implements JPAEntity {
...
#ManyToOne(cascade = CascadeType.ALL, optional = false)
#JoinColumn(name = "analystprocess_id", referencedColumnName = "id")
private AnalystProcessEntity process;
#Column(name = "process_msg_id")
private Integer processMsgId;
Additionally, I ran into another problem with null's appearing in the collection, despite the fact that I am using an #OrderColumn, which I thought fixed that issue. It seems that with the use of the #Filter, null's are inserted in place of what ended up being filtered OUT (excluded).

Related

JPA repository retrieve custom primary key value after save

I have an entity class pointing to postgresql table. Below is table structure. The paymentreferencenumber is the PK which is populated by a trigger. id field is the sequence generated field. When i try to save in this table using JPARepository save method it inserts the first record. But after that it fails due to the primary key constraint. Since PK is a string type and generated using trigger I am specifying generator strategy as 'select'. Can anyone help me with this blocker and point me in the right direction. Thanks
Table structure --
custId serial not null,
paymentreferencenumber varchar(32) not null
constraint customers1_pkey
primary key,
firstname varchar(255),
lastname varchar(255)
Entity class --
#Entity
#Table(name = "customersnew")
public class Customer implements Serializable {
private static final long serialVersionUID = -1L;
#GeneratedValue(generator = "seq")
#GenericGenerator(name="seq", strategy="sequence", parameters = { #Parameter(name="key", value = "customersnew_custid_seq")})
#Column(name = "custid")
private long id;
#Id
#GeneratedValue(generator = "trigger_generated")
#GenericGenerator(name="trigger_generated", strategy="select", parameters = { #Parameter(name="key", value = "id")})
#Column(name = "paymentreferencenumber")
private String refNum;
#Column(name = "firstname")
private String firstName;
#Column(name = "lastname")
private String lastName;
}
--- Controller using JPA save
#RestController
public class CustomerController {
#Autowired
CustomerRepository repository;
EntityManagerFactory emf;
public CustomerController(CustomerRepository repository, EntityManagerFactory emf) {
this.repository = repository;
this.emf = emf;
}
#PostMapping("/create")
public String create(#RequestBody CustomerUI customer){
// save a single Customer
Customer returnObj = repository.saveAndFlush(new Customer(customer.getFirstName(), customer.getLastName()));
PersistenceUnitUtil util = emf.getPersistenceUnitUtil();
Object retObj = util.getIdentifier(returnObj);
return "Customer is created";
}
If you don't specify an id generation strategy, Hibernate will use GenerationType.AUTO. This will result in any of
AUTO - either identity column, sequence or table depending on the
underlying DB.
If you look here, you'll notice all of those generate ids of type long, short or int, not of type String.
Say you wanted a String UUID as an id, you could use
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "uuid2")
#Column(name = "paymentreferencenumber")
private String refNum;

How to keep data in sync with JPA across entities in session cache?

Im trying to figure out how to keep data in sync in the session cache. I have the following example:
#Table (name = "language")
#Entity
public class Language
{
#Id
#Column (name = "key", unique = true, nullable = false)
private String key;
#Column (name = "name", unique = true, nullable = false)
private String name;
#OneToMany (mappedBy = "language", cascade = { CascadeType.DETACH, CascadeType.REFRESH, CascadeType.REMOVE })
private Set<Translation> translations;
}
#Table (name = "translation")
#Entity
public class Translation
{
#Id
#GeneratedValue (strategy = GenerationType.SEQUENCE)
private Long id;
#ManyToOne(optional = false)
private Language language;
#ManyToOne (optional = false)
#JoinColumn(referencedColumnName = "key")
private TranslatableEntity translatable;
#Column(name = "value")
private String value;
}
#Table (name = "translatable")
#Entity
public class Translatable
{
#Id
#GeneratedValue (strategy = GenerationType.SEQUENCE)
private Long id;
#NotNull
#Size (max = 255)
#Column (name = "key", unique = true, nullable = false)
private String key;
#OneToMany (mappedBy = "translatable", cascade = { CascadeType.DETACH, CascadeType.REFRESH, CascadeType.REMOVE, CascadeType.PERSIST })
#MapKey (name = "language")
private Map<Language, Translation> translations;
}
So basically I do:
// Print current translations for en language
Language language = languageRepository.getOne("en");
printTranslations(language);
// Add new translatable object with translation for english language
Translatable translatable = new Translatable();
translatable.addTranslation("en", "...")
translatableRepository.saveAndFlush(translatable)
// Again print translations for en language
// This still prints outdated information
language = languageRepository.getOne("en");
printTranslations(language);
So my question is how to keep data consistent.
When inserting/removing a new translatable with some translations the translation list in Language instances are not updated in the session cache.
I could not find any satisfactory answer to this. This one came closest: Keeping entity relationship in sync when deleting child in JPA.
Thanks
JPA doesn't maintain the two sides of a bidirectional relationship for you.
And the purpose of the first level cache is that within a transaction entities get loaded only once.
This gives you two options to solve the problem:
maintain both sides of the relationship yourself, for example by implementing the Translatable.add method so that it updates Language.translations and Translation.language
force a reload of language by either evicting it or by closing the session (i.e. the transaction) before calling languageRepository.getOne("en")

Spring projections select collection

I am attempting to have a station projection include a list of associated logos. Below is my domain:
#Table(name = "Station")
public class Station implements Serializable {
#Id
#Column(name = "Id")
private int id;
#OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.LAZY,
mappedBy = "station")
private Set<Logo> logos;
}
The #OneToMany associated logos:
#Table(name = "Logo")
public class Logo {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#Transient
private String fullUrl; // calculated
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "StationId", nullable = false)
private Station station;
}
My repository and query is as follows:
#Query(value = "SELECT s.id AS Id, s.logos As Logos FROM Station s JOIN s.users su WHERE su.username = ?1")
Collection<StationListViewProjection> findStationsByUsername(String username);
My station projection expects the Id and a list of logoProjections
#Projection(name = "StationListViewProjection", types = Station.class)
public interface StationListViewProjection {
int getId();
Set<LogoProjection> getLogos();
}
The logoProjection only needs the url
#Projection(name = "LogoProjection", types = Logo.class)
public interface LogoProjection {
String getFullUrl();
}
When i execute my query I get a strange response:
MySQLSyntaxErrorException: 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_5_0_, . as col_6_0_, stationlog3_.id as id1_20_0_'
If I understand this
#Transient
private String fullUrl; // calculated
correct, your fullUrl gets calculated inside your java code and more important, it doesn't have a matching column in the database. You can't use such field in projections directly. You might be able to use an Open Projection and specify the calculation to obtain the fullUrl using a #Value annotation.

Default Sort on a Spring Data JPA Repository Method with Custom Query and Pageable Parameter

I have the following repository method that works exactly the way I need it to iff the user provides a sort column in the page parameter:
public interface IdentityRepository extends JpaRepository<Identity, String> {
#Query("select distinct ident from Identity ident left outer join ident.authorities authority "
+ "where ("
+ "(:src is null or ident.source = :src) and "
+ "(:org is null or ident.organization = :org) and "
+ "(:auth is null or authority.authority = :auth) and "
+ "(:authSrc is null or authority.authoritySource = :authSrc))")
#RestResource(path="filter")
public Page<Identity> findWithFilter(
#Param("src") String source,
#Param("org") String org,
#Param("auth") Authority auth,
#Param("authSrc") AuthoritySource authSrc,
Pageable page);
...
}
If the caller provides a page count, but not a sort column, they will get back the correct number of results when retrieving all the pages. However, many of the entities will be duplicated, so even though the result count is correct, many expected entities are missing and others are duplicated (or triplicated).
What I'm wondering is if there is a way to provide a default sort column and direction if the user does not specify one. I've learned that #EnableSpringDataWebSupport can help here, but we're not using Spring MVC, so I don't have any controllers to attach the #SortDefaults to. We are using Spring Data Rest though. Also, I've tried changing the method name to findWithFilterOrderByIdAsc, but that did not seem to help. Ran across this issue in the Spring JIRA, which I believe is exactly what I need, but until it's resolved, does anyone know of a work around?
Here's my entity...
#Entity
#Table(name = "identity", indexes = { #Index(columnList = "user_id", unique = true) })
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#Audited
public class Identity implements Serializable, Identifiable<String> {
/**
* The unique identifier for this identity within the IDD application.
*/
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "IDDUidGenerator")
#GenericGenerator(name = "IDDUidGenerator")
private String id;
/**
* The name of the identity provider wherein this identity is originally defined.
*/
#Column(name = "source")
private String source = INTERNAL_SOURCE;
/**
* The unique identifier for this identity within the customer's identity provider.
*/
#NotNull
#Column(name = "user_id", nullable = false, unique = true)
private String userId;
/**
* The roles this identity is authorized to perform.
*/
#OneToMany(fetch = FetchType.EAGER, mappedBy = "identity", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<IdentityAuthority> authorities = new HashSet<>();
...
}
And its sub-entity...
#Entity
#Table(name = "identity_authority")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#Audited
public class IdentityAuthority implements Serializable, Identifiable<Long> {
private static final long serialVersionUID = -5315412946768343445L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#XmlTransient
#JsonIgnore
private Long id;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "identity_id", nullable = false)
#XmlTransient
#JsonIgnore
private Identity identity;
#Enumerated(EnumType.STRING)
#Column(name = "authority", length = 20, nullable = false)
private Authority authority;
#Enumerated(EnumType.STRING)
#Column(name = "authority_source", length = 30, nullable = false)
private AuthoritySource authoritySource;
...
}
Here's the test case I ran to demonstrate the problem...
#Test
public void testPagedRequestsReturnAllResults() {
// Create identities
String source = "One Hundred Identities Generator";
int numIdentities = 100;
int pageSize = 5;
List<Identity> input = new ArrayList<>();
for (int i=0; i<numIdentities; i++) {
Identity identity = new Identity();
identity.setUserId(UUID.randomUUID().toString());
identity.setSource(source);
input.add(identity);
}
// Save identities
List<Identity> output = repository.saveBulk(input);
Set<String> savedIds = collectIds(output, null);
assertThat(savedIds.size()).isEqualTo(numIdentities);
// Test Sorted Find Filter with Paging (THIS PASSES)
Pageable pageRequest = new PageRequest(0, pageSize, new Sort(Direction.ASC, "id"));
Set<String> foundPagedIds = new HashSet<>();
do {
Page<Identity> page = repository.findOrderByIdAsc(source, null, null, null, pageRequest);
List<Identity> foundIdentities = page.getContent();
foundPagedIds = collectIds(foundIdentities, foundPagedIds);
pageRequest = page.nextPageable();
} while (pageRequest != null);
assertThat(foundPagedIds.size()).isEqualTo(numIdentities);
assertThat(foundPagedIds).isEqualTo(savedIds);
// Test Unsorted Find Filter with Paging (THIS FAILS)
pageRequest = new PageRequest(0, pageSize);
foundPagedIds = new HashSet<>();
do {
Page<Identity> page = repository.findOrderByIdAsc(source, null, null, null, pageRequest);
List<Identity> foundIdentities = page.getContent();
foundPagedIds = collectIds(foundIdentities, foundPagedIds);
pageRequest = page.nextPageable();
} while (pageRequest != null);
assertThat(foundPagedIds.size()).isEqualTo(numIdentities);
assertThat(foundPagedIds).isEqualTo(savedIds);
}

JPQL NamedQuery: Access attribute of an #Embeddable class from an #ElementCollection reference

The follwing named query
<named-query name="fix.getByProblem">
<query>
SELECT f
FROM Fix f JOIN f.solved s
WHERE s.id IN :ids
</query>
</named-query>
is supposed to return all fixes that solve at least one of the given problems, but fails with the error message
Exception Description: Error compiling the query [fix.getByProblem]:
SELECT f FROM Fix f JOIN f.solved s WHERE s.id IN :ids
], unknown state or association field [id] of class [ProblemHandle].
The model is as follows: (simplified)
Fix.java
#ElementCollection
#CollectionTable(name = "FIX_SOLVED", schema = SCHEMA_NAME, joinColumns = {#JoinColumn(name = "SOURCE_VERSION", referencedColumnName = "version")})
#AttributeOverrides({ #AttributeOverride(column = #Column(name = "SOLVED_ID", nullable = true), name = "id") })
private Collection<ProblemHandle> solved;
ProblemHandle.java
#Embeddable
#Access(AccessType.PROPERTY)
public class ProblemHandle {
private Long id;
...
}
Problem.java
#Entity(name = Problem.ENTITY_NAME)
#Access(value = AccessType.FIELD)
#Table(name = Problem.TABLE_NAME, schema = Problem.SCHEMA_NAME)
#IdClass(ProblemHandle.class)
public class Problem {
public static final String ENTITY_NAME = "problem";
public static final String SCHEMA_NAME = "X";
public static final String TABLE_NAME = "PROBLEM";
#Id
#Column(name="id", nullable = false)
private Long id;
...
}
How can I achieve that without having to change the pattern, e.g. using handles?
You have #Access(AccessType.PROPERTY), so the name of your attribute comes from your get method, not the variable. What is the name of your get method?
Try removing #Access(AccessType.PROPERTY)
Also, what version are you using? Try using the 2.4 release.