How can I write Queries for entities containing a 1-n reference?
Based on the spring-data-jdbc examples I will explain it with the following unit-test:
#Test
public void customQuery_ReferenceMultipleInstances() {
// prepare
LegoSet smallCar = createLegoSet("Small Car 01", 5, 12);
smallCar.setManual(new Manual("Just put all the pieces together in the right order", "Jens Schauder"));
smallCar.addModel("suv", "SUV with sliding doors.");
smallCar.addModel("roadster", "Slick red roadster.");
repository.save(smallCar);
// execute
List<LegoSet> actual = repository.findByName("Small Car 01");
Iterable<LegoSet> compare = repository.findAll();
// verify
assertThat(actual).as("same number of lego sets").hasSize(Lists.newArrayList(compare).size());
assertThat(actual.get(0).getModels()).as("same number of models").hasSize(Lists.newArrayList(compare).get(0).getModels().size());
assertThat(actual.get(0).getModels().get(0)).as("model must not be null").isNotNull();
assertThat(actual.get(0).getModels().get(0).getName()).as("model must have a name").isNotEmpty();
}
This models a LegoSet referencing 2 Models. The repository.findByName() is annotated with a custom query; the repository.findAll() is the standard spring-boot-data method of the CrudRepository (just as reference).
The custom query in version 1:
#Query("SELECT ls.id, ls.name, ls.min_age, ls.max_age, " +
"h.handbuch_id AS manual_handbuch_id, h.author AS manual_author, h.text AS manual_text " +
"FROM lego_set ls JOIN handbuch h ON ls.id = h.handbuch_id " +
"WHERE name = :name")
List<LegoSet> findByName(#Param("name") String name);
In this version the test fails w/
java.lang.AssertionError: [model must not be null]
Expecting actual not to be null
Okay, after that I add another JOIN to model:
#Query("SELECT ls.id, ls.name, ls.min_age, ls.max_age, " +
"h.handbuch_id AS manual_handbuch_id, h.author AS manual_author, h.text AS manual_text, " +
"m.* " +
"FROM lego_set ls JOIN handbuch h ON ls.id = h.handbuch_id " +
"JOIN model m ON ls.id = m.lego_set " +
"WHERE name = :name")
List<LegoSet> findByName(#Param("name") String name);
Now the test fails w/
java.lang.AssertionError: [same number of lego sets]
Expected size:<1> but was:<2> in:
<[LegoSet(id=1, name=Small Car 01, minimumAge=P5Y, maximumAge=P12Y, manual=Manual(id=1, author=Jens Schauder , text=Just put all the pieces together in the right order), models={suv=Model(name=suv, description=SUV with sliding doors.), roadster=Model(name=roadster, description=Slick red roadster.)}),
LegoSet(id=1, name=Small Car 01, minimumAge=P5Y, maximumAge=P12Y, manual=Manual(id=1, author=Jens Schauder
So how do I have to write that query correctly?
The first query is actually the correct one and it is working fine.
The problem is in your test. models is a Map, but you treat it like a List which compiles because a list index is also a valid map key.
If you change the last two assertions in your test like this they will succeed:
assertThat(actual.get(0).getModels().get("suv")).as("model must not be null").isNotNull();
assertThat(actual.get(0).getModels().get("suv").getName()).as("model must have a name").isNotEmpty();
// ----------------------------------------^
An alternative is to use the second query but with a custom ResultSetExtractor to collect multiple rows for the multiple models into a single LegoSet.
I'm in a similar situation, but i don't see what is different in my situation.
I have three models:
Client: id, name..
Project: id, name, clientId, List
ProjectMember
Here is my had coded query
#Query("""
select
project.id,
project.legacy_id,
project.code,
project.client_id,
project.is_archived,
project.name,
project.budget_type,
project.hours_budget_max,
project.money_budget_max
from project
join client on client.id = project.client_id
where client.name ilike '%:name%'
""")
List<Project> findByClientNameContainingIgnoreCase(final String name);
As you can see i'm looking for project where client name (using a join) match a pattern.
Executing the request by hand give me the expected results. But going through spring data jdbc give me no results
Related
I have a spring boot app connected to oracle DB.
I am trying to order a list of records and select the top most record.
I wrote a JPA query as below but it fails.
#Query("SELECT id FROM UploadedFile uploadedFile "
+ "WHERE uploadedFile.p_file_type = 'branch' "
+ "and p_file_status='Processed' "
+ "and p_is_file_process_with_error = 0 "
+ "order by c_created_date desc "
+ "FETCH FIRST 1 rows only ")
public String findLatestBranchCodeFile();
The error received was
creating bean with name 'uploadedFileRepo': Invocation of init method
failed; nested exception is java.lang.IllegalArgumentException:
Validation failed for query for method public abstract
java.lang.String
com.rhb.pintas.repo.UploadedFileRepo.findLatestBranchCodeFile()!
org.hibernate.hql.internal.ast.QuerySyntaxException: unexpected token:
FETCH near line 1, column 204 [SELECT id FROM
com.rhb.pintas.entities.UploadedFile uploadedFile WHERE
uploadedFile.p_file_type = 'branch' and p_file_status='Processed' and
p_is_file_process_with_error = 0 order by c_created_date desc FETCH
FIRST 1 rows only ] -> [Help 1]
The issue seems to be with fetch,not sure whats wrong.
It seems you are mixing HQL and native query dialects:
If this will be a naviveQuery (like most of the columns would mention), then replace the entity name with table name and add nativeQuery option. And because You are using only a single table, You can skip the alias name:
#Query("SELECT id FROM uploaded_file "
+ "WHERE p_file_type = 'branch' and p_file_status='Processed' and "
+ "p_is_file_process_with_error = 0 "
+ "order by c_created_date desc "
+ "FETCH FIRST 1 rows only ", nativeQuery = true)
public String findLatestBranchCodeFile();
If You want to keep it as a HQL, then replace all column names with entity property names, like p_file_type > fileType (I guess column names). Secondly You will need to pass a Pageable parameter, to replace Your 'Fetch first' statement.
You can find more materials here:
Bealdung
NativeQ
StackOverflow
You are trying to execute SQL query, in this case you need to add nativeQuery=true attribute to #Query annotation
UPD.
got confused because FETCH FIRST - is a SQL syntax, for JPA way please check another solution - it is possible to return list with at most one element.
I guess, you can try passing pagable to limit result set size and unlimit your query:
public String findLatestBranchCodeFile(Pageable pageable); // pass new PageRequest(0,1)
In Spring Data JDBC examples, how do I write a simple query in #Query annotation?
e.g. In LegoSet Repository, how do I add a simple findByName query?
When I tried
#Query("select * from lego_set where name = :name")
List<LegoSet> findByName(#Param("name") String name);
it throws following error:
org.springframework.data.mapping.MappingException: Could not read property #org.springframework.data.annotation.Id() #org.springframework.data.relational.core.mapping.Column(value=handbuch_id, keyColumn=)private java.lang.Long example.springdata.jdbc.basics.aggregate.Manual.id from result set!
...
> Caused by: org.hsqldb.HsqlException: Column not found: manual_handbuch_idat org.hsqldb.error.Error.error(Unknown Source) at org.hsqldb.error.Error.error(Unknown Source) `
Also, the reference document seems to be copied from some generic spring data document since it mentioned derived query which doesn't exist in spring data jdbc yet.
Just as a completion of #jens-schauder's answer:
The query should be:
#Query("SELECT ls.id, ls.name, ls.min_age, ls.max_age, " +
"h.handbuch_id AS manual_handbuch_id, h.author AS manual_author, h.text AS manual_text " +
"FROM lego_set ls JOIN handbuch h ON ls.id = h.handbuch_id " +
"WHERE name = :name")
List<LegoSet> findByName(#Param("name") String name);
Using this method the following test passes:
#Test
public void so_52978700() {
// prepare
LegoSet cars = createLegoSet("Small Car - 01", 5, 10);
cars.setManual(new Manual("Just put all the pieces together in the right order", "Jens Schauder"));
repository.save(cars);
// execute
List<LegoSet> actual = repository.findByName("Small Car - 01");
// verify
assertThat(actual).hasSize(1);
assertThat(actual.get(0).getName()).isEqualTo("Small Car - 01");
assertThat(actual.get(0).getManual().getText()).isEqualTo("Just put all the pieces together in the right order");
}
The LegoSet entity has a 1:1 relationship to a Manual.
Spring Data JDBC selects such a construct using a join and expects the representative columns in the ResultSet.
Note that it expects the columns representing the Manual entity itself plus the one forming the back-reference to the LegoSet.
Also, all column names are to be prefixed by the property name +_, i.e. manual_ in this case.
The error message actually tells you about the missing column (modulo a missing space): Column not found: manual_handbuch_id.
Alternatively, you can also provide your own RowMapper
Regarding the documentation:
You are kind of right.
The documentation of (almost) all Spring Data modules includes a generic part which easily leads to confusion. There is a ticket for comming up with a better solution.
I think you are trying to execute a native query.So,try as below
#Query( value = "SELECT * FROM lego_set ls where ls.name = :name",
nativeQuery = true)
List<LegoSet> findByName(#Param("name") String name);
This should work.
I am getting this exception from fetching record through database based on column portfolio
Query:
String queryString= "select entity from " +Name + " entity WHERE entity."+Column+" = "+ searchId.toString();
Query query = _em.createQuery(queryString);
Many things are wrong here and a lot of important information is missing (entity code?).
className in your example should be just Portfolio instead of class com.hexgen.orm.Portfolio (assuming Portfolio is entity's name, which doesn't have to be the same as class name). If you are using someEntity.getClass().getName() to get it, change to someEntity.getClass().getSimpleName()
searchColumn should be the field name of Portfolio class, not column name in the database. Asssuming PORTFOLIO column is mapped to portfolio field, it should be portfolio.
String queryString = "select entity from " + simpleClassName + " entity WHERE entity." + searchColumn + " = :searchId";
Query query = _em.createQuery(queryString);
query.setParameter("searchId", searchId.toString());
If you don't switch to using query parameters, searchId.toString() should be enclosed in single quotes.
So, the valid query should look something like this
select entity from Portfolio entity WHERE entity.portfolio = 'HEXAGON20'
You have several problems in your query syntax and if you look at the message it's quite explicit:
You build your query with
"select entity from " +className + " entity WHERE entity."+searchColumn+" = "+ searchId.toString();
Which gives
select entity from class com.hexgen.orm.Portfolio entity WHERE entity.PORTFOLIO = HEXAGON20
1- the class should not be there. Ir looks like it comes from the variable className.
2- HEXAGON20 is a string value that should be 'HEXAGON20'
I have two tables - one containing Address and another containing Photographs. The only common field between them is the PersonID. These were mapped to two POJO Classes Address and Photo. I was able to fetch details in these tables by creating criteria and adding restrictions on the fields . How should we write a join on the two tables. Is it possible to get the result as two objects -Address and Photo.
I want to do a left join so that i can get records of persons without photos as well.
I have read that this is possible only using hql but Can this be done using criterias as well?
You can easily write HQL query that will return result as two objects using Theta Join (as Adrian noted). Here is an example:
String queryText = "select address, photo from Address address, Photo photo "
+ " where address.personID=photo.personId";
List<Object[]> rows = session.createQuery(queryText).list();
for (Object[] row: rows) {
System.out.println(" ------- ");
System.out.println("Address object: " + row[0]);
System.out.println("Photo object: " + row[1]);
}
As you can see query returns list of Object[] arrays that represents each fetched row. First element of this array will contain one obejct and second element - another.
EDIT:
In case of left join I think you need to use native SQL query (not HQL query). Here how you can do this:
String queryText = "select address.*, photo.* from ADDRESS address
left join PHOTO photo on (address.person_id=photo.person_id)";
List<Object[]> rows = sess.createSQLQuery(queryText)
.addEntity("address", Address.class)
.addEntity("photo", Photo.class)
.list();
This should work for your case.
Basically, you have two options:
Since Hibernate 5.1, you can use ad-hoc joins for unrelated entities.
Tuple postViewCount = entityManager.createQuery(
"select p as post, count(pv) as page_views " +
"from Post p " +
"left join PageView pv on p.slug = pv.slug " +
"where p.title = :title " +
"group by p", Tuple.class)
.setParameter("title", "High-Performance Java Persistence")
.getSingleResult();
Prior to Hibernate 5.1, you could only use theta-style joins. However, a theta-style join is equivalent to an equijoin, hence you can only emulate INNER JOINs not OUTER JOINs.
List<Tuple> postViewCount = entityManager.createQuery(
"select p as post, count(pv) as page_views " +
"from Post p, PageView pv " +
"where p.title = :title and " +
" ( p.slug = pv.slug ) " +
"group by p", Tuple.class)
.setParameter("title", "Presentations")
.getResultList();
Finally after 12 years the Hibernate team has implemented such a feature
From Hibernate docs:
The FROM clause can also contain explicit relationship joins using the join keyword. These joins can be either inner or left outer style joins.
List<Person> persons = entityManager.createQuery(
"select distinct pr " +
"from Person pr " +
"join pr.phones ph " +
"where ph.type = :phoneType", Person.class )
.setParameter( "phoneType", PhoneType.MOBILE )
.getResultList();
List<Person> persons = entityManager.createQuery(
"select distinct pr " +
"from Person pr " +
"left join pr.phones ph " +
"where ph is null " +
" or ph.type = :phoneType", Person.class )
.setParameter( "phoneType", PhoneType.LAND_LINE )
.getResultList();
Or you can use WITH and ON keywords. A remark on those
The important distinction is that in the generated SQL the conditions
of the WITH/ON clause are made part of the ON clause in the generated
SQL, as opposed to the other queries in this section where the
HQL/JPQL conditions are made part of the WHERE clause in the generated
SQL.
Example
List<Object[]> personsAndPhones = session.createQuery(
"select pr.name, ph.number " +
"from Person pr " +
"left join pr.phones ph with ph.type = :phoneType " )
.setParameter( "phoneType", PhoneType.LAND_LINE )
.list();
I am currently eager to try the new feature.
Joining two unrelated entities are possible in Hibernate 5.1.
Eg :
select objA from ObjectA objA
JOIN ObjectB objB on objB.variable = objA.variable
where objA.id = 1
It's best to have a class containing those classes you want to join to have them all together.
But if you are joining these tables just for some occasional purposes, you can use criteria and manually load data from each table and put them together. (and yes, you can have these tables' data separately if for Address and Photo there are two separate classes and tables)
What you are looking for is
HQL
Join on fields that you haven't modeled as relationships
Left Join
(During the time the question was first asked and this answer was given) Hibernate supports Theta Join which allows you to do 1 & 2. However, only inner join is available for theta join style.
Personally I would recommend you to model proper relationships, so you just need 1 & 3 which is well-supported in HQL.
(Another answer actually provided an update on new Hibernate feature that provides such feature, that you may simply refer to)
I try to do the function searchBook with java and jpa. I have 2 classes which are Media and Book. Book extends Media. And I keep the data in the different table. I try to select the data from the query below:
TypedQuery<Media> query = em.createQuery(
"SELECT m.title, b.isbn, b.authors"
+ " FROM Book b, Media m" + " WHERE b.isbn = :isbn"
+ " OR lower(m.title) LIKE :title"
+ " OR b.authors LIKE :authors", Media.class);
query.setParameter("isbn", book.getisbn());
query.setParameter("title", "%" + book.getTitle().toLowerCase()
+ "%");
query.setParameter("authors", "%" + book.getAuthors() + "%");
bookList = query.getResultList();
But I got the error:
java.lang.IllegalArgumentException: Cannot create TypedQuery for query
with more than one return
This is the first time I use JPA. I can't find the the mistake.
As a workaround, to get entity composed by other entity attributes, you can create it within query, providing constructor for it.
Query :
TypedQuery<Media> query = em.createQuery("SELECT NEW package_name.Media(m.title, b.isbn, b.authors)"
+ " FROM Book b, Media m"
+ " WHERE b.isbn = :isbn"
+ " OR lower(m.title) LIKE :title"
+ " OR b.authors LIKE :authors", Media.class);
Entity :
public Media(String title, int isbn, String author){
//-- Setting appropriate values
}
I have provided sample, change the datatypes of the constructor accordingly.
Without goind into details about how Media and Book should be modeled, I will at least explain why you get this exception.
You're doing:
em.createQuery(someJPQL, Media.class);
This means: create a query using someJPQL, and this query will return instances of the Media entity.
But your JPQL is:
SELECT m.title, b.isbn, b.authors ...
So the query does not return entities of type Media. It returns three fields, from two different entities. There is no way your JPA engine could magically create instances of Media from these 3 columns. A query would return instances of Media if it looked like this:
select m from Media m ...
If you still want to use TypedQuery you can change the result type to Object[].
List<Object[]> results = entityManager
.createQuery("SELECT m.title, b.isbn, b.authors ...", Object[].class)
.getResultList();
Each Object[] in the List represents a row of data. It contains the selected values for that row in the order in which they were selected in the query. Element 0 is the title, element 1 is the ISBN, and element 2 is the authors. You'll likely need to cast those values if you want to use them in a meaningful way. Since the field values come from two different tables, you could store them in some kind of container object.
List<MediaContainer> mediaList = new ArrayList<>();
for (Object[] row : results) {
MediaContainer container = new MediaContainer();
container.setTitle((String) row[0]);
container.setIsbn((int) row[1]);
container.setAuthors((String) row[2]);
mediaList.add(container);
}
#WebUser instead of doing
List<EntityIDKey> companies =
getEntityManager().createQuery(sql, EntityIDKey.class).getResultList();
Try this :
List<EntityIDKey> companies =
(List<EntityIDKey>)getEntityManager().createQuery(sql).getResultList();
works for me.
if your are using Hibernate version < 4, you can meet this bug.
I go same problem with v3.5. Finally i had to use simple Query and cast each parameter manually
see other comments here : https://groups.google.com/forum/#!topic/axonframework/eUd1d4rotMY
I... remove
Media.class
of
createQuery
because you return more Entities in this source "SELECT m.title, b.isbn, b.authors"
Ex.:
TypedQuery<Media> query = em.createQuery(
"SELECT m.title, b.isbn, b.authors"
+ " FROM Book b, Media m" + " WHERE b.isbn = :isbn"
+ " OR lower(m.title) LIKE :title"
+ " OR b.authors LIKE :authors");
You can resolve the issue by doing this kind of query:
em.createQuery( "SELECT m"
+ " FROM Book b, Media m" + " WHERE b.isbn = :isbn"
+ " OR lower(m.title) LIKE :title"
+ " OR b.authors LIKE :authors", Media.class);
but this works only if you need fields only from one of the requested tables