Blazebit / blaze-persistence

Rich Criteria API for JPA providers
https://persistence.blazebit.com
Apache License 2.0
727 stars 85 forks source link

[question] Not a managed type #427

Closed aol-nnov closed 7 years ago

aol-nnov commented 7 years ago

Hello!

Just stumbled upon your project and it looks quite interesting, however, I'm having a hard time wrapping my head around it. Please, help!

I use spring-data-jpa in my project and everything worked without a glitch before I've added blaze-persistence.. My entities have UUID PK and while constructing CriteriaBuilderFactory

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    @Lazy(false)
    public CriteriaBuilderFactory createCriteriaBuilderFactory() {
        CriteriaBuilderConfiguration config = Criteria.getDefault();
        return config.createCriteriaBuilderFactory(entityManagerFactory);
    }

I end up with exception:

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.blazebit.persistence.CriteriaBuilderFactory]: Factory method 'createCriteriaBuilderFactory' threw exception; nested exception is java.lang.IllegalArgumentException: Not a managed type: class java.util.UUID
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:189) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:588) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
    ... 44 common frames omitted
Caused by: java.lang.IllegalArgumentException: Not a managed type: class java.util.UUID
    at org.hibernate.jpa.internal.metamodel.MetamodelImpl.managedType(MetamodelImpl.java:210) ~[hibernate-entitymanager-5.0.12.Final.jar:5.0.12.Final]

Please advise how to overcome this issue!

Thanks in advance, Andrey

beikov commented 7 years ago

Sorry you are experiencing issues. It might be that the UUID type is considered as being a managed type. I'll have a look later and let you know. If you could replace the id with a long or int in the meantime you should be able to test further.

aol-nnov commented 7 years ago

@beikov Christian, thank you for a quick reply!

unfortunately, changing UUID to other type is not an option for me. some entities are created offline and later synchronised to the central db. Maintaining sequential keys is a nightmare in this scenario.

aol-nnov commented 7 years ago

Btw, if it helps, here is how I'm using UUID:

@MappedSuperclass
@Data
public class Identifiable {
    @Id
    @GenericGenerator(name = "uuid-gen", strategy = "uuid2")
    @GeneratedValue(generator = "uuid-gen")
    @Type(type = "pg-uuid")
    private UUID id;
}

and Hibernate reports, that type as registered.

[           main] org.hibernate.type.BasicTypeRegistry     : HHH000270: Type registration [java.util.UUID] overrides previous : org.hibernate.type.UUIDBinaryType@7f9e8421
beikov commented 7 years ago

I just tried to reproduce your issue but I am unable to. Can you maybe post the library versions that you are using?

aol-nnov commented 7 years ago

Christian, there is a big chance that I'm doing something wrong.. Seems, it was a mistake to plug it into existing project as I'm unable to provide you with minimal example now..

Anyway, library versions.. Spring boot 1.5.4.RELEASE with their dependency management plus

    compile('com.blazebit:blaze-persistence-integration-spring-data:1.2.0-Alpha3')
    compile('com.blazebit:blaze-persistence-integration-entity-view-spring:1.2.0-Alpha3')
    compile('com.blazebit:blaze-persistence-jpa-criteria-api:1.2.0-Alpha3')

    runtime('com.blazebit:blaze-persistence-integration-hibernate-5.2:1.2.0-Alpha3')
    runtime('com.blazebit:blaze-persistence-jpa-criteria-impl:1.2.0-Alpha3')

I'm using postgresql server.

beikov commented 7 years ago

Since you seem to use Hibernate 5.0 could you please switch from blaze-persistence-integration-hibernate-5.2 to blaze-persistence-integration-hibernate-5 ?

aol-nnov commented 7 years ago

Unfortunately, nothing changed after switching to

runtime('com.blazebit:blaze-persistence-integration-hibernate-5:1.2.0-Alpha3')

still the same stacktrace

aol-nnov commented 7 years ago

So, I've nailed it, @beikov !

It's due to composite key in my entity, which is defined as follows:

@Data
@Entity
@IdClass(Availability.AvailabilityPK.class)
public class Availability implements Serializable {
    private static final long serialVersionUID = -5432028594228229220L;

    @Id
    @ManyToOne(optional = false)
    private Item item;

    @Id
    @ManyToOne(optional = false)
    private Warehouse warehouse;

    private BigDecimal amount;

    @Data
    @NoArgsConstructor
    @EqualsAndHashCode
    public static class AvailabilityPK implements Serializable {
        private static final long serialVersionUID = 6423911231290364791L;

        private UUID item;
        private UUID warehouse;
    }
}

Blaze-persistence config is a copy-paste from github readme:

@Configuration
public class BlazePersistenceConfig {
    @PersistenceUnit
    private EntityManagerFactory entityManagerFactory;

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    @Lazy(false)
    public CriteriaBuilderFactory createCriteriaBuilderFactory() {
        CriteriaBuilderConfiguration config = Criteria.getDefault();
        // do some configuration
        return config.createCriteriaBuilderFactory(entityManagerFactory);
    }

