I'm writing a server with Spring-Boot using PostgreSQL
I'm trying to get information about images that are linked to a specific entity.
I'm trying to get User information from the server to my front-end Angular app.
In my system user have images linked to his account so i did class ImageEntity
#Entity #Table(name = "image") #Data
public class ImageEntity {
#Id #GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String name;
private String type;
#Lob
private byte[] image;
#JsonIgnore
public byte[] getImage() {
return image;
}
}
Then i linked the list of images to user account class
#Entity #Data
public class UserAccount{
#Id #GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String firstName;
private String lastName
#OneToMany(cascade = CascadeType.ALL)
#JoinTable(
name = "user_images",
joinColumns = {#JoinColumn(name = "user_id", referencedColumnName = "id")},
inverseJoinColumns = {#JoinColumn(name = "image_id", referencedColumnName = "id")}
)
private List<ImageEntity> images;
public void addImage(ImageEntity image) {
images.add(image);
}
}
Then i create endpoint to get user by id
#GetMapping("users/{id}")
public Optional<User> getUserById(#PathVariable Long id) {
return service.getUserById(id);
}
service method is very simple
#Transactional
public Optional<User> getUserById(Long id) {
return repository.findById(id);
}
I added some images through another endpoint works fine because i'm able to get image in my front-end.
Problem is when i want to get User info as a JSON from server( and i write #JsonIgnore on #Lob field because i only want to have info of image not the actual image) i get this error
Resolved exception caused by handler execution: org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Unable to access lob stream; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unable to access lob stream (through reference chain: com.app.model.user.User["images"])
I read some similar articles and i try to give #JsonIgnore on getter of the Image #Lob image i added #Transactional to service method retrieving elements but it's not working.
I simply want to achieve that kind of message from server:
{
id: "1"
firstName: "test",
lstName: "test_ln",
images: {
{
"id": 10,
"name": "IMG12.jpg",
"type": "image/jpeg"
},
{
"id": 20,
"name": "IMG456.jpg",
"type": "image/jpeg"
}
}
}
The fastest solution (not the best one) is to add fecth = EAGER to the OneToMany images relationship... the problem with this solution is that always will load the images entity (including the byte[] image) when dealing with users entities (which could be a performance problem)...
The next "best" solution is to omit the EAGER configuration described previously, and create a new method in your repository... such method should execute a JPA Query like this:
SELECT ua
FROM
UserAccount ua
LEFT JOIN FECTH ua.images img
WHERE
ua.id = :id
This will load the user and its related images... then in your service, you call such method (the problem with this solution is that loads the byte[] image even if you only want the other attributes of the ImageEntity)
The best solution is to extend the solution #2 to retrieve only the attributes that you want of the ImageEntity, resulting in a query like this:
SELECT
ua,
img.id, img.name, img.type
FROM
UserAccount ua
LEFT JOIN ua.images img
WHERE
ua.id = :id
Then, your repository method should return a JPA Tuple and in your service method you transform that tuple into the User that you want return (including the associated images' metadata) ... (UPDATE) Example (using the method that you indicate in your comments):
// #Transactional // Remove the transactional annotation to avoid cascade issues!
public User getUserById(Long id) {
List<ImageEntity> images;
List<Tuple> tuples;
User user;
tuples = repository.getUserById(id);
user = null;
if (!tuples.isEmpty()) {
user = tuples.get(0).get(0, User.class);
images = new ArrayList<>();
for (Tuple t : tuples) {
if (t.get(1) != null) {
images.add(new ImageEntity(
t.get(1, Long.class),
t.get(2, String.class)
));
}
}
user.setImages(images);
}
return user;
}
In order to this to work, you need:
Modify the signature of the method getUserById (in your repository) to return a List of Tuple
Create a constructor method in the ImageEntity class with this signature: ImageEntity(long id, String name) { ... }
The User Entity should have a method setImages(List<ImageEntity> images) { ... }
UPDATE2: in order to do something like this, while retrieving all users, you will need:
1) Create (or override) a method in the User repository whose query will be like (let's call it, findAll):
SELECT
ua,
img.id, img.name, img.type
FROM
UserAccount ua
LEFT JOIN ua.images img
2) In your service, implement a method like this:
public List<User> findAll(Long id) {
List<ImageEntity> images;
List<Tuple> tuples;
Map<Long, User> index;
tuples = repository.findAll();
index = new HashMap<>();
for (Tuple t : tuples) {
user = t.get(0, User.class);
if (!index.containsKey(user.getId()) {
images = new ArrayList<>();
user.setImages(images);
index.put(user.getId(), user)
} else {
user = index.get(user.getId());
images = user.getImages():
}
if (t.get(1) != null) {
images.add(new ImageEntity(
t.get(1, Long.class),
t.get(2, String.class)
));
}
}
return index.values();
}
EXPLANATION: The key point is we want to retrieve the user with the image metadata (only code, name and type) avoiding to load the lob attribute (because, the images can be MB and they won't be used/serialized) ... that is why we execute a query like this:
SELECT
ua,
img.id, img.name, img.type
FROM
UserAccount ua
LEFT JOIN ua.images img
The LEFT JOIN force to retrieve all user (including those without images)
The ORM (i.e. JPA implementation, for example, hibernate) maps this kind of query to a Tuple Object (always)!
The query produces N x M tuples ... where N are the total of Users and M are total of images... for example, if you have only 1 user with 2 images... the result will be 2 tuples, where first tuple's component is the always the same user, the other components will be the attributes of each images...
Then, you need transform the tuple object to a User Object (this is what we do in the service methods) ... the key point here is the use of a new ArrayList for the images attribute, before adding a new ImageEntity to it ... we need to do this because the ORM injects a proxy list for each User loaded ... if we add something to this proxy, the ORM executes the lazy loading of such proxy, retrieving the associated images (which is something that we want to avoid)...
Related
I'm using Spring boot JPA to get list of objects (Using Java 8 now). Each object has relationships and I use the related objects also to transform to a dto list.
Let's say I have below model classes.
public class Product {
#EmbeddedId
private ProductId id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "userid", referencedColumnName = "USER_ID")
#MapsId("userId")
private User owner;
}
public class User {
#Id
#Column(name = "USER_ID")
private Long userId;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "gp_code", referencedColumnName = "GP_CODE")
#JoinColumn(name = "userid", referencedColumnName = "USER_ID")
private UserGroup userGroup;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumnsOrFormulas(value = {
#JoinColumnOrFormula(formula = #JoinFormula(value = "country_id", referencedColumnName = "COUNTRY_ID")),
#JoinColumnOrFormula(column = #JoinColumn(name = "region_code", referencedColumnName = "REGION_CODE")) })
private Country country;
}
I do query for List<Product> and using stream I'm converting it into a dto object. During which I call the related entity to get the data. I have the below code and works fine unless the list is too much. If I have 1000+ items in the list it takes around 30 seconds.
I believe because of lazy loading this is happening. What is the best way to optimize this?
One option is to do pagination, but I cannot do it. I need all results together.
Is there any way to parallelly execute this? I tried to call parellelStream() instead of stream(), but it's same result.
public List<ProductDTO> getProducts(String countryId) {
//List<Product> products = Query Result
List<ProductDTO> productsList = products.stream().filter(isOwnerFromCountryAndInAnyGroup(countryId))
.map(product -> getProductDTO(product)).collect(Collectors.toList());
}
private Predicate<? super Product> isOwnerFromCountryAndInAnyGroup(String countryId) {
return product -> {
User user = product.getOwner();
return null != user && null != user.getCountry()
&& user.getCountry().getCountryId().equals(countryId) && (null != user.getUserGroup());
};
}
private ProductDTO getProductDTO(Product product) {
ProductDTO productDTO = new ProductDTO();
productDTO.setProductNbr(product.getId().getProductNbr());
productDTO.setPrice(product.getPrice());
productDTO.setOwnerName(product.getOwner().getName());
return productDTO;
}
Edit
I missed to add the line productDTO.setOwnerName(product.getOwner().getName()); for the purpose of asking question here. With query or using filter I'm getting the correct number of results. And with lazy loading, query returns faster and then while calling getOwner() for each row, the process takes time (30 seconds).
And with FethType.EAGER, the query takes similar time(30 seconds) and then processes faster. Either way it is similar time.
To fasten the process, is there any way to execute the stream code block in parallel and collect all results together in list?
public List<ProductDTO> getProducts(String countryId) {
//List<Product> products = Query Result
List<ProductDTO> productsList = products.stream().filter(isOwnerFromCountryAndInAnyGroup(countryId))
.map(product -> getProductDTO(product)).collect(Collectors.toList());
}
From your use case here I am pretty confident that it is not the creation of DTO that takes time. It is that you retrieve a huge set from database (even the complete table of Products) and then you filter for a relation with a specific country just from java.
So Step1 optimization:
If you want to filter for products that are associated with a user from a specific country then this can go on JPA level and translated in optimal way in database. Then the allocation of resources (memory, cpu) would be much more optimal, instead of your java application trying to load a huge data set and filter it there.
#Query("SELECT p FROM Product p where p.owner IS NOT NULL AND p.owner.userGroup IS NOT NULL AND p.owner.country IS NOT NULL AND p.owner.country.id = :countryId")
List<Product> findProductRelatedWithUserFromCountry(#Param String countryId);
and remove the filtering from your method getProducts.
Step2 optimization:
In addition to the above, not only you can pass the java filtering in the database query by moving it to JPA layer but you can also optimize the query a bit more by defining in JPA that you want to load the associated Owner as well so that it doesn't hit later the database to retrieve it when you create the DTO. You can achieve this with join fetch, so your query should now become:
#Query("SELECT p FROM Product p JOIN FETCH p.owner own where p.owner IS NOT NULL AND own.userGroup IS NOT NULL AND own.country IS NOT NULL AND own.country.id = :countryId")
List<Product> findProductRelatedWithUserFromCountry(#Param String countryId);
Step3 optimization:
If we want to take it an extra step further it seems that most times using DTO projections would speed up the execution. This can happen as the query would define only specific information it needs to retrieve and convert into DTO instead of the complete entities.
So your query now would be:
#Query("SELECT new org.your.package.where.dto.is.ProductDTO(p.id.productNbr, p.price, own.name) FROM Product p JOIN FETCH p.owner own where p.owner IS NOT NULL AND own.userGroup IS NOT NULL AND own.country IS NOT NULL AND own.country.id = :countryId")
List<ProductDTO> findProductRelatedWithUserFromCountry(#Param String countryId);
Also remember to have the DTO constructor used in the JPA query available in your ProductDTO.class.
First of all, I haven't written any SQL statements to create a table. I try to use Hibernate/jpa only without writing SQL.
My relation is in the following: A user can have many task, a task only has one user.
I created my models as this:
User Table:
#Entity
#Table(name="T_USER")
#EntityListeners(AuditingEntityListener.class)
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "uid")
private Long uid;
...
}
Task Table:
#Entity
#Table(name = "T_TASK")
#EntityListeners(AuditingEntityListener.class)
public class TASK{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long tid;
#ManyToOne
#JoinColumn(name ="oid")
private User owner;
public User getOwner() {
return owner;
}
public void setOwner(User owner) {
this.owner = owner;
}
...
}
The relation is task's ownerid(oid) is user's uid.
To save a user to my database, I'm using postman with the following parameters:
{
"username": "firstuser",
"email": "firstuser#email.com"
}
To save a task to my database I'm using this:
{
"description": "some description",
"oid": "12" // I also can state username of the user rather than ID
}
However, as I execute save a task, the oid of task is NULL. In data access objects, I have:
#PostMapping("/save")
public QR createTask(#Valid #RequestBody Task task)
{
return taskDAO.save(task);
}
1-What am I doing wrong? I just want to add a task with owner id to database, however it returns null as ownerid.
2-Should I create a table first with SQL using
Create table task(
tid BIGINT,
description VARCHAR(255),
oid BIGINT,
PRIMARY KEY(tid), FOREIGN KEY(oid) REFERENCES (user.uid))
3-Should I change my save method in TaskDAO?
public Task save(Task task)
{
return taskRepository.save(task);
}
4- Should I change my controller method(createTask method using RESTcall)
5- Assume that all of the problems above is fixed. How can I fetch all task that a user has?
6- How can I delete a task when a user is deleted(cascase in SQL, but is there any method in Hibernate)
I hope I explained my problem. Any feedback will be appreciated.
1-What am I doing wrong? I just want to add a task with owner id to database, however it returns null as ownerid
First of all, I would make sure that the owner is being persisted in the db, just to be sure that you have a value to be referencing
2-Should I create a table first with SQL using
Since you're using ORM, writing an SQL query would defeat the purpose of that, you could, but it's not all that necessary, since the relationships are specified already
3-Should I change my save method in TaskDAO?
4- Should I change my controller method(createTask method using RESTcall)
I think it would be best to change your createTask method, you could include the user's id as a pathvariable or a queryparameter and in that method you find the user using their id and set the user field in the task before passing it to the dto to save the value.
The reason the oid is null is because you do not have such a field in there.
5- Assume that all of the problems above is fixed. How can I fetch all task that a user has?
In your task repository, you can create a method like
Collection<Task> findAllTasksByOwnerId(Long id);
6- How can I delete a task when a user is deleted(cascase in SQL, but is there any method in Hibernate)
You can specify the cascade type where you have specified the relationship between the task and the user
You can check this link for a simple tutorial on how to cascade in spring
The oid is null because there is no such field in Task. I think you are mixing two concepts here. The first one is the data transfer object that represents your REST data structure. This one should have an oid field. The second one is the persisting entity. This one you have, it's the Task class.
I would implement a TaskDTO, use Hibernate's session to load the User by its id, then build a Task from the User and the other fields from TaskDTO, then save the Task like you do.
Regarding your other questions
2 - With a create or updatevalue for hibernate.hbm2ddl.auto, Hibernate can generate or update the tables when you start the application.
5 - You could have this interface
#GetMapping("/user/{id}")
public List<TaskDTO> getTasks(#PathVariable Long id)
Then I think you can't escape coding a criteria query of some sort.
6 - This is done with configuring the relation with cascade = CascadeType.ALL
The hibernate builds table and foreign keys automatically.Complex queries we can write in repo/controller in hibernate syntax.
With Crud repository we can delete , create update and read data easily.
for example we have student to course one to many relation.
#OneToMany
#JoinColumn(name = "studentId", referencedColumnName = "studentId", insertable = false, updatable = false)
private List<StudentCourses> studentCourses;
in StudentController I will write
#CrossOrigin(origins = "http://localhost:8090")
#RequestMapping(value = "/registerStudentCourse", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces =
MediaType.APPLICATION_JSON_VALUE )
public StudentCourses registerStudentCourse(#RequestBody StudentCourses studentCourse) {
if (studentCourse != null) {
studentCourse.setLastupdated(new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date()));
studentCourse = studentCourseRepository.save(studentCourse);
}
return studentCourse;
}
#CrossOrigin(origins = "http://localhost:8090")
#RequestMapping(value = "/findStudentCourses", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces =
MediaType.APPLICATION_JSON_VALUE )
public List<StudentCourses> findStudentCourses(#RequestBody String studentId) {
List<StudentCourses> courses = new ArrayList<>();
JSONObject requestedJSONObject;
try {
requestedJSONObject = new JSONObject(studentId);
String student = requestedJSONObject.getString("studentId");
courses =
studentCourseRepository.findCoursesByStudent(Long.parseLong(student));
} catch (JSONException e) {
// TODO Auto-generated catch block
}
return courses;
}
I'm trying to use JPA (with Hibernate) to save 2 entities. Spring data is providing the interface but I don't think it matters here.
I have a main entity called 'Model'. This model has many 'Parameter' entities linked. I'm writing a method to save a model and its parameters.
This is the method:
private void cascadeSave(Model model) {
modelRepository.save(model);
for (ParameterValue value : model.getParameterValues()) {
parameterValueRepository.save(value);
}
}
This is the problem:
When I load a Model that already existed before, add some new parameters to it and then call this method to save both of them something strange happens:
Before the first save (modelRepository.save) this is what the model's data looks like when debugging:
The model has 2 parameters, with filled in values (name and model are filled).
Now, after saving the model the first save in my method, this happens. Note that the object reference is a different one so Hibernate must have done something magical and recreated the values instead of leaving them alone:
For some reason hibernate cleared all the attributes of the parameters in the set.
Now when the saving of the new parameters happens in the following code it fails because of not null constraints etc.
My question: Why does hibernate clear all of the fields?
Here are the relevant mappings:
ParameterValue
#Entity
#Table(name = "tbl_parameter_value")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "PARAMETER_TYPE")
public abstract class ParameterValue extends AbstractBaseObject {
#Column(nullable = false)
#NotBlank
private String name;
private String stringValue;
private Double doubleValue;
private Integer intValue;
private Boolean booleanValue;
#Enumerated(EnumType.STRING)
private ModelType modelParameterType;
#Column(precision = 7, scale = 6)
private BigDecimal bigDecimalValue;
#Lob
private byte[] blobValue;
ParameterValue() {
}
ParameterValue(String name) {
this.name = name;
}
ModelParameterValue
#Entity
#DiscriminatorValue(value = "MODEL")
public class ModelParameterValue extends ParameterValue {
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "model_id", foreignKey = #ForeignKey(name = "FK_VALUE_MODEL"))
private Model model;
ModelParameterValue() {
super();
}
ModelParameterValue(String name) {
super(name);
}
Model
#Entity
#Table(name = "tbl_model")
public class Model extends AbstractBaseObject implements Auditable {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "model")
private Set<ModelParameterValue> parameterValues = new HashSet<>();
EDIT
I was able to reproduce this with a minimal example.
If you replace everything spring data does this is what happened under the hood (em is a JPA EntityManager):
public Model simpleTest() {
Model model = new Model("My Test Model");
em.persist(model);
model.addParameter(new Parameter("Param 1"));
em.merge(model);
for (Parameter child : model.getParameters()) {
em.persist(child);
}
return model;
}
When the merge is executed, all of the attributes of the parameters are set to null. They are actually just replaced with completely new parameters.
I guess you are using Spring Data Jpa as your modelRepository. This indicates following consequences.
Spring Repository Save
S save(S entity)
Saves a given entity. Use the returned
instance for further operations as the save operation might have
changed the entity instance completely.
So it is normal behaviour which you had encountered.
Code should be changed to :
model = modelRepository.save(model);
for (ParameterValue value : model.getParameterValues()) {
parameterValueRepository.save(value);
}
EDIT:
I think that your saving function is broken in sense, that you do it backwards. Either you can use CascadeType on your relation or you have to save children first.
Cascade
Cascade works like that "If you save Parent, save Children, if you update Parent, update Children ..."
So we can put cascade on your relation like that :
#Entity
#Table(name = "tbl_model")
public class Model extends AbstractBaseObject implements Auditable {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "model", cascade = CascadeType.ALL)
private Set<ModelParameterValue> parameterValues = new HashSet<>();
and then only save like this
private void cascadeSave(Model model) {
modelRepository.save(model);
//ParamValues will be saved/updated automaticlly if your model has changed
}
Bottom-Up save
Second option is just to save params first and then model with them.
private void cascadeSave(Model model) {
model.setParameterValues(
model.getParameterValues().stream()
.map(param -> parameterValueRepository.save(param))
.collect(Collectors.toSet())
);
modelRepository.save(model);
}
I haven't checked second code in my compiler but the idea is to first save children (ParamValues), put it into Model and then save Model : )
I am developing an application with Struts2, Spring and Hibernate and I'm working on models fetch optimization. For this example, consider that the "example_table" may have more than 500k records, and all his OneToMany relation may have many more records (ex. document table in relation with document_row).
Here is the example code:
#Entity
#Table(name = "example")
public class Example extends BaseModel { // Base Model is mapped as superclass and contains the Id column and create,update,delete timestamps
private String exampleName;
/*
* ...
*/
/*
* This relation contains another relation inside it
* (ex. Set<ExampleRelationRelation> exampleRelationRelations)
*/
private ExampleRelation1 exampleRelation1;
private Set<ExampleRelation2> exampleRelations2;
// COSTRUCTORS --------------------------------------------------------
/*
* Entity constructors
*/
// GETTER AND SETTER --------------------------------------------------
#Column(name = "exampleName")
public String getExampleName() {
return exampleName;
}
public void setExampleName(String exampleName) {
this.exampleName = exampleName;
}
/*
* ...
*/
#OneToOne(mappedBy = "example_relation_1", cascade = CascadeType.ALL, fetch = FetchType.LAZY, targetEntity = ExampleRelation1.class)
#NotFound(action = NotFoundAction.IGNORE)
public ExampleRelation1 getExampleRelation1() {
return exampleRelation;
}
public void setExampleRelation1(ExampleRelation1 exampleRelation1) {
this.exampleRelation1 = exampleRelation1;
}
#OneToMany(mappedBy = "example", targetEntity = ExampleRelation2.class, fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#NotFound(action = NotFoundAction.IGNORE)
public Set<ExampleRelation2> getExampleRelation2() {
return exampleRelation2;
}
public void setExampleRelation2(Set<ExampleRelation2> exampleRelation2) {
this.exampleRelation2 = exampleRelation2;
}
}
In my application I might need to load Example with ExampleRelation1 and his child relation exampleRelationRelations, but not always. Maybe next time I have to load Example with no relations or just with exampleRelation1 and exampleRelation2.
The best solution I've found is to implement the Fetch profiles in order to dynamically join the table with the relations I need. With this solution I can tell to Struts2 controller to retrieve the data I need.
Is this a good solution? or should i use other strategies?
* EDIT *
I've found also this question, it may be a solution?
Fetch Profiles can be an answer for your problem.
But you can control this behaviour on code, just lazy loading the entities you want calling the right method each time inside a transaction.
I might need to load Example with ExampleRelation1 and his child relation exampleRelationRelations
So, you can use something like that:
#Transactional // from Spring
public Example getExampleWithExampleRelation1AndExampleRelationRelations(String exampleName) {
Example example = em.find(Example.class, exampleName);
ExampleRelation1 exampleRelation1 = example.getExampleRelation1(); //lazy load
exampleRelation1.exampleRelationRelations().size(); //lazy load list
return example;
}
Maybe next time I have to load Example with no relations or just with exampleRelation1 and exampleRelation2.
Just create another method and call it:
#Transactional // from Spring
public Example getExampleWithExampleRelation1AndExampleRelation2(String exampleName) {
Example example = em.find(Example.class, exampleName);
example.getExampleRelation1(); //lazy load
example.getExampleRelation2(); //lazy load
return example;
}
You can also use JPQL and join with the FETCH word to bring the entities using only one query, like:
String jpql = "SELECT e FROM Example e " +
"JOIN FETCH e.ExampleRelation1" +
"JOIN FETCH e.ExampleRelation2 ";
Lets say we have User entity class. User can be friends with other users. How can i map this self-reference collection field without creating a new entity called Connection or creating multiple entries in the database?
#Entity
public class User {
...
#ManyToMany
private Collection<User> friends;
...
}
USER_ID-FRIEND_ID
1 - 2
2 - 1 (duplicate... I don't need it)
Following is snapshot from my code for ElementEntity:
#OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
private List<ElementEntity> children;
#JoinColumn(name = "ParentId", referencedColumnName = "ElementId")
#ManyToOne(fetch = FetchType.LAZY)
private ElementEntity parent;
Where on database there are fields:
ElementId - primary key;
ParentId relation with parent
You can't - you need both records in the database.
Actually, for friendship relations, I'd say that a graph database like neo4j is the proper thing to use. There you have the two users and simply add an edge "friends".
At least you will need a relational table.
So you have a USER table and a FRIENDS:
user_id friend_id
1 2
But #Bozho answer is way better than mine (neo4j).
Well, in fact you can.
You can use annotations like #PreUpdate, #PrePersists, #PostUpdate and so to convert manually the elements of a collection. This way your entity can render then them way you want while in database you only store a raw text.
A more pausible alternative will be to use #Convert annotation, available since jpa 2.1 (#UserType in hibernate). It tells jpa to convert the field into another type everytime it read/save in database.
For it you should use #Convert anotation, specifying and AttributeConverter object.
For example
public class Parent {
#Id
private Integer id;
#Convert(converter = FriendConverter.class)
private Set<Parent>friends;
}
And converter class like the following:
#Component
public class FriendConverter implements AttributeConverter<List, String>{
#Autowired
private SomeRepository someRepository;
#Override
public String convertToDatabaseColumn(List attribute) {
StringBuilder sb = new StringBuilder();
for (Object object : attribute) {
Parent parent = (parent) object;
sb.append(parent.getId()).append(".");
}
return sb.toString();
}
#Override
public List convertToEntityAttribute(String dbData) {
String[] split = dbData.split(".");
List<Parent>friends = new ArrayList<>();
for (String string : split) {
Parent parent = someRepository.findById(Integer.valueOf(string));
friends.add(accion);
}
return friends;
}
}
It is a dummy implementation but it gives you the idea.
As a personal comment, I do recommend to map the relationship as it should. In the future it will avoid you problems. AttributeConverter comes in handy when working with enums