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

Primary key with @Converter gives different instance on .find() and .*Query() #2378

Open
mb-dbc-dk opened this issue Mar 4, 2025 · 0 comments

Comments

@mb-dbc-dk
Copy link

When a primary key (either @Id or @EmbeddedId) has a @Converter then, when using .find() and .*Query() you get different instances of the same entity.

When EntityManager.*Query(...) is called, It seems like the AttributeConverter(s) aren't called, before it is looked up/stored in the cache.
I found out by calling .find(), and then .createQuery(), and EntityManager.remove() on the one from the query, I could still find it the entity by primary key in the cache, in the next transaction.

To Reproduce

From pom.xml:

        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>eclipselink</artifactId>
            <version>4.0.5</version>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>postgresql</artifactId>
            <version>1.20.5</version>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.7.5</version>
        </dependency>

using Java:
openjdk version "17.0.4" 2022-07-19
OpenJDK Runtime Environment GraalVM CE 22.2.0 (build 17.0.4+8-jvmci-22.2-b06)
OpenJDK 64-Bit Server VM GraalVM CE 22.2.0 (build 17.0.4+8-jvmci-22.2-b06, mixed mode, sharing)

Running this test:

package com.example;

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Column;
import jakarta.persistence.Converter;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Id;
import jakarta.persistence.Persistence;
import jakarta.persistence.Table;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.EnumSet;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.sql.DataSource;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.postgresql.ds.PGSimpleDataSource;
import org.postgresql.util.PGobject;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.wait.strategy.Wait;

import static org.eclipse.persistence.config.PersistenceUnitProperties.*;
import static org.junit.Assert.assertEquals;

public class MyTest {

    @FunctionalInterface
    public interface Execution {

        void execute(EntityManager em) throws Exception;
    }

    private static PostgreSQLContainer PG;
    private static DataSource DS;
    private static EntityManagerFactory EMF;
    private EntityManager entityManager;

    @BeforeClass
    public static void startup() throws Exception {
        PG = makePostgreSQL();
        DS = makeDataSource();
        setupPgSchema();
        EMF = makeEntityManagerFactory();
    }

    @AfterClass
    public static void shutdown() {
        PG.stop();
    }

    @Before
    public void setupEntityManager() {
        entityManager = EMF.createEntityManager();
    }

    @After
    public void closeEntityManager() {
        entityManager.close();
    }

    @Test
    public void test() throws Exception {
        transaction(em -> {
            em.persist(new MyEntity(MyType.ALPHA));
        });

        transaction(em -> {
            MyEntity byFind = em.find(MyEntity.class, MyType.ALPHA);
            System.out.println("byFind = " + byFind);
            MyEntity byQuery = em.createQuery("SELECT e FROM MyTest$MyEntity e", MyEntity.class).getSingleResult();
            System.out.println("byQuery = " + byQuery);
            assertEquals(System.identityHashCode(byFind), System.identityHashCode(byQuery));
        });
    }

    private void transaction(Execution ex) throws Exception {
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        try {
            ex.execute(entityManager);
            transaction.commit();
        } finally {
            if (transaction.isActive()) {
                try {
                    transaction.rollback();
                } catch (Exception e) {
                    System.err.println("Error rolling back transaction: " + e);
                }
            }
        }
    }

    public static enum MyType {
        ALPHA("a"),
        BETA("b");
        final String name;

        private MyType(String name) {
            this.name = name;
        }
        private static final Map<String, MyType> BY_NAME = EnumSet.allOf(MyType.class)
                .stream()
                .collect(Collectors.toMap(MyType::getName, e -> e));

        public static MyType byName(String name) {
            return BY_NAME.get(name);
        }

        public String getName() {
            return name;
        }
    }

    @Entity
    @Table(name = "MyEntities")
    public static class MyEntity implements Serializable {

        private static final long serialVersionUID = 0x1011C561B1B80328L;