    @Bean
    public EntityViewConfiguration entityViewConfiguration() {
        return EntityViews.createDefaultConfiguration();
    }

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    @Lazy(false)
    // inject the criteria builder factory which will be used along with the entity view manager
    public EntityViewManager createEntityViewManager(CriteriaBuilderFactory cbf, EntityViewConfiguration entityViewConfiguration) {
        return entityViewConfiguration.createEntityViewManager(cbf);
    }
}
aol-nnov commented 7 years ago

If you would like, I can attach that minimal example, but anyway, everything you need to reproduce the issue is already in the ticket.

beikov commented 7 years ago

Could you try to add an @EmbeddedId attribute instead of using the @IdClass? Id classes are not very well tested and it seems to be because of the usage.

beikov commented 7 years ago

I think you would need to annotate the relations item and warehouse as being insertable = false and updatable = false in order to make this work.

aol-nnov commented 7 years ago

Well, while moving from @IdClass to @EmbeddedId I've found a SO question.

Just removed @IdClass and AvailabilityPK all together, still having CONSTRAINT availability_pkey PRIMARY KEY (warehouse_id, item_id) on a table and CriteriaBuilderFactory instantiated just fine!

Let's see what I'll miss due to this move.

aol-nnov commented 7 years ago

after those changes Availability looks as follows:

@Data
@Entity
public class Availability implements Serializable {
    private static final long serialVersionUID = -5432028594228229220L;

    @Id
    @ManyToOne(optional = false)
    private Item item;

    @Id
    @ManyToOne(optional = false)
    private Warehouse warehouse;

    private BigDecimal amount;
}
beikov commented 7 years ago

I think you need an embedded id to benefit from the "single valued id access" optimizations that allows to avoid a join when filtering by FK columns. Imagine you have a class like

@Data
@Entity
public class Product implements Serializable {
    @Id
    @ManyToOne(optional = false)
    private Availability availability;
}

If you do a query like FROM Product p WHERE p.availability.id = :id the join to the availability table can be avoided when you have an embedded id like

@Data
@Embeddable
public class AvailabilityId implements Serializable {

    private UUID item;
    private UUID warehouse;
}

I don't think this is possible when using @IdClass

aol-nnov commented 7 years ago

Christian, thank you for all the hints and quick resolution! But now I finally came to the real question why I've started looking into blaze persistence:

I need a sum(amount) on all warehouses for all items. I've started with AvailabilityView:

@EntityView(Availability.class)
public interface AvailabilityView {
    Item getItem();
    BigDecimal getAvailableTotal();
}

Could you help me with the query? I can simply write it in plain sql, but how should I do it with CriteriaBuilderFactory?

Thank you in advance!

beikov commented 7 years ago

Also, as it is mentioned in the comments of the SO question, you can't use EntityManager.find() or EntityManager.getReference() without an id class.

Of course I'll help you with the query :)

First of all, I'd like to recommend not using entity types like Item in an entity view but to create an entity view for that too. Since you seem to care about the identity of the item, also don't forget to use @IdMapping. The availableTotal attribute needs a mapping. The context of the mapping is the entity type, so in mappings within AvailabilityView you can access all entity attributes of the entity type Availability.

@EntityView(Availability.class)
public interface AvailabilityView {
    ItemView getItem();
    @Mapping("SUM(amount)")
    BigDecimal getAvailableTotal();
}
@EntityView(Item.class)
public interface ItemView {
    @IdMapping
    UUID getId();
    // Other attributes
}
beikov commented 7 years ago

To fetch objects of that type, you need something like this

CriteriaBuilder<Availability> cb = criteriaBuilderFactory.create(entityManager, Availability.class);
CriteriaBuilder<AvailabilityView> viewCb = entityViewManager.applySettings(
    EntityViewSetting.create(AvailabilityView.class), 
    cb
);
List<AvailabilityView> result = viewCb.getResultList();

This will create a JPQL query like

SELECT item.id, SUM(a.amount)
FROM Availability a
JOIN a.item item
GROUP BY item.id
aol-nnov commented 7 years ago

THAT easy?? Oh, gosh! It's a simple repository findAll call! And paging support! Yay! Brilliant! :)) Thank you! You've made me a really quick start with your library!

The last question, if you don't mind :)

What if I need to order the result set by another entity? Say, I have

@Data
@Entity
class SortOrder {
    Item item;
    int placementWeight;
}

in normal sql I'd join it with Availability and then ordered by placementWeight. How could I integrate it into your example?

Worth to mention, that SortOrder does not strictly have records for all available Items

beikov commented 7 years ago

I suppose there is a relation in Item for the SortOrder? In that case you'd do something like this...

@EntityView(Availability.class)
public interface AvailabilityView {
    ItemView getItem();
    @Mapping("item.sortOrder.placementWeight")
    Integer getPlacementWeight();
    @Mapping("SUM(amount)")
    BigDecimal getAvailableTotal();
}
@EntityView(Item.class)
public interface ItemView {
    @IdMapping
    UUID getId();
}

