Crudrepository / Join single field from other table to entity as readonly - java

Bear with me for any mistakes I make, as this is my first question here.
I have a database with two tables, one table called: PERSON
with the following entity:
#Entity
class Person {
#Id
private String guid;
private String firstName;
private String organisationGuid;
...
...
}
And one table called: ORGANISATION
with the following entity:
#Entity
class Organisation {
#Id
private String guid;
private String name;
...
...
}
As you can see, every Person belongs to an Organisation.
And now I need to list all my persons, with the name of the organisation. I do not want the full Organisation-entity on the Person-entity, rather just the name. Like so:
[{
"guid": "xxx",
"firstName": "Name",
"organisationGuid": "yyy",
"organisationName": "Name of yyy"
}]
How can I accomplish this in the easiest way possible?
Things I have already tried:
1) Adding property to Person and modyfing select-statement
#Entity
class Person {
#Id
private String guid;
private String firstName;
private String organisationGuid;
private String organisationName;
...
...
}
--
#Repository
public interface PersonRepository extends CrudRepository<Person, String> {
#Query(nativeQuery = true, value = "select p.*, o.name as organisation_name from person p left join organisation o on p.organisation_guid = o.guid")
List<Person> findAll();
}
Result: This works fine when using findAll but as soon as I try to save a Person I get an error stating that column ORGANISATION_NAME does not exist.
2) OK, makes sense, so I tried to put #Transient on the field in the entity
#Entity
class Person {
...
#Transient
private String organisationName;
...
...
}
Result: Now it works to save the entity, but I never get the organisationName (as it is marked as Transient).
3) Well damn, then I try to use the annotation #ReadOnlyProperty
#Entity
class Person {
...
#ReadOnlyProperty
private String organisationName;
...
...
}
Result: Same error as in (1). I can findAll but as soon as I try to save a person entity hibernate reports that the column does not exist (because in order to save an item, hibernate first needs to select it, and this particular select does NOT use my own custom select I created in the repository).
4) So then I created a class called PersonOrganisation (with #Table(name="organisation")) with a #ManyToOne-relation from Person to PersonOrganisation, where PersonOrganisation is an entity with just two fields, guid and name.
Result: Error. I can findAll but as soon as I try to save a person entity hibernate reports that the organisationGuid does not match a PersonOrganisation in the database (as it seems that PersonOrganisation is not an Organisation the way Hibernate sees it).

Many things that can be improved here:
1) Add a relationship in the Person relating to the Organization:
#Entity
class Person {
#Id
private String guid;
private String firstName;
#ManyToOne
#JoinColumn(name = "organisationGuid")
private Organisation organisation;
2) create a Result Class which would be holding the projection results:
package com.mypkg;
#Entity
class PersonOrganization {
private String guid;
private String firstName;
private String organisationGuid;
private String organisationName;
public PersonOrganization(String guid, String firstName
, String organisationGuid, String organisationName){
// set the fields
}
}
3) Change the query (dont use native.. its not necessary):
#Repository
public interface PersonRepository extends CrudRepository<Person, String> {
#Query("select NEW com.mypkg.PersonOrganization(p.guid as guid ...
, o.name as organisationName)
from person p left join p.organisation o")
List<PersonOrganization> findPersonWithOrganization();
}
Remeber to add aliases to each result column to match the consturctor of the PersonOrganization class.

I started using the solution from Maciej above, but ran into problems when I didn't want to create an extra "projection" entity with the same fields as the original entity (there were 20 additional fields the on Person entity).
So I actually found another solution which I am very happy with.
1) I started out with adding a #ManyToOne in Person (like Maciej suggested)
#Entity
public class Person {
#Id
private String guid;
private String firstName;
#ManyToOne
private Organisation organisation;
...
...
}
2) I also added a custom serializer to the get-method for the Organisation on the Person entity:
#JsonSerialize(using = OrganisationLightSerializer.class)
public Organisation getOrganisation() {
return organisation;
}
The custom serializer is super simple:
public class OrganisationLightSerializer extends JsonSerializer<Organisation> {
#Override
public void serialize(Organisation organisation, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException, JsonProcessingException {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("name", organisation.getName());
jsonGenerator.writeEndObject();
}
}
3) Then I changed all find-queries in my repository and added join fetch, and with this I stopped hibernate from using (at least) two SQL-queries when fetching a list of Persons:
#Repository
public interface PersonRepository extends CrudRepository<Person, String> {
#Override
#Query("select p from Person p join fetch p.organisation o")
List<Person> findAll();
}
Result: I did not get the JSON-structure exactly the way I wanted it, but I managed to get just the pertinent information from each Organisation on each Person. The resulting JSON when fetching persons looks like so (as you can see I skipped the guid of the Organisation in the end, but it can easily be added again by just changing the custom serializer):
[{
"guid": "xxx",
"firstName": "Name",
"organisation": { name: "Name of yyy"}
}]
Disclaimer: Now I realize that my response here is not an exact answer to my own query, as I stated I wanted the JSON as a flat structure, but having an Organisation object in the JSON, which only contains the name of the Organisation is almost as good a solution. Should I edit/change/comment the question so that it reflects the actual answer here, or is this minor change an "acceptable" deviation of the requirements.

Related

Spring JPA - read a list of entities (with only partial properties) and update value without SELECTING the whole object from database?

I'm using Spring Boot 1.5.2. I have a big Entity with multiple properties and multiple OneToMany relations, for example:
#Entity
#Table(name = "person")
class Person {
#Id
protected long id;
private String property1;
private String property2;
private String property3;
private String property4;
private String property5;
private String property6;
#OneToMany
private List<Obj1> obj1List;
#OneToMany
private List<Obj2> obj2List;
#OneToMany
private List<Obj3> obj3List;
#OneToMany
private List<Obj4> obj4List;
}
How can I read the list of Person from database, but only with 2 properties id and property2, and update property2=0.
Then, I can use JPA CrudRepository to save():
public interface PersonRepository extends CrudRepository<Person, Long> {
}
for (Person person : personList) {
this.personRepository.save(person)
}
I don't want to use findAll() from CrudRepository which enables Hibernate to SELECT the whole list of Person with a big SQL query before saving to database.
Inside your PersonRepository interface, you should be able to add a Query like this:
#Query("select new Person(id, property2) from Person")
List<Person> findIdAndProperty2();
The other fields should come back null as they haven't been specified in your query. You'll just need to add a constructor to Person with id and property2 as arguments.
For updating, you can use similar syntax...
#Modifying
#Query("update Person set property2 = ?1 where id in ?2")
int updateProperty2(String property2, List<Long> ids);
com.google.common.collect.Iterables.partition can be used to process your updates in chunks. For instance...
for (List<Long> curUpdateIds : Iterables.partition(ids, 1000)) {
personRepository.updateProperty2("0", curUpdateIds);
}

Spring Data Projection with OneToMany returns too many results

I have a JPA entity (Person) with onetomany relation (ContactInfo).
#Entity
public class Person {
#Id
#GeneratedValue
private Integer id;
private String name;
private String lastname;
private String sshKey;
#OneToMany(mappedBy = "personId")
private List<ContactInfo> contactInfoList;
}
#Entity
public class ContactInfo {
#Id
#GeneratedValue
private Integer id;
private Integer personId;
private String description;
}
I've defined a projection interface that includes this onetomany relation as described here.
public interface PersonProjection {
Integer getId();
String getName();
String getLastname();
List<ContactInfo> getContactInfoList();
}
public interface PersonRepository extends JpaRepository<Person,Integer> {
List<PersonProjection> findAllProjectedBy();
}
When I retrieve the data with findAllProjectedBy the result contains too many rows. It looks like the returned data is the result of a join query similar to:
select p.id, p.name, p.lastname, ci.id, ci.person_id, ci.description
from person p
join contact_info ci on ci.person_id = p.id
For example for this data set:
insert into person (id,name,lastname,ssh_key) values (1,'John','Wayne','SSH:KEY');
insert into contact_info (id, person_id, description) values (1,1,'+1 123 123 123'), (2,1,'john.wayne#west.com');
The findAllProjectedBy method returns 2 objects (incorrectly) and the standard findAll returns 1 object (correctly).
Full project is here
I've done some debugging and it seems that the problem is with the jpa query.
The findAll method uses this query:
select generatedAlias0 from Person as generatedAlias0
The findAllProjectedBy uses this query:
select contactInfoList, generatedAlias0.id, generatedAlias0.name, generatedAlias0.lastname from Person as generatedAlias0
left join generatedAlias0.contactInfoList as contactInfoList
Does anyone know how to fix this invalid behaviour?
A quick fix for this problem is described here:
https://jira.spring.io/browse/DATAJPA-1173
You need to describe one of the single projection attributes with a #Value annotation. For the example posted above you will end up with:
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
public interface PersonProjection {
#Value("#{target.id}")
Integer getId();
String getName();
String getLastname();
List<ContactInfo> getContactInfoList();
}

Spring JPA - Find By EmbeddedId partially

