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

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'
)

Related

How to convert Integer param which can be 'null' to '0' when selecting data with Spring Data JPA

Assume we have entity Animal. There are animals in DB with 'amount' = null, it's a valid case to save animal without the 'amount'.
Is there a way to convert field 'amount' to 0 in case it's null in query?
The simplest workaround seems to convert amount null to '0' earlier
when saving, but it's not allowed.
As another workaround we can do this mapping to '0' after fetching
it from the repository. When sorting by amount in asc order, null values will be at the beginning, in desc order they will be at the end. And after
converting to '0' everything will be at the right place. But it seems that can cause problems with pagination in future
What is the proper way to do it in Query?
Spring Data Jpa 2.2.9.RELEASE, Postgresql 42.2.16.
#Repository
public interface AnimalRepository extends JpaRepository<AnimalEntity, Long> {
#Query(value = "SELECT animal FROM AnimalEntity animal" +
" WHERE animal.ownerId = :ownerId" +
" and function('replace', upper(animal.name), '.', ' ') like function('replace', upper(concat('%', :name,'%')), '.', ' ') "
)
Page<AnimalEntity> findAllLikeNameAndOwnerSorted(String ownerId, String name, Pageable pageable);
}
#Entity
#Table(name = "animal")
public class AnimalEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Integer amount;
private String name;
private String ownerId;
}
UPDATE
Also important to mention. The solution I suggested with replacing nulls with zero is incorrect, because of the different null ordering in Postgresql and HSQLDB.
But it will work in tests, if you're using HSQLDB.
Animal entities in DB test sample: [
Animal(name=Cat, amount=599999.99),
Animal(name=Dog, amount=null),
Animal(name=John, amount=5000)
]
Hsqldb amount desc query result:
[
Animal(name=Cat, amount=599999.99),
Animal(name=John, amount=5000),
Animal(name=Dog, amount=null)
]
Postgresql amount desc query result:
[
Animal(name=Dog, amount=null)
Animal(name=Cat, amount=599999.99),
Animal(name=John, amount=5000)
]
The JPA supports the COALESCE function. Thus you can set up the desired value via this function.
SELECT COALESCE(amount,0) AS desiredAmount FROM AnimalEntity animal
The code should look like this:
#Entity
#Table(name = "animal")
public class AnimalEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer amount;
public AnimalEntity() {
}
public AnimalEntity(Integer amount, String name) {
this.amount = amount;
this.name = name;
}
public Long getId() {
return id;
}
public Integer getAmount() {
return amount;
}
public void setAmount(Integer amount) {
this.amount = amount;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
And the repository:
#Repository
public interface AnimalRepository extends JpaRepository<AnimalEntity, Long> {
#Query(
value = "SELECT animal.id AS id, COALESCE(animal.amount,0) AS amount, UPPER(animal.name) AS name FROM animal animal WHERE animal.name = :name",
nativeQuery = true)
Page<AnimalEntity> findAllLikeNameAndOwnerSorted(String name, Pageable pageable);
}
Also I have prepared the test:
#SpringBootTest
class AnimalRepositoryTest {
#Autowired
private AnimalRepository animalRepository;
#Test
void findAllLikeNameAndOwnerSorted() {
AnimalEntity animalEntity = new AnimalEntity(null, "dog");
animalRepository.save(animalEntity);
AnimalEntity animalEntity2 = new AnimalEntity(1, "CAT");
animalRepository.save(animalEntity2);
System.out.println(animalEntity2.getId());
Pageable sortedByName = PageRequest.of(0, 3, Sort.by("id"));
Page<AnimalEntity> animals = animalRepository.findAllLikeNameAndOwnerSorted("dog", sortedByName);
animals.forEach(System.out::println);
}
}
You can check the commit: https://gitlab.com/chlupnoha/meth/-/commit/76abbc67c33b2369231ee89e0946cffda0460ec9 - it is experiment project.

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();
}

JPA Native Query to map One to Many

