The title maybe is not properly written but here is what, more or less, I want to achieve.
I would like to be able to write dynamic queries with use of Query by Example that would join multiple tables and create (projection?) DTO for me.
This DTO would have fields that are mapped to different columns in joined tables. Consider following:
Tables:
CREATE TABLE address
(
id SERIAL,
address_code VARCHAR(255) NOT NULL,
street_name VARCHAR(255),
building_number VARCHAR(255)
);
CREATE TABLE account
(
id SERIAL,
account_number BIGINT UNIQUE
);
CREATE TABLE customer
(
id SERIAL,
name VARCHAR(255)
)
I would like to be able to create a query which result would be:
address.address_code, account.account_number, customer.name
so basically the result would be a custom DTO. I also mentioned that I would like to have this backed up with Query by Example because I will to dynamically append WHERE clauses so I thought that if I created a DTO like:
public record CustomQueryResultDTO(String addressCode, BigInteger accountNumber, String name) {}
I could simply query just like it is in Spring R2DBC documentation.
The problem here is that I am not sure what should be a viable solution for such problem because on one hand I would like to reuse ReactiveQueryByExampleExecutor but that would mean that I have to create something like:
#Repository
public interface CustomQueryResultRepository extends ReactiveCrudRepository<CustomQueryResultDTO, Integer>, ReactiveQueryByExampleExecutor<CustomQueryResultDTO> {
}
Which kind of seems to me not a way to go as I do not have a corresponding table for CustomQueryResultDTO therefore there is really no mapping for this repository interface - or am I overthinking this and it is actually a way to go?
I think you are potentially overthinking it.
You can do it in a number of ways (note Java 17 text blocks):
Via R2DBC JPA-like #Query
Create a normal ReactiveCrudRepository but collect into a projection (DTOP)
// Repository
#Repository
public interface UserRefreshTokenRepository extends ReactiveCrudRepository<UserRefreshToken, Integer> {
#Query(
"""
select *
from user.user_refresh_tokens t
join user.user_infos c on c.user_id = t.user_id
where c.username = :username
"""
)
Flux<UserRefreshTokenDtop> findAllByUsername(String username);
}
// Entity
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
#ToString(exclude = {"refreshToken"})
#Table(schema = "user", name = "user_refresh_tokens")
public class UserRefreshToken {
#Id private Integer id;
private String userId;
private String username; # will be joined
private String ipAddr;
private OffsetDateTime createdAt;
private String refreshToken;
private OffsetDateTime refreshTokenIat;
private OffsetDateTime refreshTokenExp;
}
// DTO projection
public interface UserRefreshTokenDtop {
Integer getId();
String getUserId();
String getUsername(); # will be joined
String getIpAddr();
OffsetDateTime getRefreshTokenIat();
OffsetDateTime getRefreshTokenExp();
}
Via DatabaseClient
This one also uses TransactionalOperator to ensure query atomicity
private final DatabaseClient client;
private final TransactionalOperator operator;
#Override
public void deleteAllUsedExpiredAttempts(Duration resetInterval) {
// language=PostgreSQL
String allUsedExpiredAttempts = """
select t.id failed_id, c.id disable_id, t.username
from user.failed_sign_attempts t
join user.disable_sign_attempts c on c.username = t.username
where c.is_used = true
and :now >= c.expires_at + interval '%d seconds'
""";
// POTENTIAL SQL injection - half-arsed but %d ensures that only Number is allowed
client
.sql(String.format(allUsedExpiredAttempts, resetInterval.getSeconds()))
.bind("now", Instant.now())
.fetch()
.all()
.flatMap(this::deleteFailed)
.flatMap(this::deleteDisabled)
.as(operator::transactional)
.subscribe(v1 -> log.debug("Successfully reset {} user(s)", v1));
}
Via R2dbcEntityTemplate
I don't have a working example but it is pain in the ass to join via the .join() operator
If you are interested check the docs for R2dbcEntityTemplate
13.4.3. Fluent API > Methods for the Criteria Class
https://docs.spring.io/spring-data/r2dbc/docs/current/reference/html
Related
I am trying to do an aggregation with Esper 8.8.0 EPL using the following code. When ProductEvent is published then I am trying to save it into a table after converting the complete ProductEvent bean object into json. Is there any way I can pass ProductEven Object itself in custom function when executing merge statement -
#public create table OutputTable
(
productId string primary key
, productName string
, productJson string
);
#name('stmtUpdateOutputTable') on ProductEvent pe
merge OutputTable ot
where ot.productId = pe.productId
when not matched
then insert select productId, productName, Utils.getJson(*)
when matched
then update
set
ot.productName= pe.productName,
ot.productJson = Utils.getJson(*)
;
ProductEvent is a java bean that contains more than 100 property so it is not a good idea I pass each individual field when call custom function -
public class ProductEvent{
private String productId;
private String productName;
private Double price;
private LocalDate firstAvailableDate;
//..... around 100 more properties here
}
Utils is a helper class that contains static method -
public static String getJson(ProductEvent event) {
return new ObjectMapper().writeValueAsString(event);
}
In the on-merge, there are two aliases: "pe" for ProductEvent and "ot" for OutputTable, so "...Utils.getJson(pe)..." would work.
I am writing an API where I am inserting a record into a table (Postgres). I was hoping to use JPA for the work. Here is the potential challenge: the primary key for the insert is generated from a database trigger, rather than from sequence count or similar. In fact, the trigger creates the primary key using the values of other fields being passed in as part of the insert. So for example,
if I have a entity class like the following:
#Entity
#Validated
#Table(name = "my_table", schema="common")
public class MyModel {
#Id
#Column(name = "col_id")
private String id;
#Column(name = "second_col")
private String secCol;
#Column(name = "third_col")
private String thirdCol;
public MyModel() {
}
public MyModel(String id, String secCol, String thirdCol) {
this.id = id;
this.secCol = secCol;
this.thirdCol = thirdCol;
}
}
I would need the col_id field to somehow honor that the key is generated from the trigger, and the trigger would need to be able to read the values for second_col and third_col in order to generate the primary key. Finally, I would need the call to return the value of the primary key.
Can this be done with jpa and repository interface such as:
public interface MyRepo extends JpaRepository <MyModel, String> {
}
and then use either default save method such as myRepo.saveAndFlush(myModel) or custom save methods? I can't find anything on using JPA with DB triggers that generating keys. If it cannot be done with JPA, I would be grateful for any alternative ideas. Thanks.
ok, I was able to get this to work. It required writing a custom query that ignored the primary key field:
public interface MyRepo extends JpaRepository <MyModel, String> {
#Transactional
#Modifying
#Query(value = "INSERT INTO my_table(second_col, third_col)", nativeQuery = true)
int insertMyTable(#Param("second_col") String second_col, #Param("third_col") String third_col);
}
The model class is unchanged from above. Because it was executed as a native query, it allowed postGres to do its thing uninterrupted.
I have a transaction class which stores the each transaction of a customer,Following are the fields in this class.
class Transaction{
#Id
private String id;
private Date date;
private String customerId;
private double openBalance;
private double transctionAmount;
private double finalAmount;
}
I need to fetch only the last inserted record of a customer (let say for customerId = cust123).
I defined following function in repository.
public interface TranscationRepository extends MongoRepository<Transaction, String> {
Optional<Transaction> findTopByCustomerIdOrderByIdDesc(String id);
}
This method giving last entry not by customerId but overall. I tried few modifications to it but did not get success.
I know I can findAllByCustomer but I don't want to pull huge list of transaction which is of no use in this use case. What is correct signature in spring mongo to get last inserted record by a field? I am ok to use custom #Query also.
Thank you.
I currently have it working with a custom n1ql query, however it's such a simple query, I figured I could just use the built in jpa query method, however I can't figure out the key words, because I'm not getting anything back.
This code works:
#Query("SELECT meta().id as _ID, meta().cas as _CAS, * FROM `my-bucket` mb " +
"WHERE mb.name like $1 OR ANY Parent " +
"IN mb.Parents SATISFIES Parent.name like $1 END")
List<MyObject> searchObjectByName(String name);
This however doesn't work
#N1qlPrimaryIndexed
public interface MyObjectRepository extends CouchbasePagingAndSortingRepository<MyObject, String> {
List<MyObject> findBySecondObjectNameContains(String name);
}
#Data
#Document
public class MyObject{
#Id
private String objectId;
#Field
private SecondObject secondObject;
}
#Data
public class SecondObject {
#Field
private String name;
}
My test method:
#Autowired
private MyObjectRepository myObjectRepository;
#Test
public void testFind() {
List<MyObject> myObjects = myObjectRepository.findBySecondObjectNameContains("my name");
Assert.assertNotNull(myObjects);
}
The query looks correct, a few things that might be missing:
1) In your test, are you sure that "my name" isn't supposed to be "my name%"?
2) Check if you have a primary or secondary index that covers this query (run the same query via web console)
3) When did you insert the data? If you haven't configured couchbase to be strong consistent, you might have been reading an old version of your data
The model is simplified for the question.
I have this entity:
#Entity
public class Formation {
#Id
Long id;
String login;
String code;
String level;
// geters and Setters
With this repository:
public interface FormationRepository extends JpaRepository<Formation, Long> {
#Query(value = "SELECT UNIQUE l.id, l.login, d.code,d.level\n"
" FROM table_login l,\n" +
" table_diploma d,\n" +
" WHERE
" l.fhab_key = d.fhab_key\n" +
" AND l.login= :login", nativeQuery = true)
List<Formation> findAllByLogin(#Param("login")String login);
So far so good, this works.
Now I want to add test for the repository (with and h2database). But I can't save data, as the entity isn't mapped to a single table.
So this won't work:
#RunWith(SpringRunner.class)
#SpringBootTest(classes = Application.class)
#DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class FormationRepositoryTest {
#Autowired
FormationRepository formationRepository;
#Test
public void communeRepositoryTest() {
Formation formation = new Formation();
formation.setId(123L);
formation.setDlog_login("123");
formationRepository.save(formation); // ok
formationRepository.findAllByLogin("123"); // ko -> Caused by: Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException: scolarite.scol_droit_login is not mapped
}
}
Here I need to add, that I don't want sql files in my app.
So what would be a solution here ?
Change my model to create an entity by table ? (in real, my request use 8 inner joins, so it will be quite long to code all that...)
Another solution ?
If you do not want to create the entities for all your tables, you could pre-populate your testing H2 database by defining schema.sql and data.sql on your classpath.
schema.sql will contain the DDL statements to create all tables involved (you mention 8 tables).
data.sql will contain the insert statements needed to make your custom sql return login = 123.
You can find an example here.
Then your test code would be like:
#RunWith(SpringRunner.class)
#SpringBootTest(classes = Application.class)
#DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class FormationRepositoryTest {
#Autowired
FormationRepository formationRepository;
#Test
public void communeRepositoryTest() {
List<Formation> formations = formationRepository.findAllByLogin("123");
Assert.assertEquals(formations.size(), 1);
}
}