Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for querying by the embedded MonetaryAmount attributes #497

Closed
vladmihalcea opened this issue Oct 17, 2022 · 15 comments
Closed
Assignees
Milestone

Comments

@vladmihalcea
Copy link
Owner

This will allow us to run queries like the following one:

List<Salary> salaries = entityManager.createQuery("""
    select s 
    from Salary s 
    where s.salary.amount >= :amount
    """, Salary.class)
.setParameter("amount", amount)
.getResultList();
@vladmihalcea
Copy link
Owner Author

Fixed.

@viktorvoltaire
Copy link

viktorvoltaire commented Nov 7, 2022

Hi @vladmihalcea

I am running kotlin spring boot, with a simple JpaRepository, and since this fix im getting this error

org.springframework.orm.jpa.JpaSystemException: Calling setPropertyValues is illegal on on javax.money.MonetaryAmount because it's an immutable object!; nested exception is org.hibernate.HibernateException: Calling setPropertyValues is illegal on on javax.money.MonetaryAmount because it's an immutable object!

when running save on our repository. Like below 👇 I removed some stuff and renamed a few things. I can provide some more details if needed

@Repository
interface ARepo : JpaRepository<AnEntity, UUID>

@TypeDef(name = "monetary-amount-currency", typeClass = MonetaryAmountType::class, defaultForType = MonetaryAmount::class)
class AnEntity(
    id: UUID,
    @Columns(columns = [Column(name = "amount"), Column(name = "currency")])
    @Type(type = "monetary-amount-currency")
    var netPrice: MonetaryAmount,
....

Here is an example on a test:

    @Autowired
    private lateinit var repo: ARepo

    @Test
    fun `validate money is stored correctly in DB`() {
        repo.save(
            AnEntity(
                id = randomUUID(),
                netPrice = Money.of(5555, "SEK"),
            )
        )
.........

Any thoughts?

@vladmihalcea
Copy link
Owner Author

vladmihalcea commented Nov 7, 2022

@viktorvoltaire Most likely, it's thrown from this method, but you will have to debug and see why that one got called in your case while the existing test cases in this repository work just fine.

Doing a comparative debug between your code and the integration tests in this repository will surely help you find the problem.

Looking forward to seeing what you found during your investigation.

@viktorvoltaire
Copy link

viktorvoltaire commented Nov 7, 2022

Tbh im not sure, prior to bumping to version 2.20.0 i end up here while debugging https://github.com/hibernate/hibernate-orm/blob/5.6.12/hibernate-core/src/main/java/org/hibernate/type/TypeHelper.java#L265 and everything is working

Once bumping to version 2.20.0 i end up here while debugging https://github.com/hibernate/hibernate-orm/blob/5.6.12/hibernate-core/src/main/java/org/hibernate/type/TypeHelper.java#L260

Hence the Calling setPropertyValues is illegal on on javax.money.MonetaryAmount because it's an immutable object! exception. Not really sure how to proceed 🤔

@vladmihalcea
Copy link
Owner Author

Try to debug the PostgreSQLMonetaryAmountTypeTest and see why it works fine there.

@viktorvoltaire
Copy link

I guess its because isComponentType() was changed from false to true

@vladmihalcea
Copy link
Owner Author

vladmihalcea commented Nov 7, 2022

According to the Hibernate API spec:

boolean isComponentType()
Return true if the implementation is castable to CompositeType.
Essentially a polymorphic version of (type instanceof CompositeType.class).
A component type may own collections or associations and hence must provide certain extra functionality.

Now, it could be a wrong assumption, but it's curious why it works in the official tests?

And, without it, you won't be able to query by its internal attributes.

@viktorvoltaire
Copy link

viktorvoltaire commented Nov 7, 2022

According to the Hibernate API spec:

boolean isComponentType()
Return true if the implementation is castable to CompositeType.
Essentially a polymorphic version of (type instanceof CompositeType.class).
A component type may own collections or associations and hence must provide certain extra functionality.

Now, it could be a wrong assumption, but it's curious why it works in the official tests?

And, without it, you won't be able to query by its internal attributes.

Interesting, we are not working with EntityManager the same way you are, basically we are just calling an Interface that implements a JpaRepository, and then we run save on that. The types we use for MonetaryAmount in our postgres db is

    add column amount numeric,
    add column currency text;

Other than that we have followed your guide https://vladmihalcea.com/monetaryamount-jpa-hibernate/ on how to implement it

@viktorvoltaire
Copy link

Could it be because hibernate is invoking merge, based on how we create the entity

@vladmihalcea
Copy link
Owner Author

vladmihalcea commented Nov 7, 2022

It's not Hibernate, it's Spring then. Check out this article about the Spring save anti-pattern.

However, you could try to provide a replicating test case for merge in this repo, and I'll create an issue if you can replicate it.

@viktorvoltaire
Copy link

I will have a look when I have time, here is a bit more from the stacktrace. As you pointed out its org.springframework.orm.jpa.SharedEntityManagerCreator thats invoking it

Caused by: org.hibernate.HibernateException: Calling setPropertyValues is illegal on on javax.money.MonetaryAmount because it's an immutable object!
	at com.vladmihalcea.hibernate.type.ImmutableCompositeType.setPropertyValues(ImmutableCompositeType.java:365)
	at org.hibernate.type.TypeHelper.replaceAssociations(TypeHelper.java:260)
	at org.hibernate.event.internal.DefaultMergeEventListener.copyValues(DefaultMergeEventListener.java:476)
	at org.hibernate.event.internal.DefaultMergeEventListener.entityIsTransient(DefaultMergeEventListener.java:248)
	at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:318)
	at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:172)
	at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:70)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107)
	at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:829)
	at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:816)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base/java.lang.reflect.Method.invoke(Method.java:577)
	at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:311)
	at jdk.proxy2/jdk.proxy2.$Proxy187.merge(Unknown Source)
	at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:669)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base/java.lang.reflect.Method.invoke(Method.java:577)

@viktorvoltaire
Copy link

viktorvoltaire commented Nov 7, 2022

I did some more testing, and by adding a version to the entity

    @Version
    val version: Long? = null,

makes the isNew true, causing the entityManager to run persist rather than merge

    if (this.entityInformation.isNew(entity)) {
      this.em.persist(entity);
      return entity;
    } else {
      return this.em.merge(entity);
    }

We use a UUID as the ID on the table, so

  public boolean isNew(T entity) {
    ID id = this.getId(entity);
    Class<ID> idType = this.getIdType();
    if (!idType.isPrimitive()) {
      return id == null;
    } else if (id instanceof Number) {
      return ((Number)id).longValue() == 0L;
    } else {
      throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
    }
  }

will always make isNew false, but as said with version it solves it

@vladmihalcea
Copy link
Owner Author

@viktorvoltaire I changed the test cases to include merge, which also work fine when merge is called for detached entities.

The problem is, most likely, called by the Spring save anti-pattern, which calls merge for new/transient entities.

@viktorvoltaire
Copy link

Yeah you are right @vladmihalcea thanks for all the assist!

@vladmihalcea
Copy link
Owner Author

@viktorvoltaire You're welcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants