How to query jsonb column using spring data specification? - spring-data

I have below table temp_tbl (postgres):
ID(int) NAME(TEXT) LINKS(jsonb)
-------- ---------- -------------------
1 Name1 ["1","23","3", "32]
2 Name2 ["11","3","31", "2]
3 Name3 ["21","13","3", "12]
Now my native query to get rows which has 'LINKS' with value as "3" is:
select * from temp_tbl where links #> '["3"]'
returns rows 2 & 3.
I want implement this query using org.springframework.data.jpa.domain.Specification
I have implemented something like below where the jsonb column is not-an-array, but has key-value json using jsonb_extract_path_text. But the above column stores only values in an array.
My entity class.
#Entity
#Table(name = "temp_tbl")
public class TempTbl {
#Id
#Column(name = "ID")
private Long id;
#Column(name = "NAME", nullable = false)
private String name;
#Column(name = "LINKS", columnDefinition = "jsonb null")
#Convert(converter = JsonbConverter.class)
private List<String> linkIds;
}
I need help in translating above query into specification using criteriabuilder.

One way to check if a jsonb array contains a String using Jpa specifications is to use the functions jsonb_contains and jsonb_build_array, the latter of which is used to change the String into a jsonb array for use in jsonb_contains.
public static Specification<TempTbl> linkInLinks(String linkId) {
return (root, query, builder) -> {
Expression toJsonbArray = builder.function("jsonb_build_array", String.class, builder.literal(linkId));
return builder.equal(builder.function("jsonb_contains", String.class, root.get("linkIds"), toJsonbArray), true);
};
}

Related

Cascade delete problem on **custom** unidirectional #OneToMany relationship (JPA eclipselink)

I'm having problem deleting an entity having an Unidirectional #OneToMany custom relationship.
Here the relationship on the "base" entity (relevant columns only):
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "Id", updatable = false)
protected Integer id;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "ObjectId", referencedColumnName = "Id")
protected Collection<Attachment> attachmentsCollection;
Here the relevant columns on the "child" entity:
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "Id", updatable = false)
protected Integer id;
#NotNull
#Basic(optional = false)
#Size(min = 1, max = 64)
#Column(name = "ObjectTable", updatable = false)
protected String objectTable;
#Basic(optional = false)
#Column(name = "ObjectId", updatable = false)
protected Integer objectId;
#NotNull
#Basic(optional = false)
#Lob
#Column(name = "Data")
protected byte[] data;
#NotNull
#Basic(optional = false)
#Column(name = "SizeInBytes")
protected Long sizeInBytes;
#NotNull
#Basic(optional = false)
#Size(min = 1, max = 128)
#Column(name = "Name")
protected String name;
Here explanation on why this is a custom relationship:
The base object is the super class of all entities. It has, additional to its own id, the opportunity to associate any number of attachments via the attachments collection. Since Ids are not unique between tables (entities), it's needed to add an additional column in the child table (the attachment table). This additional column (ObjectTable) identifies the entity-kind owning the attachment. Adding this column to the entity'id (ObjectId) column, the relation is complete:
Suppose record 99 of entity Invoice has 2 attachments (attachment 'Z' and attachment 'Y'):
Table Invoice
-------------
Id ColumnA ColumnB ColumnC...
99 'xyz' '2343' 'zyx'
.
.
Table Attachment
----------------
Id ObjectTable ObjectId Data SizeInBytes Name
43542 'Invoice' 99 11100110 437834 'Z.pdf'
43543 'Invoice' 99 101110 867454 'Y.pdf'
I managed to load the relation with a mapping customizer:
public static final String TABLENAME = "TECAttachment";
public static final String OBJECTIDFIELDNAME = TABLENAME + ".ObjectId";
public static final String OBJECTTABLEFIELDNAME = TABLENAME + ".ObjectTable";
// Customize how records are selecting inside entity's attachments collection: include the entities table name
#Override
public void customize(ClassDescriptor descriptor) {
OneToManyMapping mapping = (OneToManyMapping)descriptor.getMappingForAttributeName(AttachmentEntitySessionCustomizer.ATTACHMENTSCOLLECTIONNAME);
ExpressionBuilder eb = new ExpressionBuilder();
Expression eObjectIdNotNull = eb.getField(AttachmentEntitySessionCustomizer.OBJECTIDFIELDNAME).notNull();
Expression eObjectId = eb.getField(AttachmentEntitySessionCustomizer.OBJECTIDFIELDNAME).equal(eb.getParameter(descriptor.getPrimaryKeyFields().get(0)));
Expression eObjectTable = eb.getField(AttachmentEntitySessionCustomizer.OBJECTTABLEFIELDNAME).equalsIgnoreCase(descriptor.getTableName());
mapping.setSelectionCriteria(eObjectIdNotNull.and(eObjectId.and(eObjectTable)));
}
... but I'm having problems during the delete operation of any entity. For a reason that I still don't understand, JPA is executing an update statement over the Attachment table without taking in consideration the ObjectTable column. Here is what's going on when I delete a record from the table PRHTABidParticipationItem:
Finest: Execute query DeleteObjectQuery(com.tec.uportal.prhta.model.bid.participation.PRHTABidParticipationItem[ id=24 ])
Finest: Execute query DataModifyQuery()
Fine: UPDATE TECAttachment SET ObjectId = ? WHERE (ObjectId = ?)
bind => [null, 24]
Fine: DELETE FROM TECPRHTABidParticipationItem WHERE (Id = ?)
bind => [24]
Finer: end unit of work flush
Finer: resume unit of work
Finer: begin unit of work commit
Finer: commit transaction
My problem is that the UPDATE statement over the table TECAttachment updates all records with the given Id and not only those related to the Entity TECPRHTABidParticipationItem.
I think I must override the sql statements DeleteObjectQuery or DataModifyQuery but don't know how.
Any help will be really appreciated.
I'm working with eclipselink-2.7.4
Thanks in advanace!
After a lot or searching, digging and reading I managed to solve my problem.
Basically:
My attachments collection is an unidirectional custom #OneToMany relationship.
I was in the correct path trying to achieve my goal with a collection customizer (a class associated to my collection via annotation and implementing the DescriptorCustomizer interface).
My customizer not only needs to customize the way the "child" records are selected, also it's needed to customize what to do when the parent record is deleted.
The way to do this is to override the default removeAllTargetsQuery property providing a new custom query through the method mapping.setCustomRemoveAllTargetsQuery(DataModifyQuery).
The most difficult part was to understand how the underlying eclipselink implementation sends parameters (which, order, type, etc.) to the custom DataModifyQuery. I had to download the source code of EclipseLink's JPA implementation and figure out how things are done...
Finally, everything is working good and ok thanks to the following simple DescriptorCustomizer:
package com.tec.uportal.model.customizer;
import org.eclipse.persistence.config.DescriptorCustomizer;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.expressions.ExpressionBuilder;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.mappings.OneToManyMapping;
import org.eclipse.persistence.queries.DataModifyQuery;
/**
*
* #author MarcB
*/
public class AttachmenstCollectionAttributeCustomizer implements DescriptorCustomizer {
public static final String ATTACHMENTS__COLLECTION_NAME = "attachmentsCollection";
public static final String ATTACHMENTS_TABLE_OBJECT_TABLE__FIELD_NAME = "ObjectTable";
// Customize attachments collection mapping
#Override
public void customize(ClassDescriptor descriptor) {
// Customize how records are selected inside parent entity's attachments collection: include in the WHERE clause the column ObjectTable
OneToManyMapping mapping = (OneToManyMapping)descriptor.getMappingForAttributeName(ATTACHMENTS__COLLECTION_NAME);
ExpressionBuilder eb = new ExpressionBuilder();
Expression eObjectId = eb.getField(mapping.getTargetForeignKeyFields().get(0).getQualifiedName()).equal(eb.getParameter(descriptor.getPrimaryKeyFields().get(0)));
Expression eObjectTable = eb.getField(mapping.getTargetForeignKeyFields().get(0).getTable().getQualifiedName() + "." + ATTACHMENTS_TABLE_OBJECT_TABLE__FIELD_NAME).equalsIgnoreCase(descriptor.getTable(descriptor.getTableName()).getQualifiedName());
mapping.setSelectionCriteria(eObjectId.and(eObjectTable));
// Customize what must be done (delete childs) when parent entity is deleted
DataModifyQuery dmf = new DataModifyQuery("DELETE " + mapping.getTargetForeignKeyFields().get(0).getTable().getQualifiedName() + " WHERE " + mapping.getTargetForeignKeyFields().get(0).getName() + " = #" + mapping.getTargetForeignKeyFields().get(0).getName() + " AND " + ATTACHMENTS_TABLE_OBJECT_TABLE__FIELD_NAME + " = '" + descriptor.getTableName() + "'");
mapping.setCustomRemoveAllTargetsQuery(dmf);
}
}

How check exist element in postgresql jsonb column with jpa criteria?

I have an entity as follow:
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
#TypeDef(name = "JsonbType", typeClass = JsonBinaryType.class)
public class Message {
#Id
#GeneratedValue
private Long id;
private String content;
#Column(columnDefinition = "jsonb")
#Type(type = "JsonbType")
private readTimes UserTimeSet;
}
public class UserTimeSet extend HashSet<UserTime> {
}
public class UserTime implement Serializable {
private String username;
private Date time;
}
Some records are as follow:
id | read_times
----+--------------------------------------
1 | [{"username": "user1", "time": 12312412}, {"username": "user2", "time": 123}]
2 | [{"username": "user2", "time": 713}]
3 | []
4 | []
For saving object to column with Hibernate, I use Hibernate Types project.Now I want to get records there is no user1 in read_times with JPA criteria? the response must be records with id: 2, 3, 4.
Update:
I solved with query but can't convert to JPA Criteria:
SELECT * FROM message WHERE jsonb_path_exists("read_times", '$[*] ? (#.username == "user1")')
If using EclipseLink, REGEXP is supported, and will work on Postgres,
You can also use the SQL function to call any SQL specific syntax inside JPQL
An example for Predicates are as follows:
Predicate inJsonNumbers = cb
.function("jsonb_extract_path_text",
String.class,
root.get("json"),
cb.literal("number"))
.in(filter.getJsonNumbers())
Predicate inJsonNames = cb
.function("jsonb_extract_path_text",
String.class,
root.get("json"),
cb.literal("name"))
.in(filter.getJsonNames())
ALso, you can write a custom SQLFunction for the CAST(jsonField->'key' AS float) expression and then use that in JPQL.
public String render(Type firstArgumentType, List args, SessionFactoryImplementor factory) throws QueryException {
return "CAST(" + args.get(0).toString() + "->'" + args.get(1).toString() + "' AS float)";
}
Try this
with temp AS(
select t.id, jsonb_array_elements(t.read_times)->>'username' as username from table_Name t
)
select id from table_name where id not in
(
select id from temp
where
username='user1'
)

How to return a count column not exists in table by JPA

I want find a way to get extra column that count my records and return it in 1 mapping entity with extra filed.
I tried #transient on field but it will not return value when query.
Then I remove #transient but get an exception when save.
Also I tried #Formula but received null pointer exception.
Here's my repository code:
#Query(value = "select id,account,session_id,create_time,count from query_history a join " +
"(select session_id sessionId,max(create_time) createTime,count(*) count from query_history group by session_id) b " +
"on a.session_id = b.sessionId and a.create_time = b.createTime where account = ?1 order by create_time desc",
countQuery = "select count(distinct(session_id)) from query_history where account = ?1",
nativeQuery = true)
Page<QueryHistory> findByNtAndGroupBySessionAndAction(String account, Pageable pageable);
entity code:
#Entity
#Table(name = "query_history")
#Data
public class QueryHistory {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column
private String account;
#Column
private Long sessionId;
#Column
private long createTime;
#Transient
private Integer count;
}
Sorry about my English and thanks a lot for any advice.
I solved the problem by projections spring-data-projections, in fact I tried this before but in my sql:
select id,account,session_id,create_time,count
which should be:
select id,account,session_id sessionId,create_time createTime,count
PS:
projection interface:
public interface QueryHistoryWithCountProjection {
Long getId();
String getAccount();
Long getSessionId();
long getCreateTime();
Integer getCount();
}

Mapping from table to entity on non-unique column

I have a table definition which looks like this. I do not have the luxury to change the column defn.
table definition
Table FOO{
F_ID NUMBER, -- PK
CODE VARCHAR2(10), -- NON UNIQUE
F_NAME VARCHAR2(10)
}
Table BAR{
B_ID NUMBER, -- PK
CODE VARCHAR2(10), --NON UNIQUE
B_NAME VARCHAR2(10)
}
I need to map this to two entities.
Please note that the relationship is one to many. that is, one Foo can have multiple Bars from an entity point of view.
However,
the same code, which is the common column is non unique in both tables, and can have multiple values in both tables.
I know this can be solved using a join afterwards, but the Bar information is required for every Foo object that gets loaded up.
Is there a way to map this naturally using ORM using a join on the common column code?
#Entity
#Table(name = "FOO")
class Foo{
#Id
#Column(name = "F_ID", nullable = false)
private Long id;
#Column(name = "F_NAME")
private String fName;
#Column(name = "CODE", nullable = false)
private String code;
//What should be the join condition?
private List<Bar> bars;
}
#Entity
#Table(name = "BAR")
class Foo{
#Id
#Column(name = "B_ID", nullable = false)
private Long id;
#Column(name = "B_NAME")
private String bName;
//Is this required?
//private String code;
}

JPA Nullable JoinColumn