Below code is for demo purpose only.
My Entity bean looks like this
#Entity
class Employee {
#EmbeddedId
private EmployeeKey employeeKey;
private String firstName;
private String lastName;
// Other fields
// Getter and Setters
}
The Embeddable class:
#Embeddable
class EmployeeKey implements Serializable {
private int employeeId;
private String branchName;
private String departmentName;
//Getter and Setters
}
I can write JPARepository interface method to find Employees by the EmbeddedId that returns me results as well.
interface EmployeeRepository extends JpaRepository<Employee, EmployeeKey> {
List<Employee> findByEmployeeKey(EmployeeKey employeeKey);
}
Question:
Suppose, while querying I have employeeId and branchName only, and I don't want to put filter on departmentName
In such cases how can I write my Repository method
Does JPA have something in-build for such scenario?
List<Employee> findByEmployeeKeyEmployeeIdAndEmployeeKeyBranchName(
int employId,
String branchName);
Should work Have a look at query derivation
Here is how it worked for me.
#Ketrox's answer is absolutely correct and works fine. But in my real scenario I had 6 fields to search by and which resulted in an 120+ characters long method name. (Something like below)
List<Employee> findByEmployeeKeyField1AndEmployeeKeyField2AndEmployeeKeyField3AndEmployeeKeyField4AndEmployeeKeyField5AndEmployeeKeyField6(String field1, String field2, String field3, String field4, String field5, String field6);
Which is certainly not good enough to read and more than good enough to make codenarc unhappy.
Finally I used find by example and that turned out to be really pleasant solution.
Repository:
//skipped lines
import org.springframework.data.domain.Example
//skipped lines
interface EmployeeRepository extends JpaRepository<Employee, EmployeeKey>{
List<Employee> findAll(Example<Employee> employee);
}
Usage:
// Prepare Employee key with all available search by keys (6 in my case)
EmplyeeKey key = new EmplyeeKey();
key.setField1("field1_value");
key.setField2("field2_value");
//Setting remaining 4 fields
// Create new Employee ans set the search key
Employee employee = new Employee();
employee.setEmployeeKey(key);
// Call the findAll by passing an Example of above Employee object
List<Employee> result = employeeRepository.findAll(Example.of(employee));
I have elaborated the search by Spring Data JPA find by #EmbeddedId Partially

Make the properties of a mapped entity read-only

Consider the following entities:
#Entity
public class MyEntity implements Serializable {
#Id
private String id;
#OneToOne
private Person person;
}
#Entity
public class Person implements Serializable {
#Id
private String id;
private String name;
// ... many more properties which should be read-only
}
Sometimes the name of the mapped Person is modified, and Hibernate generates update statements. But I don't want these to happen.
Is there a way to mark the properties of the mapped person read-only?
Changes to the id of the mapped person (I mean, a different person is attached to MyEntity) should however still make Hibernate update MyEntity.
#Column(updatable=false)
From the docs:updatable (optional): whether or not the column will be part of the update statement (default true)

NucleusUserException: Cannot access field on entity extending mapped superclass

I am running into a NucleusUserException while querying my google datastore instance. I am querying for a field that exists on a MappedSuperclass on the class that extends it. Here is my abstract class that contains the field I am interested in:
#Entity
#MappedSuperclass
#JsonIgnoreProperties({ "password" })
public abstract class AbstractUser implements User {
#Persistent
protected String emailAddress;
public void setEmailAddress(String email) {
this.emailAddress = email;
}
public String getEmailAddress() {
return this.emailAddress;
}
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long key;
//Other stuff.
}
The concrete instance looks like this:
#Entity
public class Client extends AbstractUser {
//Things that only clients have.
}
My query that is failing looks like this:
List existingUsersWithEmail = manager
.createQuery(
"SELECT c from Client AS c WHERE c.emailaddress = :mail")
.setParameter("mail", request.getEmailAddress())
.getResultList();
The exception is this:
Cannot access field emailaddress on type org.workouthound.user.Client
org.datanucleus.exceptions.NucleusUserException: Cannot access field emailaddress on type org.workouthound.user.Client
at org.datanucleus.query.compiler.JavaQueryCompiler.getType(JavaQueryCompiler.java:552)
at org.datanucleus.query.compiler.JavaQueryCompiler.getType(JavaQueryCompiler.java:529)
at org.datanucleus.query.symbol.SymbolTable.getType(SymbolTable.java:118)
at org.datanucleus.query.expression.PrimaryExpression.bind(PrimaryExpression.java:118)
at org.datanucleus.query.expression.DyadicExpression.bind(DyadicExpression.java:85)
at org.datanucleus.query.compiler.JavaQueryCompiler.compileFilter(JavaQueryCompiler.java:299)
at org.datanucleus.query.compiler.JPQLCompiler.compile(JPQLCompiler.java:75)
at org.datanucleus.store.query.AbstractJPQLQuery.compileInternal(AbstractJPQLQuery.java:246)
at org.datanucleus.store.query.Query.setImplicitParameter(Query.java:690)
at org.datanucleus.jpa.JPAQuery.setParameter(JPAQuery.java:428)
at org.workouthound.rest.client.UserResources.emailIsRegistered(UserResources.java:55)
at org.workouthound.rest.client.UserResources.createClient(UserResources.java:33)
I am new to DataNucleus and Google Data Store. I attempted to follow the tutorial as outlined here however I very well could have missed something. Please let me know as additional information is necessary.
UPDATE:
If I change the field name to email as well as the getters, setters and query, it works...why?

Categories