1

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"
    }
 }
}
Piter _OS
  • 105
  • 2
  • 11

1 Answers1

5

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:

  1. Modify the signature of the method getUserById (in your repository) to return a List of Tuple
  2. Create a constructor method in the ImageEntity class with this signature: ImageEntity(long id, String name) { ... }
  3. 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)...

Carlitos Way
  • 3,279
  • 20
  • 30
  • So i did the third option, but first of all I added @JsonIgnore to images field. I write this: @Query("SELECT re, img.id, img.name FROM User re JOIN re.images img WHERE re.id = :id") Optional getUserById(@Param("id") Long id); but i get this JSON { "id":"1", "firstName"; "test", "lastName": "test_ln" } 42, IMG12.jpg Any advice how to fix this to have JSON structure presented in main question? { "id":"1", "firstName"; "test", "lastName": "test_ln", "images":{"id": 42, "name":"IMG12.jpg"} } ?? – Piter _OS Jan 02 '19 at 23:54
  • also this only works when i got one image. When i added 4 images then i got NonUniqueResultException: query did not return a unique result: 4 – Piter _OS Jan 03 '19 at 00:01
  • I modified the queries (and the transformation method) to manage the case when the user does no have any images! – Carlitos Way Jan 03 '19 at 00:23
  • 1
    I guess that you forgot to write user.addImage(new ImageEntity()). I did that and it's not working. I get `Caused by: org.hibernate.HibernateException: Unable to access lob stream org.postgresql.util.PSQLException: Large Objects may not be used in auto-commit mode.` during adding new ImageEntity to User – Piter _OS Jan 03 '19 at 00:35
  • Did you remove the Transactional annotation? – Carlitos Way Jan 03 '19 at 00:38
  • yes i did. Also i tried with and without @JsonIgnore on images field in user class and it's not working :( – Piter _OS Jan 03 '19 at 00:39
  • Sorry, according to this: https://stackoverflow.com/questions/3164072/large-objects-may-not-be-used-in-auto-commit-mode, you need add the `Transactional` annotation (in order to retrieve lobs)!!! But, change the cascade type to Persist Only! or detach the user entity before adding the images!! – Carlitos Way Jan 03 '19 at 00:40
  • what do you mean by "detaching the user entity before addding the images" ? I add @Transactional and i change cascade and i get org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist: com.app.model.image.ImageEntity; org.hibernate.PersistentObjectException: detached entity passed to persist: com.app.model.image.ImageEntity – Piter _OS Jan 03 '19 at 00:46
  • Sorry, In Oracle this works like a charm, but I want to help you... I feel that we are getting close ... So, let's try this ... remove the `Transactional` annotation and update the code as I suggested (creating the additional image list and setting to the user later) – Carlitos Way Jan 03 '19 at 00:53
  • 1
    BTW, when you use `@Transactional` annotation, you have to be careful what you do with the objects that you retrieve from the database, specially, if those objects have permissive cascade types ... if you not are careful, in our example, you end with adding new images to the retrieve user ... that is why I told you to: either remove the `@Transactional` annotation or detach the User entity from the entittyManager (in order to not modify the entity per se) – Carlitos Way Jan 03 '19 at 00:58
  • ok it works great :) thank you a lot. But i also would like to ask how to retrieve all users in that manner because now endpoint for all users fails :/ And also could you explain is couple words why you solve this that way? – Piter _OS Jan 03 '19 at 01:02
  • 1
    I added an example for how to do this for the /users endpoint – Carlitos Way Jan 03 '19 at 01:33
  • It works also great. Thank you very much for all your help :) – Piter _OS Jan 03 '19 at 01:47