I have an entity:
public class Foo {
#Id
#GeneratedValue
private Long id;
private String username;
#ManyToOne(cascade = { CascadeType.MERGE }, fetch = FetchType.LAZY, optional = true)
#JoinColumn(name = "ParentID", nullable = true)
private Foo parent;
// other fields
}
With this kind of relationship, each Foo object has a parent, except the first Foo instance in the DB which has a NULL ParentID. When i try to create a query via criteria api and try to search for the first Foo object (null parent id) using any of it's properties (ID, username etc..), i get a:
javax.persistence.NoResultException: No entity found for query
How should the JoinColumn be implemented in this case? How should i get the first Foo object in the DB which has a null parent ID?
Thanks
Updated September 28 2011
What i am trying to achieve is to look lookfor all Foos with a username starting in "foo" and would like to use as separate object being returned then:
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder;
CriteriaQuery<FooDto> criteriaQuery = criteriaBuilder.createQuery(FooDto.class);
Root<Foo> foo = criteriaQuery.from(Foo.class);
criteriaQuery.multiselect(foo.get("id"), foo.get("username"), foo.get("parent").get("username"), foo.get("property1")
//other selected properties);
Predicate predicate = criteriaBuilder.conjunction();
predicate = criteriaBuilder.and(predicate, criteriaBuilder.like(foo.get("username").as(String.class), criteriaBuilder.parameter(String.class, "username")));
// other criteria
criteriaQuery.where(predicate);
TypedQuery<FooDto> typedQuery = entityManager.createQuery(criteriaQuery);
typedQuery.setParameter("username", "foo%");
// other parameters
// return result
assuming that the usernames are foo1, foo2 and foo3 wherein foo1 has a null parent, the result set will only return foo2 and foo3 even if the foo1 has a username that starts with the specified predicate.
and also searching for foo1 alone will throw a
javax.persistence.NoResultException: No entity found for query
Is there a way to still include foo1 with the rest of the results? or foo1 will always be a special case since i have to add a criteria specifying that the parent is null? Or perhaps ai have missed an option in the joinColumn to do so.
Thanks
UPDATED Thu Sep 29 13:03:41 PHT 2011
#mikku
I have updated the criteria part of the post above (property1), because i think this is the part that is responsible for foo1 not to be included in the Result Set.
Here's a partial view of the generated query from the logs:
select
foo.id as id,
foo.username as username,
foo.password as password,
foo.ParentID as parentId,
foo_.username as parentUsername,
foo.SiteID as siteId,
from FOO_table foo, FOO_table foo_ cross join Sites site2_
where foo.ParentID=foo_.id and foo.SiteID=site2_.id and 1=1
and foo.username=? and site2_.remoteKey=? limit ?
and this wont return Foo1, obviously because username value is from foo_ which is why my original question is 'how should i get to Foo1'.
On the part that i have commented that ill use SelectCase and mix it up with your first reply, what i did is to add this part on the multiselect:
criteriaBuilder
.selectCase()
.when(criteriaBuilder.isNotNull(agent.get("parent")), agent.get("parent").get("username"))
.otherwise(criteriaBuilder.literal("")),
replacing the
foo.get("parent").get("username")
However this wont be able to get Foo1 as well. My Last resort though inefficient would probably check if the parameters are of Foo1 and create a criteriaQuery specifying a literal value for username, and using the default query otherwise.
Suggestions/alternatives are very much appreciated.
EDIT:
In your edited question nullable joincolumn is not problem - missing construction of FooDto, multiselect etc are. You can achieve your goal with following:
Querying:
/**
* Example data:
* FOO
* |id|username|property1|parentid|
* | 1| foo | someval| null|<= in result
* | 2| fooBoo | someval2| 1|<= in result
* | 3|somename| someval3| null|
*/
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<FooDto> c = cb.createQuery(FooDto.class);
Root<Foo> fooRoot = c.from(Foo.class);
Predicate predicate = cb.like(fooRoot.get("username").as(String.class),
cb.parameter(String.class, "usernameParam"));
c.select(
cb.construct(FooDto.class,
fooRoot.get("id"),
fooRoot.get("username"),
fooRoot.get("property1")))
.where(predicate);
TypedQuery<FooDto> fooQuery = em.createQuery(c);
fooQuery.setParameter("usernameParam", "foo%");
List<FooDto> results = fooQuery.getResultList();
Object to hold data:
public class FooDto {
private final long id;
private final String userName;
private final String property1;
//you need constructor that matches to type and order of arguments in cb.construct
public FooDto(long id, String userName, String property1) {
this.id = id;
this.userName = userName;
this.property1 = property1;
}
public long getId() { return id; }
public String getUserName() { return userName; }
public String getProperty1() { return property1; }
}