2

I create one-to-one relationship and I want to have methods that fetch the same entity(Distributor.class) but one does lazy fetch, other eager.

@Entity
@Table
public class Distributor {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String site;

    @OneToOne(
            mappedBy = "distributor",
            cascade = CascadeType.ALL,
            orphanRemoval = true,
            fetch = FetchType.LAZY,
            optional = false
    )
    private Location location;

    public void setLocation(Location repositoryLocation) {
        if (repositoryLocation == null) {
            if (this.location != null) {
                this.location.setDataProvider(null);
            }
        }
        else {
            repositoryLocation.setDataProvider(this);
        }
        this.location = repositoryLocation;

    }

// getters/setters/constructor

    }

}
@Entity
@Table(name = "location")
public class Location {

    @Id
    private Long id;

    @Column(name = "country_code")
    private String countryCode;

    private Double longitude;

    private Double latitude;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    @JoinColumn(name = "id")
    private Distributor distributor;

    public Distributor getDistributor() {
        return distributor;
    }

    public void setDistributor(Distributor distributor) {
        this.distributor = distributor;
    }
// other getters/setters
}

Biggest problem I encounter is that it seems that Spring Data JPA ignores FetchType and fetches all related tables eagerly(based on related threads and Spring Data JPA docs). All Spring Data Repository methods that get data using Distributor.class fetch Location eagerly in two selects, from Distributor and Location. By using @NamedEntityGraph as such:

@Entity
@Table
@NamedEntityGraph(
        name = "distributor-entity-graph",
        attributeNodes = {
                @NamedAttributeNode("location"),
        }
)
public class Distributor {
//spring data jpa methods
  @EntityGraph(value = "distributor-entity-graph", type = EntityGraph.EntityGraphType.LOAD)
    Optional<Distributor> findById(Long id);

I get single left outer join on Location if use such a graph and though it's a better type of eager load this is still eager load.

Everything I have found so far seems like bad workarounds. Is there some more or less neat way of doing this or maybe is it better(in terms of performance first of all) just to not create the relationship and just fetch Location on demand? Eager load is a smell but when I fetch single distributor I want to do just that in most of the cases, but in some cases I don't and especially if I do findAll().

improbable
  • 2,308
  • 2
  • 15
  • 28
  • 1
    What hibernate version do you use? – SternK May 26 '20 at 12:03
  • In this test env I use latest Spring version 2.3.0.RELEASE, which uses hibernate v.5.4.15.Final(everything was tested in test env), in prod hibernate v.5.2.17 final(earlier Spring). – improbable May 26 '20 at 12:16

1 Answers1

3

Hibernate ignores LAZY on the parent-side of every bi-directional one-to-one mapping that has mappedBy set (or uses JoinColumn and is optional), because it needs to know whenever to initialize the field with null or proxy.

Your problem is, even though you use MapsId on the child (Location), the parent still uses mappedBy.

Here is how you could achieve lazy loaded bi-directional association (it has it's downsides though, read till the end).

Parent

@Entity
public class TestParent {

  @Id
  @GeneratedValue(strategy = IDENTITY)
  private Long id;

  @OneToOne(fetch = FetchType.LAZY, optional = false)
  @JoinColumn(name = "id")
  private TestChild child;
}
  • child field is not mandatory, it is a bit hacky and you could just ignore it and fetch the child when needed using the repository - you know it's id
  • Because this is not standard parent-to-child mapping, you cannot specify any cascades on the child field (which will reflect on the way you persist them, so read till the end), otherwise the persist will fail because it won't be able to assign id to the child entity.
  • optional = false is mandatory, otherwise it will be eagerly fetched anyway.

Child

public class TestChild {

  @Id
  private Long id;

  @OneToOne(fetch = FetchType.LAZY)
  @MapsId
  @JoinColumn(name = "id")
  private TestParent parent;
}

This is how you defined your child, nothing important has changed.

Persisting

EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();

TestParent parent = new TestParent();
TestChild child = new TestChild();
parent.setChild(child);
child.setParent(parent);

em.persist(parent);
em.persist(child);

em.getTransaction().commit();
em.close();

Since the parent no longer cascades to the child entity, you have to persist the child separately.

Read more.

Kamil Bęben
  • 1,064
  • 8
  • 12
  • Described in Vlad Mihalcea's article approach does not work for [hibernate 5.3](https://stackoverflow.com/questions/60597259/). I have not tested it for v.5.2 branch. According to the [documentation](https://docs.jboss.org/hibernate/stable/orm/userguide/html_single/Hibernate_User_Guide.html#associations-one-to-one-bidirectional-lazy): `if you really need to use a bidirectional association and want to make sure that this is always going to be fetched lazily, then you need to enable lazy state initialization bytecode enhancement and use the @LazyToOne annotation as well` – SternK May 27 '20 at 07:24
  • Good to know, it was a bug and was fixed in 5.4 though – Kamil Bęben May 27 '20 at 09:21
  • The thing is I can't assign null to the location this way as well as I can't assign a location object with null fields(results in `org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Referential integrity constraint violation: "FKL1QA7PX59VE6IOSS7PNVYIPHD: PUBLIC.REPOSITORY FOREIGN KEY(ID) REFERENCES PUBLIC.LOCATION(ID) (4)"; SQL statement: `) given I persist Distributor(parent class) via Spring Data. – improbable May 27 '20 at 20:45
  • Oh, true. In that case, the only way you could make lazily loaded one-to-one association is using bytecode enhancement and `@LazyToOne`. Keep in mind that turning bytecode enhancement can be pain in the ass if your project is already big (it might be time consuming). – Kamil Bęben May 27 '20 at 23:20
  • This answer is indeed useful but it doesn't answer my original question. This answer works only if used with pure Hibernate. – improbable Jun 01 '20 at 15:03