        @Id
        @Column(name = "t", updatable = false, insertable = true, nullable = false)
        private MyType type;

        public MyEntity() {
        }

        public MyEntity(MyType type) {
            this.type = type;
        }

        public MyType getType() {
            return type;
        }

        @Override
        public int hashCode() {
            return Objects.hash(this.type);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null || getClass() != obj.getClass())
                return false;
            final MyEntity other = (MyEntity) obj;
            return this.type == other.type;
        }

        @Override
        public String toString() {
            return String.format("MyEntity{type=%s @0x%x}", type, System.identityHashCode(this));
        }
    }

    @Converter(autoApply = true)
    public static class MyConverter implements AttributeConverter<MyType, Object> {

        @Override
        public Object convertToDatabaseColumn(MyType attribute) {
            try {
                PGobject obj = new PGobject();
                obj.setType("MyType");
                obj.setValue(attribute.getName());
                return obj;
            } catch (SQLException ex) {
                throw new IllegalStateException("Error mapping " + MyType.class + " to PG.ENUM", ex);
            }
        }

        @Override
        public MyType convertToEntityAttribute(Object dbData) {
            return MyType.byName((String) dbData);
        }
    }

    private static PostgreSQLContainer makePostgreSQL() {
        PostgreSQLContainer pg = new PostgreSQLContainer<>("postgres:latest")
                .withUsername("duser")
                .withPassword("dpass")
                .withDatabaseName("data")
                .withLogConsumer(t -> System.out.println(t.getUtf8StringWithoutLineEnding()))
                .withExposedPorts(5432)
                .waitingFor(Wait.forSuccessfulCommand("POSTGRES_PASSWORD=dpass psql -U duser -d data -h 127.0.0.1 -e 'SELECT 1'"));
        pg.start();
        return pg;
    }

    private static DataSource makeDataSource() {
        PGSimpleDataSource ds = new PGSimpleDataSource();
        ds.setUser("duser");
        ds.setPassword("dpass");
        ds.setDatabaseName("data");
        ds.setServerNames(new String[] {PG.getHost()});
        ds.setPortNumbers(new int[] {PG.getMappedPort(5432)});
        return ds;
    }

    private static void setupPgSchema() throws SQLException {
        try (Connection connection = DS.getConnection();
             Statement stmt = connection.createStatement();) {
            stmt.executeUpdate("CREATE TYPE MyType AS ENUM ('a', 'b');");
            stmt.executeUpdate("CREATE TABLE MyEntities ( t MyType PRIMARY KEY )");
        }
    }

    private static EntityManagerFactory makeEntityManagerFactory() throws IOException {
        Path.of("target", "test-classes", "META-INF").toFile().mkdirs();
        Files.writeString(Path.of("target", "test-classes", "META-INF", "persistence.xml"),
                          """
                          <?xml version="1.0" encoding="UTF-8"?>
                          <persistence version="3.0" xmlns="https://jakarta.ee/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd">
                            <persistence-unit name="db-test" transaction-type="RESOURCE_LOCAL">
                              <shared-cache-mode>ALL</shared-cache-mode>
                              <class>@class@$MyEntity</class>
                              <class>@class@$MyConverter</class>
                              <properties>
                                <property name="javax.persistence.schema-generation.database.action" value="none"/>
                              </properties>
                            </persistence-unit>
                          </persistence>
                          """.replace("@class", MyTest.class.getCanonicalName()));
        return Persistence.createEntityManagerFactory("db-test",
                                                      Map.of(JDBC_USER, "duser",
                                                             JDBC_PASSWORD, "dpass",
                                                             JDBC_URL, PG.getJdbcUrl(),
                                                             JDBC_DRIVER, PG.getDriverClassName(),
                                                             "eclipselink.logging.level", "FINE"));
    }
}

The test fails with different values from System.identityHashCode().

Expected behavior
Expected the two objects to be same instance

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

No branches or pull requests

1 participant