I added the placement weight as attribute so you can sort based on it. You could also put it into the ItemView if you want.

To actually sort the results, you need to add a so called attribute sorter on the EntityViewSetting

CriteriaBuilder<Availability> cb = criteriaBuilderFactory.create(entityManager, Availability.class);
EntityViewSetting<AvailabilityView, CriteriaBuilder<AvailabilityView>> setting =
    EntityViewSetting.create(AvailabilityView.class);
setting.addAttributeSorter("placementWeight", Sorters.ascending());
CriteriaBuilder<AvailabilityView> viewCb = entityViewManager.applySettings(setting, cb);
List<AvailabilityView> result = viewCb.getResultList();

I'm honestly not sure if pagination with the PaginatedCriteriaBuilder API will work in this case as one of the current limitations is the inability to use group by. You might have to use setFirstResult and setMaxResults instead, but give it a try, I could be wrong 😆

aol-nnov commented 7 years ago

No, Item does not relate to SortOrder. Only SortOrder has a reference to Item. And I'd like not to link Item with SortOrder as it is a kind of helper entity for a single operation.

beikov commented 7 years ago

Since you use Hibernate 5 and not 5.1 or newer, you have to use a correlated subquery to do that, but PostgreSQL's optimizer should be able to optimize that into a join so no worries.

The mapping changes to

@EntityView(Availability.class)
public interface AvailabilityView {
    ItemView getItem();
    @MappingSubquery(PlacementWeightProvider.class)
    Integer getPlacementWeight();
    @Mapping("SUM(amount)")
    BigDecimal getAvailableTotal();

    class PlacementWeightProvider implement SubqueryProvider {
        @Override
        public <T> T createSubquery(SubqueryInitiator<T> subqueryBuilder) {
            return subqueryBuilder.from(SortOrder.class, "sortOrder")
                .where("sortOrder.item.id").eqExpression("OUTER(item.id)")
                .select("sortOrder.placementWeight")
                .end();
        }
    }
}
@EntityView(Item.class)
public interface ItemView {
    @IdMapping
    UUID getId();
}

The rest stays the same.

aol-nnov commented 7 years ago

Astonishing! Seems like a Swiss army knife! Thank you so much!!

You probably think by yourself "ugly noobies.. Y don't read the docs!!1 Y???" no! We do! But reference manual is sooo immense and the desire to jump and try things is so strong, LOL!

Thank you for the help and for blaze-persistence! It's cool!

P.S.: will read the docs more, I promise! :-D

beikov commented 7 years ago

No problem, I'm here to help :) Don't hesitate to ask questions! After all, these questions lead to improvements, so please don't keep that to yourself 😆 May I ask how you came to know Blaze-Persistence?

beikov commented 7 years ago

Here is the issue for the IdClass support if you want to follow it: https://github.com/Blazebit/blaze-persistence/issues/399

aol-nnov commented 7 years ago

Sure, will do!

I came to know about Blaze-Persistence when I was seeking for keyset pagination implementation for JPA. It was written in one article that there is not much choice in this area - blaze-persistence and one other implementation that I do not remember. ))

I'll post a link if I find it again!

aol-nnov commented 7 years ago

Oh, here it is: http://use-the-index-luke.com/no-offset :)

beikov commented 7 years ago

Thanks :) I talked to Markus Winand(author of the article) a few weeks ago, and figured out, that the chosen keyset pagination strategy is not yet perfect, but that will change in the final 1.2.0 version: https://github.com/Blazebit/blaze-persistence/issues/419

aol-nnov commented 7 years ago

Christian, everything is well so far, my code became much simpler with blaze-persistence :)

But now I'm trying to add a having clause here

my naive approach looks like

viewCb.having("sum(amount)").gt(0);

but no luck - I'm getting java.lang.IllegalStateException: Having without group by

Please tell me what's wrong with it? Seems, I'm applying my having clause before generated group by is appended to the query or something lite that...

beikov commented 7 years ago

The Group By clause is added because of your use of an aggregate function. To be able to define a custom having clause you'd need to also specify a group by. Duplicates are filtered out so simply call groupBy("item.id") before using having.

aol-nnov commented 7 years ago

Sql group by is correctly generated when I do not specify viewCb.having. It only vanishes away when I add my viewCb.having call. Is it by design and I have to manually craft the whole group by then?

aol-nnov commented 7 years ago

Wow, just changed it to viewCb.groupBy("item.id").having("sum(amount)").gt(0); and everything went fine!

I thought, viewCb.groupBy("item.id") will overwrite the generated part of sql group by statement, but I was wrong!

Thanks again for a quick help!

beikov commented 7 years ago

f you look into the core documentation there is something called implicit group by generation which is what is causing the group by being generated. This is a post step that just makes your life easier. If you want to have custom grouping or having predicates you can simply add them, on top of that, the rest of the needed group bys is added, skipping duplicates. It is by design that having is only allowed when specifying an explicit group by. Note that you don't have the specify the full group by, just the item Id is enough.