0

I have a case where I'm persisting a large jsonb field into a PostGres table, but do not want to read it when I fetch the entity; if I do fetch it, my service goes OOM. A better design might be to separate this into a 1 to 1 table, but I can't do that at this time.

To plead that this is not a duplicate question, here's some of my research:

  1. I'm not able to mark the column LAZY since I have a simple column not a join` JPA/Hibernate write only field with no read
  2. I tried the empty setter in this suggestion, which makes sense - but it still appears to read the column and I OOM: https://www.zizka.ch/pages/programming/java/hibernate/hibernate-write-only.html
  3. I also tried omitting the setter altogether in my @Data class: Omitting one Setter/Getter in Lombok

So, I can not see the field, but I can't seem to keep it from being read into memory in the background. It seems like there must be some simple setting in JPA or Hibernate to exclude a column from read. Before I go try to make a complex repository hierarchy just to see if it works, I thought I would ask here in case I get lucky.

Thanks in advance!

orbfish
  • 7,381
  • 14
  • 58
  • 75
  • 1
    The super-easy solution is to create two entity classes for the same table, one with the field and one without it. Then save the entity that has the field but never read the entity using that class. Alternatively I'm pretty sure that Hibernate can make a single field lazy if you use byte code enhancement even if the field is in the same table. EDIT: with @Basic(fetch=FetchType.LAZY). – ewramner Jun 11 '22 at 17:43
  • @ewramner Oddly I just saw the bit about `@Basic` and tried it, did no good. Also tried 2 entities already, can't get the repositories right to read the same table. But thanks! – orbfish Jun 11 '22 at 22:01
  • Surprisingly, since all the docs say to make a class hierarchy, the 2 entity/2 repository method seemed to compile and run OK. Testing. Also, `@Basic` didn't work with the hibernate enhancer turned on, but the buildfile/plugin seems to be making a difference – orbfish Jun 13 '22 at 23:47

1 Answers1

2

Lazy loading attributes

Hibernate can load attribute lazily, but you need to enable byte code enhancements:

  1. First you need to set the property hibernate.enhancer.enableLazyInitialization to true

  2. Then you can annotate the field with @Basic( fetch = FetchType.LAZY ). Here's the example from the documentation:

    @Entity
    public class Customer {
    
      @Id
      private Integer id;
    
      private String name;
    
      @Basic( fetch = FetchType.LAZY )
      private UUID accountsPayableXrefId;
    
      @Lob
      @Basic( fetch = FetchType.LAZY )
      @LazyGroup( "lobs" )
      private Blob image;
    
      //Getters and setters are omitted for brevity
    }
    

You can also enable this feature via the Hibernate ORM gradle plugin

Named Native queries

You could also decide to not map it and save/read it with a named native query. It seems a good trade off for a single attribute - it will just require an additional query to save the json.

Example:

@Entity
@Table(name = "MyEntity_table")
@NamedNativeQuery(
    name = "write_json",
    query = "update MyEntity_table set json_column = :json where id = :id")
@NamedNativeQuery(
    name = "read_json",
    query = "select json_column from MyEntity_table where id = :id")
class MyEntity {
....
}

Long id = ...
String jsonString = ...
session.createNamedQuery( "write_json" )
    .setParameter( "id", id )
    .setParameter( "json", jsonString )
    .executeUpdate();

jsonString = (String)session.createNamedQuery( "read_json" )
    .setParameter( "id", id )
    .getSingleResult();

In this case, schema generation is not going to create the column, so you will need to add it manually (not a big deal, considering that there are better tools to update the schema in production).

MappedSuperclass

You can also have two entities extending the same superclass (this way you don't have to copy the attributes). They have to update the same table:

@MappedSuperclass
class MyEntity {

   @Id
   Long id;
   String name
   ...
}

@Entity
@Table(name = "MyEntity_table")
class MyEntityWriter extends MyEntity {

   String json
}

@Entity
@Table(name = "MyEntity_table")
class MyEntityReader extends MyEntity {
   // No field is necessary here
}

Now you can use MyEntityWriter for saving all the values and MyEntityReader for loading only the values you need.

I think you will have some problems with schema generation if you try to create the tables because only one of the two will be created:

  • If MyEntityWriter is the first table created, then no problem
  • If MyEntityWriter is the second table created, the query will fail because the table already exist and the additional column won't be created.

I haven't tested this solution though, there might be something I haven't thought about.

Davide D'Alto
  • 7,421
  • 2
  • 16
  • 30
  • Thanks for all the good info! I already tried `@Basic` based on the Hibernate documentation, but it didn't mention the supporting configuration so maybe that will do it. Would this *only* affect the field I annotate? (The rest of the application is sensitive to performance changes). Also tried 2 subclasses, but can't get it to work without extensive fiddling with Lombok, so set this aside for now. – orbfish Jun 11 '22 at 22:04
  • Also the Named Native Query link pointed me to docs about SELECT named queries. Do you know if there is comparable functionality for insert? I read way down the doc and don't see that. Thank you! – orbfish Jun 11 '22 at 22:09
  • Yes, it should only affect the fields annotated with `@Basic` – Davide D'Alto Jun 11 '22 at 23:25
  • Yes, it works the same, in one case you go with `createNamedQuery(...).getSingleResult()`, in the other `createNamedQuery(...).executeUpdate()`. I've updated the answer – Davide D'Alto Jun 11 '22 at 23:40
  • Unfortunately lazy loading didn't work - would have been perfect, since it would have meant minimal mucking with the entity code. Trying other things. – orbfish Jun 13 '22 at 00:06
  • Got it to work. I set the config for bytecode enhancement, but apparently you need the gradle plugin as well. Thank you, it's a kinda cool feature! – orbfish Jun 17 '22 at 16:13