How to query with Spring JPA on jsonb columns? - java

I'm using spring JPA with PostgreSQL database. I have an Entity as follow:
#Entity
#TypeDef(name="json_binary", typeClass = com.vladmihalcea.hibernate.type.json.JsonBinaryType.class)
public class LiveTokens {
#Id
#GeneratedValue()
private Long id;
private String username;
#Type(type="json_binary")
#Column(columnDefinition = "jsonb")
private Token token
}
and Token:
public class Token {
private Long expireTime;
private String accessToken;
}
For saving object to column with Hibernate, I use Hibernate Types project.
Now I want to get All LiveTokens that expired. I can't do it with Spring JPA. How do I query to posgresql jsonb column with Spring data JPA?

SQL JSON functions
If you want to call a SQL function that processes the JSON column, then you just need to make sure you register the function with Hibernate prior to using it.
JSON operators
For JSON operators, like the PostgreSQL ones (e.g., ->>), the only option you have is to use a native SQL query. JPQL or HQL don't support database-specific JSON operators.

Using EclipseLink and spring data jpa, if your data in db is something like: {"expireTime":102020230201, "accessToken":"SOMETHING" }, my first question is why to use long numbers for your dates instead of timestamps (ex '2019-09-14 12:05:00'). If you use timestamps there are also options to manage timezones (either from postgresql or from you source code).
Regarding your issue you may use the FUNC JPQL keyword of EclipseLink (Hibernate may have something similar) in order to run a database specific function. In the example below I use FUNC('jsonb_extract_path_text', lt.token, 'expireTime') to get the values of the json for token.expireTime.
PostgreSql method jsonb_extract_path_text returns text, thus you cannot do a less that condition, so I cast the output of the function using JPQL CAST keyword with (CAST -data- TO -type-).
#Repository
public interface MyRepository extends JpaRepository<LiveTokens, Integer> {
#Query(value = "SELECT lt FROM LiveTokens lt WHERE CAST(FUNC('jsonb_extract_path_text', lt.token, 'expireTime') AS LongType) < :expirirationThreshold")
List<LiveTokens> findByExpireTime(#Param("expirirationThreshold") Long expirirationThreshold);
}
Again, this is not tested.

This is how a native query using postgres JSON query operators would look like, incoorporating your example:
#Query(value="SELECT t.* FROM LiveTokens t WHERE CAST(t.token ->> 'expireTime' AS LONG) < now()", native=true)
Assuming your real tablename is LiveTokens, native queries do no longer use the JPA translations, and the tablename has to match the one in the DB. (You may also need to specify it's schema.)

Try it:
Service class:
Long currentTime = new Date().getTime();
Repository:
#Query("SELECT lt FROM LiveTokens lt WHERE lt.token.expireTime <= :currentTime")
List<LiveTokens> findExpiredLiveTokens(#Param("currentTime") long currentTime)

Related

Can Spring Data JDBC use object properties as query parameters?

Currently I use the following code for a custom SQL statement in a Spring Data JDBC Repository interface:
default void upsert(LoanRecord r) {
upsert(r.getId(), r.getUcdpDocumentFileId(), r.getFreSellerNo(),
r.getFreDeliverySellerNo(), r.getLenderLoanNo(), r.getFreLpKey());
}
#Modifying
#Query("""
INSERT INTO loan (id, ucdp_document_file_id, lender_loan_no, fre_seller_no, fre_delivery_seller_no, fre_lp_key)
values (:id, :ucdpDocumentFileId, :lenderLoanNo, :freSellerNo, :freDeliverySellerNo, :freLpKey)
ON CONFLICT (ucdp_document_file_id) DO UPDATE SET
lender_loan_no = EXCLUDED.lender_loan_no,
fre_seller_no = EXCLUDED.fre_seller_no,
fre_delivery_seller_no = EXCLUDED.fre_delivery_seller_no,
fre_lp_key = EXCLUDED.fre_lp_key""")
void upsert(UUID id, String ucdpDocumentFileId, String freSellerNo,
String freDeliverySellerNo, String lenderLoanNo, String freLpKey);
The actual statement doesn't really matter, but as you can see, there's a wrapper method that unpacks the object so I can use simple parameters in the second method. Is there some way to refer to object properties in the query (like with MyBatis) so we can get rid of the wrapper method?
For reference, my spring-data-jdbc version is 2.4.2.
This is possible with the latest milestone release of Spring Data JDBC(3.0.0-RC1!
It is probably the main use case behind SpEL support.
With it you can now use constructs like this:
#Query("select u from User u where u.firstname = :#{#customer.firstname}")
List<User> findUsersByCustomersFirstname(#Param("customer") Customer customer);
Just as you can for a long time in Spring Data JPA.

Use Hibernate Registered Function in Formula Field

I'm trying to utilize a registered function from my custom hibernate dialect inside of a formula field. The problem is that the formula field does not utilize my registered function. I'm trying to figure out what I'm doing wrong.
For background, I have an application that I'm working to make functional for both Oracle and Postgresql. Not simultaneously, but for whichever database its being deployed to. I have several formula fields in my models that are used to aggregate the names of OneToMany mapped entities into a single comma-delimited list for easy searching and display. This was done utilizing LISTAGG when it was purely Oracle. That won't work in Postgresql, but given that it needs to work with both environments, I can't just change the syntax of my Formula to STRING_AGG. So, I'm attempting to register a function for both that will utilize the appropriate format for whichever database is being used.
I'm using Custom Dialect extensions and registering my functions, but it doesn't utilize my registered function. I'm not sure what I'm doing wrong.
If this isn't actually possible and I'm approaching this from the wrong direction, is there a good approach to defining the formula fields dynamically? Not during runtime, but during compile time when the dialect is set?
public class CustomPostgresqlDialect extends PostgreSQL95Dialect {
public CustomPostgresqlDialect() {
super();
registerFunction("MY_LISTAGG", new SQLFunctionTemplate( StandardBasicTypes.STRING, " STRING_AGG(?1 , ', ' ORDER BY ?1) "));
}
}
...
public class CustomOracleDialect extends Oracle12cDialect {
public CustomOracleDialect() {
super();
registerKeyword("WITHIN");
registerFunction("MY_LISTAGG", new SQLFunctionTemplate( StandardBasicTypes.STRING,"LISTAGG(?1,', ') WITHIN GROUP(ORDER BY ?1)"));
}
}
And here is my model with the formula:
public class Contractor extends Object implements Serializable {
...
#OneToMany(mappedBy = "contractor", fetch = FetchType.LAZY)
private Set<ProjectManager> projectManagers;
...
#Formula("(" +
"SELECT\r\n" +
" MY_LISTAGG(PM.NAME)\r\n" +
"FROM\r\n" +
" PROJECTMANAGERS PM\r\n" +
" INNER JOIN CONTRACTORS C ON C.ID = PM.FK_CONTRACTOR\r\n" +
"WHERE\r\n" +
" C.ID = id" +
")"
)
#NotAudited
private String pmNames;
...
}
Like SternK wrote, it's not possible to use JPQL/HQL functions in #Formula and I would also advise against using subqueries in formulas in general as that will incur the penalty of always executing these subqueries even if you don't need the data.
I think this is a perfect use case for Blaze-Persistence Entity Views.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(Contractor.class)
public interface ContractorDto {
#IdMapping
Long getId();
#Mapping(value = "projectManagers.name", fetch = MULTISET)
Set<String> getPmNames();
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
ContractorDto a = entityViewManager.find(entityManager, ContractorDto.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
Page<ContractorDto> findAll(Pageable pageable);
The best part is, it will only fetch the state that is actually necessary! So in this case, a SQL query like the following would be created:
select
c.id,
(
select json_agg(json_object(c1, pm.name))
from project_manager pm
where pm.fk_contractor = c.id
)
from contractor c
If you really want an aggregated string, you could also use the GROUP_CONCAT function as provided by Blaze-Persistence within the mapping:
#EntityView(Contractor.class)
public interface ContractorDto {
#IdMapping
Long getId();
#Mapping("GROUP_CONCAT(projectManagers.name, 'SEPARATOR', ', ', 'ORDER BY', projectManagers.name,, 'ASC')")
String getPmNames();
}
You can not use #Formula for this purpose. As it's stated in the documentation:
You should be aware that the #Formula annotation takes a native SQL clause which may affect database portability.
You can try to use JPQL/Criteria query for this.

How to run a native SQL query in Spring without an entity and a JPA Repository?

I am trying to run some native SQL queries in my Spring application. I donĀ“t have an entity or JpaRepository class. I know it's strange, but this is a microservice just to collect two count queries and send it to Kafka.
Well trust me, all I need is these two integers from the queries. I run these code and always returns 0. I can see in the logs that Hikari is connecting to the database, so I don't know what to do. Searched a lot, but all answers involved the #Query solution, which does not work for me.
#Repository
#AllArgsConstructor
public class ReportRepository {
private final EntityManager em;
public int numberOfAccounts() {
String sql = "SELECT count(*) FROM account";
Query query = em.createNativeQuery(sql);
System.out.println(query.getFirstResult());
return query.getFirstResult();
}
public int numberOfSubscriptions() {
String sql = "SELECT count(*) FROM subscriptions";
Query query = em.createNativeQuery(sql);
System.out.println(query.getFirstResult());
return query.getFirstResult();
}
}
If you have EntityManager, and from what you are saying it can connect to DB, try this way:
public int numberOfSubscriptions() {
// >> "subscriptions" has to be the exact name of your table
// if does not work, consider trying SUBSCRIPTIONS or Subscriptions
String sql = "SELECT count(*) FROM subscriptions";
Query query = em.createNativeQuery(sql);
// getSingleResult() instead :)
return ((Number) query.getSingleResult()).intValue();
}
There is this (a bit old) JavaDoc for Query.getFirstResult() :
The position of the first result the query object was set to retrieve. Returns 0 if setFirstResult was not applied to the query object
So, I'd say that is not the right method for your case.
Happy Hacking :)
You should be using JDBC instead of an Entity Manager. Under the JPA uses JDBC but it requires defined entites to work. JDBC allows you to manage the connection and run the raw SQL queries.
Here's a link for how to do it in Spring:
https://spring.io/guides/gs/relational-data-access/#_store_and_retrieve_data

JPA: compare LocalDateTime attribute to builder.currentTimestamp()

Given:
#Entity
public class Paramter() {
#Id
private Long id;
private LocalDateTime startDate;
// getters & setters
}
// Extract from repository/dao method that grabs parameters:
...
final CriteriaBuilder builder = entityManager.getCriteriaBuilder();
final CriteriaQuery<Parameter> query = builder.createQuery(Parameter.class);
final Root<Parameter> param = query.from(Parameter);
// The line below is generates following compilation error due to mismatch
// of the types of LocalDateTime and Timestamp parameters:
// java: no suitable method found for greaterThanOrEqualTo(javax.persistence.criteria.Path<java.time.LocalDateTime>,javax.persistence.criteria.Expression<java.sql.Timestamp>)
query.where(builder.greaterThanOrEqualTo(param.get(Parameter_.startDate), builder.currentTimestamp())); ...
How to properly use java 8 date-time api (more specifically LocalDateTime property) with jpa criteria builder to achive: "select * from parameter where start_date >= sysdate"?
Please consider:
The solution can be Oracle specific but should use JPA criteria builder
I do not want to create a select query to grab the sysdate and than use it in another select query to grab the valid parameters. It should be done in one query like in the expected query example above.
The time from the server is not reliable - database time should be used
Using Hibernate 5.2.16 which seems to include JPA 2.1. Changing to JPA 2.2 doesn't seem to make a difference.

How to prevent SQL Injection in Hibernate via POJO

We are using Hibernate on Tomcat. We recently found a SQL injection vulnerability in our Hibernate code. Specifically in reguards to the POJOs we use for ORM.
We are taking in the user input and creating a new POJO like this:
//POJO associated with Foo.hib.xml
public class Foo{
private String a;
private String b;
public Foo(String a, String b){
this.a=a;
this.b=b;
}
//Getters go here
}
We have many many pojos like this that are used in various rest services across various parts of our tomcat platmform. The problem we found is that users can set a sql query as a value for Foo so when we create it and save it, they can do sql injection:
//example of possible injection
Foo foo = new Foo("select * from *;", "insert * into * as *");
//save new Foo to hibernate
session.saveOrUpdate(foo); //above queries will be executed on insert
The above is just a generic example of what we are seeing.
I have looked all over and so far all I have found is examples for sanitizing data on queries and not on insert values through hibernate. Is there a way through hibernate to have it sanitize the data of a pojo before inserting it into the db?
Simple Answer:
You need to esnure that you are NOT concatenating the HQL/SQL query strings with input data directly like below:
//Unsafe Hibernate query, Never do this
Query query = session.createQuery(" from Employee where empId='"+inputEmpId+"'");
Rather, you need to set the data using setParameter() methods provided by org.hibernate Query API (or for JDBC PraparedStatements use setString(""), etc..) like below:
//Safe Hibernate query
Query query = session.createQuery(" from Employee where empId=:empId");
query.setParameter("empId", inputEmpId);
For long answer, you can look at here.

Categories