spring-data-jdbc: JdbcQueryCreator: Cannot query by nested entity - spring

I'm in the process of creating a service using Spring Boot (first time using Spring). I have an entity that is identified by a triple (mission, cycle, id) in the database, this is logically the primary key.
To work around the requirement of Spring-data-jdbc of having an #Id Long primary key I have created an IDENTITY primary key and added UNIQUE constraints for these columns. In my experiment I have two tables that are keyed this way (the second has an addition column in the logical key):
CREATE TABLE IF NOT EXISTS submission_counter (
id IDENTITY NOT NULL PRIMARY KEY,
mission VARCHAR(10) NOT NULL,
cycle INTEGER NOT NULL,
proposal_id INTEGER NOT NULL,
count INTEGER NOT NULL DEFAULT 1,
UNIQUE (mission, proposal_id, cycle),
CHECK (count >= 1),
CHECK (proposal_id >= 1)
);
CREATE TABLE IF NOT EXISTS submission (
id IDENTITY NOT NULL PRIMARY KEY,
mission VARCHAR(10) NOT NULL,
cycle INTEGER NOT NULL,
proposal_id INTEGER NOT NULL,
number INTEGER NOT NULL,
CHECK (number >= 1),
CHECK (proposal_id >= 1),
UNIQUE (mission, cycle, proposal_id, number)
);
To package the key triple in the Java code I've created an embedded record for the two entity classes.
enum Mission {A, B, C}
public record SubmissionCount(int count){}
public record ProposalId(int id) {}
public record SubmissionNumber(int number) {}
public record SubmissionKey(Mission mission, Cycle cycle, ProposalId proposalId) {}
public record Submission(#Id Long id, SubmissionKey key, SubmissionNumber number) {}
public record SubmissionCounter(#Id Long id, SubmissionKey key, SubmissionCount count) {}
Previously, I had all the SubmissionKey fields inlined in the records where they are used and this all worked fine. Now, when attempting to write a query for the key I get an exception from JdbcQueryCreator
This repository definition results "Cannot query by nested entity":
public interface SubmissionRepository extends CrudRepository<Submission, Long> {
Streamable<Submission> findByKey(SubmissionKey key);
}
Caused by: java.lang.IllegalArgumentException: Cannot query by nested entity: key
at org.springframework.data.jdbc.repository.query.JdbcQueryCreator.validateProperty(JdbcQueryCreator.java:147)
I've tried writing the repository query method a number of ways. My latest attempt is based on a number of questions stating that the method name can do field navigation.
public interface SubmissionCounterRepository extends CrudRepository<SubmissionCounter, Long> {
Optional<SubmissionCounter> findByKeyMissionAndKeyCycleAndKeyProposalId(SubmissionKey key);
}
This results in
Caused by: java.lang.IllegalStateException: Query method expects at least 2 arguments but only found 1. This leaves an operator of type SIMPLE_PROPERTY for property key.cycle unbound.
at org.springframework.data.relational.repository.query.RelationalQueryCreator.throwExceptionOnArgumentMismatch(RelationalQueryCreator.java:126)
at org.springframework.data.relational.repository.query.RelationalQueryCreator.validate(RelationalQueryCreator.java:110)
at org.springframework.data.jdbc.repository.query.JdbcQueryCreator.validate(JdbcQueryCreator.java:117)
If I were hand rolling everything I would put the (mission, cycle, proposal_id) triple only in the submission_counter table and join that with the submission for reads.
Is there a better way to do this with Spring? Can I do field navigation using #Query and named parameters?

Circling back to this after a few days, the issue was the missing #Embedded annotation on the SubmissionKey element.
Added #Embedded like this solved the issues:
public record Submission(#Id Long id, #Embedded(onEmpty = Embedded.OnEmpty.USE_EMPTY) SubmissionKey key, SubmissionNumber number) {}
public record SubmissionCounter(#Id Long id, #Embedded(onEmpty = USE_EMPTY) SubmissionKey key, SubmissionCount count) {}
This allows me to define findByKey methods on my repository interfaces and the queries are correctly derived:
public interface SubmissionRepository extends CrudRepository<Submission, Long> {
Streamable<Submission> findByKey(SubmissionKey key);
}
public interface SubmissionCounterRepository extends CrudRepository<SubmissionCounter, Long> {
Optional<SubmissionCounter> findByKey(SubmissionKey key);
}
I think the error saying "Cannot Query by nested entity" meant that Spring JDBC had concluded that SubmissionKey was a separate entity nested inside the Submission entity. Obvious in hindsight.

Related

Spring Data JDBC aggregate ID in child objects

I was playing with Spring-Data-JDBC and encountered 2 issues. I have following entities with 1:N relationship.
------
DROP TABLE IF EXISTS product;
CREATE TABLE product (
product_id int AUTO_INCREMENT PRIMARY KEY,
name varchar(250) not null,
description varchar(512) not null
);
DROP TABLE IF EXISTS product_line;
CREATE TABLE product_line (
product_id int constraint fk_product_line_product references product(product_id),
label varchar(250) not null
);
----------
#Data
#Builder
public class Product {
#Id
private Long productId;
private String name;
private String description;
#Singular
#MappedCollection(idColumn = "product_id", keyColumn = "product_id")
private Set<ProductLine> lines;
}
#Data
#Builder
public class ProductLine {
private Long productId;
private String label;
}
Problem 1: Following test case fails because I was expecting to have the productId populated in the ProductLine object but it is not. Is this the expected behavior of Spring Data JDBC?
#SpringBootTest
class SpringDataJdbcApplicationTests {
#Autowired
private ProductRepository productRepository;
#Test
void saveTest() {
Product product = Product.builder()
.name("Product-1")
.description("Description")
.line(ProductLine
.builder()
.label("Line-label")
.build())
.build();
this.productRepository.save(product);
assertThat(product.getProductId()).isNotNull();
assertThat(product.getLines()).isNotNull().isNotEmpty().hasSize(1);
assertThat(product.getLines().stream().findFirst()).isPresent();
assertThat(product.getLines().stream().findFirst().get().getProductId()).isNotNull().isEqualTo(product.getProductId()); // -----> Fails here.
}
}
Problem 2: If I change Set<ProductLine> to List<ProductLine>, it fails due to JdbcSQLIntegrityConstraintViolationException, which means the product id set to 0 as seen in the log snippet below.
2022-09-10 22:33:12.393 DEBUG 18460 --- [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [INSERT INTO "PRODUCT_LINE" ("LABEL", "PRODUCT_ID") VALUES (?, ?)]
2022-09-10 22:33:12.393 TRACE 18460 --- [ main] o.s.jdbc.core.StatementCreatorUtils : Setting SQL statement parameter value: column index 1, parameter value [Line-label], value class [java.lang.String], SQL type 12
2022-09-10 22:33:12.393 TRACE 18460 --- [ main] o.s.jdbc.core.StatementCreatorUtils : Setting SQL statement parameter value: column index 2, parameter value [0], value class [java.lang.Integer], SQL type 4
Following test case fails because I was expecting to have the productId populated in the ProductLine object but it is not. Is this the expected behavior of Spring Data JDBC?
Yes, if you want a productId you have to (and can easily) populate it yourself using plain Java code.
But you really shouldn't need the productId in the first place since if you follow Domain Driven Design, you will access a ProductLine exclusively from a Product which already has the id at hand.
The article https://spring.io/blog/2021/09/22/spring-data-jdbc-how-do-i-make-bidirectional-relationships might be helpful.
If I change Set<ProductLine> to List<ProductLine>, it fails due to JdbcSQLIntegrityConstraintViolationException, which means the product id set to 0 as seen in the log snippet below.
You have two problems here:
You already have two sources for the product_id field: The relation from the aggregate root and the simple field, which may cause problems.
You mapped both the back reference to the aggregate root idColumn and the index of the list keyColumn to the same database column. Together with the simple field from above these are three values all mapped to the same column. Not good.
The value that seems to win is the list index, resulting in the exception.
In order to fix that, create an additional column in the product_line table and map the list index to it.

Spring Data JDBC One-To-Many with Custom Column Name

I'm using spring-boot-starter-data-jdbc 2.4.2. In my domain aggregate I need to map a List of Strings that is populated from a column in another table. It is a legacy database so I have no control over the table and column names and need to use custom names. I see there is an #MappedCollection annotation, but can't see how to use it in this scenario. Below is my class:
#Data
#Table("NMT_MOVIE_THEATRE")
public class MovieTheatre {
#Id
#Column("MOVIE_THEATRE_ID")
private Long id;
#Column("ZIP_CODE")
private String zipCode;
// this comes from table NMT_CURRENT_MOVIE, column CM_ID, joined by MOVIE_THEATRE_ID
private List<String> currentMovieIds;
}
Using Spring Data JDBC, how can I create the one-to-many relation?
Wrap your String in a little entity.
#Table("NMT_CURRENTMOVIE")
class MovieId {
#Id
#Column("CM_ID")
final String id
// add constructor, equals and hashCode here or generate using Lombok
}
Then use it in the MovieTheatre. Since you don't have a column for an index, the proper collection to use is a Set
// ...
class MovieTheatre {
// ...
#MappedCollection(idColumn="MOVIE_THEATRE_ID")
Set<MovieId> currentMovieIds;
}
Note that equals and hashCode is important as well as the constructor taking all arguments used in those, since the entity is used in a Set.

Spring boot api how to prevent duplicate data creation

I have a POST API to create an entity called Person, and I check if person with the name and address exist or not, if not i create a person entity:
class Person {
UUID id
String name;
String address
}
public void createPerson(String name, String addr){
Person p = repository.findPersonByNameAndAddress(name, addr);
if (p != null) {
repository.create(name, addr);
}
}
If a client calls the POST with same data at the same time I will end up creating two person with same name and address (but different ID). How can i prevent this from happening?
I am using spring boot + JPA/Hibernate + postgres
Thanks!
Such constraints can be enforced only at the database layer. They cannot be handled at the application layer. If, as per your data model, two people cannot have the same name and address, you can add a unique constraint on (name, address) in your database.
ALTER TABLE person
add CONSTRAINT person_name_address UNIQUE (name, address);
With such a constraint, one of the two API calls will result in a SQLIntegrityConstraintViolationException which you can handle accordingly.
You can fix it by either of two ways:
Define a unique key constraint for both name and address
Find person by name and address by ignoring case using existsByNameIgnoreCaseAndAddressIgnoreCase
class PersonDao {
public void createPerson(String name, String addr) {
boolean isExists = repository.existsByNameIgnoreCaseAndAddressIgnoreCase(name, addr);
if (!isExists) {
PersonEntity person = new PersonEntity();
person.setName(name);
person.setAddress(addr);
repository.save(person);
}
}
}

Retrieve the record using Spring Data JPA

I'm having spring data jpa repository.I the entity contain primary key id int and ipaddress string. The table contain only 1 record at a time otherwise null.
How do i retrieve the record using JPA , if it is not found return null.
#Repository
public interface IpConfigRepository extends JpaRepository<IpConfig, Integer> {
//
IpConfig findIpConfig();
}
According to the naming convention, you should define the method with the name findById(Integer id) ( assume the Id is the primary key )
Suppose you have a class A as shown below
class A{
private int id;
private String data;
// getters and setters
}
You can now search the items by the following ways.
public interface ARepo extends JpaRepository<A,Integer>{
// get all the records from table.
List<A> findAll();
// find record by id
A findById(int id);
// find record by data
A findByData(String data);
// find by date created or updated
A findByDateCreated(Date date);
// custom query method to select only one record from table
#Query("SELECT * FROM a limit 1;")
A findRecord();
}

Spring data JPA for returning specific fields

Does Spring Data have a mechanism for returning specific fields?
I'm familiar with the syntax:
Invoice findByCode(String code);
How about this:
Integer findIdByCode(String code);
which returns the id field only. Or
Tuple findIdAndNameByCode(String code);
which returns a tuple. Or
Invoice findIdAndNameByCode(String code);
which returns an entity only populated with specific fields. Can use a constructor taking only those field if defined - else construct empty and populate the fields.
EDIT
To qualify some more, I'm aware of solutions like #Query, constructor expressions and now, #NamedEntityGraph. My question is simply - does Spring data support such a shorthand syntax as I'm suggesting?
If not, perhaps this is a cool enhancement for a later version...
I'm not looking for workarounds.
You can use JPQL Constructor Expressions:
SELECT NEW com.company.PublisherInfo(pub.id, pub.revenue, mag.price)
FROM Publisher pub JOIN pub.magazines mag WHERE mag.price > 5.00
The constructor name must be fully qualified
If you want to return just 1 field from table and it's primitive(or autoboxing), you can use next:
#Query("select distinct t.locationId from Table t")
List<Long> findAllWashroomLocationId();
Where:
Table - name of class which represent your table
t - alias
locationId - name of field(in your Table object)
Long - type of locationId (Integer, String, ...)
Not sure if what you're trying to achieve is the same as using multiple projections on the same JPA generated query (where method name are the same). I have posted an answer in this post.
https://stackoverflow.com/a/43373337/4540216
So I've managed to figure out how to use multiple projections with a
single query.
<T> T getByUsername(String username, Class<T> projection) This allows the method caller to specified the type of projection to be
applied to the query.
To further improve this so it is less prone to error, I made a blank
interface that the projection will have to extend in order to be able
to insert class into the parameter.
public interface JPAProjection {
}
public interface UserRepository extends CrudRepository<UserAccount, Long> {
<T extends JPAProjection > T getByUsername(String username, Class<? extends JPAProjection> projection);
}
Projection Interface
public interface UserDetailsProjection extends JPAProjection{
#Value("#{target.username}")
String getUsername();
#Value("#{target.firstname}")
String getFirstname();
#Value("#{target.lastname}")
String getLastname();
}
Then I can call the query method by
getByUsername("...", UserDetailsProjection.class)
i have a nativequery,
this is a insert and i going to return all fields after insert whit "RETURNING *"
this query return all fields of my database, and this data going to save in my entity
"Perfil Detalles"
my entity have all configurations of my fields of my database
#Query(
value= "INSERT INTO \"USUARIO\".\"PERFIL_CONFIGURACION\" (id_perfil, id_group, id_role) VALUES(:id_perfil, :id_group, :id_role) returning *",
nativeQuery = true)
public PerfilDetalles insertPerfilDetalles(
#Param("id_perfil") Long id_perfil,
#Param("id_group") int id_group,
#Param("id_role") int id_role);

Resources