I have a complex Oracle query which for simplicity's sake looks like this;
SQL> SELECT d.id AS dept_id,
2 d.name AS dept_name,
3 e.id AS emp_id,
4 e.name AS emp_name,
5 e.dept_id AS emp_dept_id
6 FROM drs2_dept d, drs2_emp e
7 WHERE d.id = e.dept_id (+)
8 /
DEPT_ID DEPT_NAME EMP_ID EMP_NAME EMP_DEPT_ID
---------- ------------------- ---------- -------------- -----------
1 SALES 101 JOHN 1
1 SALES 102 JANE 1
2 ADMIN
My Department class is;
#SqlResultSetMapping(
name = "Department.employeeMapping",
classes = {
#ConstructorResult(
targetClass = Department.class,
columns = {
#ColumnResult(name = "DEPT_ID", type = Integer.class),
#ColumnResult(name = "DEPT_NAME")
}
),
#ConstructorResult(
targetClass = Employee.class,
columns = {
#ColumnResult(name = "EMP_ID", type = Integer.class),
#ColumnResult(name = "EMP_NAME"),
#ColumnResult(name = "EMP_DEPT_ID", type = Integer.class)
}
)
}
)
#NamedNativeQuery(
name = "Department.findAllEmployees",
query = "SELECT d.id AS dept_id, " +
" d.name AS dept_name, " +
" e.id AS emp_id, " +
" e.name AS emp_name " +
" e.dept_id AS emp_dept_id, " +
"FROM drs2_dept d, drs2_emp e " +
"WHERE d.id = e.dept_id (+)",
resultSetMapping = "Department.employeeMapping"
)
#Entity
public class Department {
#Id // JPA will not start without it.
Integer id;
String name;
#OneToMany // JPA will not start without it.
List<Employee> employees = new ArrayList<>();
public Department(Integer id, String name) {
this.id = id;
this.name = name;
}
public Department() {}
// getters and setters
}
My Employee class is;
#Entity
public class Employee {
#Id Integer id;
Integer departmentId;
String name;
public Employee(Integer id, String name, Integer departmentId) {
this.id = id;
this.name = name;
this.departmentId = departmentId;
}
public Employee() {}
// getters and setters
}
Because I am using #ConstructorResult I am able to get the data, but it still in a flat structure, that is to say a List<Object[]> with three entries, each containing [Department, Employee]. So I have to do the following to move the Employee records within their respective Department;
#Component
public class DepartmentDAO {
#PersistenceContext EntityManager entityManager;
public Collection<Department> getAllDepartments() {
Query query = entityManager.createNamedQuery("Department.findAllEmployees");
Map<Integer, Department> map = new HashMap<>();
List<Object[]> list = query.getResultList();
for (Object[] tuple: list) {
Department d = (Department) tuple[0];
if (! map.containsKey(d.getId())) {
map.put(d.getId(), d);
}
d = map.get(d.getId());
Employee e = (Employee) tuple[1];
if (e.getId() != null) {
d.getEmployees().add(e);
}
}
return map.values();
}
}
Whenever I add any additional properties to the #OneToMany I seem to get spurious SQL generated in the Hibernate logs which is incorrect (i.e. non-existent column or table names), but as I stated at the start of this question, I want the native SQL only - I don't want Hibernate to figure out what I am trying to do.
Is there any way to get JPA/Hibernate to put the Employee objects into the Department's list for me?
(
As a sub-note, I have seen this question asked here, but either never answered or answered back in 2011, by which time JPA and Hibernate may have progressed.
I should also add that elsewhere in my project I already have Department and Employee fully mapped for CrudRepository use with #Table and #Column, however their #OneToMany definitions do not depict what I am doing in the above query, hence their absence in my example code.
)
This query does not have any clause that forces it to be implemented with a native.
In fact it is considered a bad practice.
Try this:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Departament> cq = cb.createQuery(Departament.class);
Root<Departament> rootDepartament = cq.from(Departament.class);
Join<Departament,Employee> joinEmployee = rootDepartament.join(Departament_.employees,JoinType.Left);
cq.select(rootDepartament);
List<Departament> result = entityManager.createQuery(cq).getResultList();

How to query jsonb column using spring data specification?

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);
};
}

JPA SQLResultSetMapping for SQL Aliases not Working At All?

Native SQL with aliased field names + remapping to receive managed entities is required for more complex queries with joined tables.
However, the mapping of the SQL aliases leads to an exception where the aliased fields cannot be found. Can anybody detect an error in the code below, or is SQLResultSetMapping broken? (The sample below is intentionally simple to allow quick checking)
RDBMS H2, DDL
create table A(
ID INTEGER DEFAULT NOT NULL AUTO_INCREMENT PRIMARY KEY,
VAL VARCHAR(10)
);
insert into A (val) values ('val1');
insert into A (val) values ('val2');
Java class
#Entity
#NamedNativeQuery(name = "queryall",
query="select ID as AID, val from A",
resultSetMapping = "mapping")
#SqlResultSetMapping(name = "mapping",
entities = #EntityResult(
entityClass = A.class,
fields = {#FieldResult(name = "ID", column = "AID")})
)
public class A implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Integer id;
#Column(name = "VAL")
private String val;
public A() {
}
public A(Integer id) {
this.id = id;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getVal() {
return val;
}
public void setVal(String val) {
this.val = val;
}
#Override
public String toString() {
return "entities.A[ id=" + id +", val="+val+ " ]";
}
public static void main(String[] args) {
EntityManagerFactory entityManagerFactory =
Persistence.createEntityManagerFactory("JavaApplication6PU");
EntityManager em = entityManagerFactory.createEntityManager();
Query sqlQuery = em.createNamedQuery("queryall");
List list = sqlQuery.getResultList();
for (Iterator<A> iterator = list.iterator(); iterator.hasNext();) {
a = iterator.next();
System.out.println(String.format("entity %s, managed: %s", a, em.contains(a)));
}
}
}
Execution stops with exception:
[EL Warning]: 2018-01-12 21:45:42.748--UnitOfWork(1823014131)--Exception
[EclipseLink-6044] (Eclipse Persistence Services - 2.5.2.v20140319-9ad6abd):
org.eclipse.persistence.exceptions.QueryException
Exception Description: The primary key read from the row [DatabaseRecord(
A.ID => null
A.VAL => val1)] during the execution of the query was detected to be null.
Primary keys must not contain null.
Query: ResultSetMappingQuery(name="queryall" referenceClass=A sql="select ID as AID, val from A")
This, in other words, means: No mapping has taken place -> aliased fields not found
The same when the mapping is announced in adhoc Queries.
Query sqlQuery = em.createNativeQuery("select ID as AID, val from A","mapping");
If resultClass is used instead of resultSetMapping and no SQL aliases exist, the output is as it should be. (This proves that there is no misspelling of fields or any other error)
#NamedNativeQuery(name = "queryall",
query="select ID, val from A",
resultClass = A.class)
Output:
entity entities.A[ id=1, val=val1 ], managed: true
entity entities.A[ id=2, val=val2 ], managed: true