Thursday, October 5, 2017

Java JPA Preventing Proxies from calling db

Leave a Comment

I have a spring boot (1.5.4.RELEASE) project using Java 8. I have an entity and it's related domain class like this:

@Entity @Table(name = "Foo", schema = "dbo") public class FooEntity implements Serializable {     private static final long serialVersionUID = 1L;      @Id     @GeneratedValue(strategy = GenerationType.IDENTITY)     @Column(name = "Id")     private int id;      @Column(name="Name")     private String name;      @Column(name="Type")     private String type;      @Column(name="Color")     private String color;      @ManyToOne(fetch = FetchType.LAZY)     @JoinColumn(name = "Car")     private Car car;      //getter and setter }  public class Foo {     private int id;     private String name;     private String type;     private String color;     private Car car;      //Constructors and getters } 

I want to create a repository that fetches this Foo object from the DB but only fetching the complex fields if the user asks for them to prevent unnecessary join statements. The repo looks like this:

import static com.test.entities.QFooEntity.fooEntity; import static com.test.entities.QCarEntity.carEntity;  @Repository public class FooRepository {     private final JPAQuery<FooEntity> query = createQuery().from(fooEntity);      public FooRepository getFooByName(String name) {         query.where(fooEntity.name.eq(name));         return this;     }      public FooRepository withCar() {         query.leftJoin(fooEntity.car, carEntity).fetchJoin();         return this;     }      public Foo fetch() {         FooEntity entity = query.fetchOne();         return FooMapper.mapEntityToDomain().apply(entity);     } } 

So a barebones call for a Foo object will return the Entity with values for all the fields except for the car field. If the user wants car information then they have to explicitly call withCar.

Here is the mapper:

public class FooMapper {     public static Function<FooEntity, Foo> mapEntityToDomain() {         return entity -> {              return new Foo(e.getId(), e.getName(), e.getType(), e.getColor(), e.getCar());         };     } } 

The problem is when you do e.getCar() if the value is not there (i.e. there's a proxy present) JPA will go out and fetch it for you. I don't want this to be the case. It will just grab the values and map them to the domain equivalent if it's not there then null.

One solution that I've heard (and tried) is calling em.detach(entity); however, this doesn't work as I intended because it throws an exception when you try to access getCar and I've also heard this is not best practice.

So my question is what is the best way to create a repo using a builder pattern on a JPA entity and not have it call the DB when trying to map.

7 Answers

Answers 1

You could create a utility method that will return null if the given object is a proxy and is not initialized:

public static <T> T nullIfNotInitialized(T entity) {     return Hibernate.isInitialized(entity) ? entity : null; } 

Then you can call the method wherever you need it:

return new Foo(e.getId(), e.getName(), e.getType(), e.getColor(), nullIfNotInitialized(e.getCar())); 

Answers 2

Just map it to a new object and leave out the Car relation, this is the standard approach. You can use MapStruct and just ignore the car field during mapping: http://mapstruct.org/documentation/stable/reference/html/#inverse-mappings

Answers 3

Just don't map the car... Map a field holding the ID and use another method to get the actual Car. I would use a distinctive method name, to differentiate it from the other getters.

class FooEntity {     @Column     private int carId;      public int getCarId() {          return carId;      }      public void setCarId(int id) {          this.carId = id;      }      public Car fetchCar(CarRepository repo) {         return repo.findById(carId);     }          } 

Answers 4

You can write query on top of JPA

@Query("select u from Car c")    import org.springframework.data.repository.CrudRepository;  import com.example.model.FluentEntity;  public interface DatabaseEntityRepository extends CrudRepository<FooEntity , int > {  } 

Answers 5

As you said

I don't want this to be the case. It will just grab the values and map them to the domain equivalent, if it's not there then null.

Then you just set it to null, because the field car will always not be there.

Otherwise, if you mean not there is that the car not exists in db, for sure a subquery(call the proxy) should be made.

If you want to grab the car when call Foo.getCar().

 class Car {   }   class FooEntity {       private Car car;//when call getCar() it will call the proxy.       public Car getCar() {           return car;      }  }   class Foo {      private java.util.function.Supplier<Car> carSupplier;        public void setCar(java.util.function.Supplier<Car> carSupplier) {          this.carSupplier = carSupplier;      }       public Car getCar() {          return carSupplier.get();      }  }   class FooMapper {      public static Function<FooEntity, Foo> mapEntityToDomain() {          return (FooEntity e) -> {              Foo foo = new Foo();              foo.setCar(e::getCar);              return foo;          };      }  } 

Make sure you have the db session ,when you call Foo.getCar()

Answers 6

You could try adding state to your repository and influence the mapper. Something like this:

import static com.test.entities.QFooEntity.fooEntity; import static com.test.entities.QCarEntity.carEntity;  @Repository public class FooRepository {     private final JPAQuery<FooEntity> query = createQuery().from(fooEntity);     private boolean withCar = false;      public FooRepository getFooByName(String name) {         query.where(fooEntity.name.eq(name));         return this;     }      public FooRepository withCar() {         query.leftJoin(fooEntity.car, carEntity).fetchJoin();         withCar = true;         return this;     }      public Foo fetch() {         FooEntity entity = query.fetchOne();         return FooMapper.mapEntityToDomain(withCar).apply(entity);     } } 

In your mapper, you then include a switch to enable or disable car lookups:

public class FooMapper {     public static Function<FooEntity, Foo> mapEntityToDomain(boolean withCar) {         return e -> {              return new Foo(e.getId(), e.getName(), e.getType(), e.getColor(), withCar ? e.getCar() : null);         };     } } 

If you then use new FooRepository().getFooByName("example").fetch() without the withCar() call, e.getCar() should not be evaluated inside FooMapper

Answers 7

You may want to use the PersistentUnitUtil class to query if an attribute of entity object is already loaded or not. Based on that you may skip the call to corresponding getter as shown below. JpaContext you need to supply to user entity bean mapper.

PersistenceUnitUtil putil = obtainPersistentUnitUtilFor(context, FooEntity.class);     return e -> {         return new Foo(                 e.getId(),                 e.getName(),                 e.getType(),                 e.getColor(),                 putil.isLoaded(e, "car") ? e.getCar() : null);     }; }  private PersistenceUnitUtil obtainPersistentUnitUtilFor(JpaContext context, Class<?> entity) {     return context.getEntityManagerByManagedType(entity)             .getEntityManagerFactory()             .getPersistenceUnitUtil(); } 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment