0

I am facing this issue since long now and I am not aware of this problem in java side. I am working on an old JPA project without prior knowledge in java and Postgres SQL.

I have an entity class where I can able to push into derby db. But now I want to make a replica of data into Postgres as well .

Entity class:

@Table(name = "certs")
public class LocalCertificate implements Serializable {

    private static final long serialVersionUID = -5003848691574858779L;

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

    @Expose
    @Column(name = "d_id")
    private String d_id;
    
    @Expose
    @Column(name = "certificate", columnDefinition="clob")
    @Convert()
    @Lob
    private X509CertificateHolder certificate;
    
    @Expose
    @Column(name = "revoked")
    public boolean revoked = false;
    
    public byte[] getCertDER() throws IOException {
        return certificate.getEncoded();
    }
    
    
    public long getId() {
        return id.longValue();
    }
    
    public String getDId() {
        return d_id;
    }
    
    public void store() {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("LDB");
        EntityManager locEm = emf.createEntityManager();

        EntityTransaction ta = locEm.getTransaction();
        ta.begin();
        locEm.persist(this);
        ta.commit();
        
        // for Postgres
        EntityManagerFactory eemf = Persistence.createEntityManagerFactory("PU_CSA");
        EntityManager llocEm = eemf.createEntityManager();

        EntityTransaction tta = llocEm.getTransaction();
        tta.begin();
        llocEm.persist(this);
        tta.commit();
    }
    
    public void update() {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("LDB");
        EntityManager locEm = emf.createEntityManager();

        EntityTransaction ta = locEm.getTransaction();
        
        ta.begin();
        locEm.merge(this);
        ta.commit();
        
        // for postgres
        EntityManagerFactory eemf = Persistence.createEntityManagerFactory("PU_CSA");
        EntityManager llocEm = eemf.createEntityManager();

        EntityTransaction tta = llocEm.getTransaction();
        
        tta.begin();
        llocEm.merge(this);
        tta.commit();
    }
    
    public void setD_id(String dId) {
        // TODO Auto-generated method stub
        this.d_id =  dId;
    }


    public void setCertificate(X509CertificateHolder cert) {
        // TODO Auto-generated method stub
        this.certificate = cert;        
    }
}

Persistence xml file:

<persistence-unit name="LDB">
        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
        <non-jta-data-source>java:comp/env/jdbc/LDB</non-jta-data-source>
        <class>org.backend.dao.LocalCertificate</class>
        <class>org.backend.certserver.jpa.LocalCertificateAdapter</class>
        
        <properties>
            <!-- EclipseLink should create the database schema automatically -->
            <property name="eclipselink.ddl-generation" value="create-or-extend-tables" />
            <property name="eclipselink.ddl-generation.output-mode" value="database" />
        </properties>
    </persistence-unit>

    <persistence-unit name="PU_CSA">
        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
        <non-jta-data-source>java:comp/env/jdbc/CSA</non-jta-data-source>
        <class>org.backend.dao.Device</class>
        <class>org.backend.dao.LocalCertificate</class>
        <class>org.backend.certserver.jpa.LocalCertificateAdapter</class>
        <properties>
            <!-- EclipseLink should create the database schema automatically -->
            <property name="eclipselink.ddl-generation" value="create-or-extend-tables" />
            <property name="eclipselink.ddl-generation.output-mode" value="database" /> 
        </properties>
    </persistence-unit>

For d_id and certificate column I can see NULL value. How to solve this issue ?

EDIT:

Adding the servlet file where it is failing

private void processDevCertRequest(HttpServletResponse response, String devId) throws IOException {

        LocalCertificate newCert = new LocalCertificate();
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("LDB");
        EntityManager locEm = emf.createEntityManager();

        EntityTransaction ta = locEm.getTransaction();
        ta.begin();
        locEm.persist(newCert);
        ta.commit();
        
        // postgres
        EntityManagerFactory posemf = Persistence.createEntityManagerFactory("PU_CSA");
        EntityManager posEm = posemf.createEntityManager();
        logger.info(newCert.toString());
        EntityTransaction posta = posEm.getTransaction();
        posta.begin();
        posEm.persist(newCert);
        posta.commit();
        
        BigInteger certID = BigInteger.valueOf(newCert.getId());

        CertificateAuthorithyManager cam = CertificateAuthorithyManager.getInstance();
        KeyPairCert kpc = cam.generateSignedKeyPairCert(devId, certID);

        String pemcert = CAServletHelper.generatePEMBlock(kpc.cert);
        String pemkey = CAServletHelper.generatePEMBlock(kpc.keyPair.getPrivate());
        

        ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
        ZipOutputStream zos = new ZipOutputStream(byteOutput);
        zos.putNextEntry(new ZipEntry(devId + "_cert.pem"));
        zos.write(pemcert.getBytes(), 0, pemcert.length());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry(devId + "_key.pem"));
        zos.write(pemkey.getBytes(), 0, pemkey.length());
        zos.closeEntry();
        zos.close();

        newCert.setDev_id(devId);
        newCert.setCertificate(kpc.cert);
        ta.begin();
        locEm.merge(newCert);
        ta.commit();            <==== here it is failing
        
        // postgress
        newCert.setDev_id(devId);
        newCert.setCertificate(kpc.cert);
        posta.begin();
        posEm.merge(newCert);
        posta.commit();

        OutputStream resos = response.getOutputStream();
        response.setContentType("application/zip");
        response.setStatus(HttpServletResponse.SC_CREATED);
        response.setHeader("Content-Disposition", "attachment;filename=\"" + devId + ".zip\"");
        resos.write(byteOutput.toByteArray());
        resos.flush();
        byteOutput.close();
    }
NoobCoder
  • 493
  • 8
  • 25
  • Take care of java naming conventions. Names should be camelCase not snake_case. This should slolve your problem too – Jens Feb 16 '23 at 09:58
  • @Jens which name you are talking about ? Table or ? – NoobCoder Feb 16 '23 at 10:18
  • `private String d_id;` should be `private String dId;` – Jens Feb 16 '23 at 10:20
  • You haven't defined the issue exactly - how are you setting d_id, or why do you expect to be something other than null? Is this specific to the second persistence unit? Ideally, you'd 'persist' a copy of the entity into the second and not directly call persist on the same entity instance in two separate entityManagers - JPA providers will manage that object and inject change tracking hooks that will interfere if you use it in a second context. – Chris Feb 16 '23 at 21:07
  • @Chris Yes. So it is specific to second persistence unit. Though the first part is in local db and second is in postgres so I am confused how to maintain same data for both. Sorry, I am new to java JPA things. Can you show me sample example what do you means by directly calling ? – NoobCoder Feb 17 '23 at 03:38
  • @Jens That didn't helped. same issue. – NoobCoder Feb 17 '23 at 03:44
  • @NoobCoder Have you also renamed the setter? – Jens Feb 17 '23 at 05:46
  • @Jens yes, I did. I guess that is not an issue because when I am creating same entities in different class for different persistence, it is working but now I want to use same class which is already working for local DB ie; LDB persistence unit name. Basically need to mix LDB and PU_CSA together to create same values but push into different dbs – NoobCoder Feb 17 '23 at 06:17
  • Copy/clone the data in 'this' instance into a second entity instance and use that to merge or persist into the llocEm context. These entity instances are tied to a single entity manager context, so I don't think it a good idea to have the update/store logic in the data object itself, and EntityManagerFactory instances are meant to be used as singletons as they have a large amount of overhead to create and you are responsible to close them which you are not doing in this code. see https://stackoverflow.com/a/17746371/496099 – Chris Feb 17 '23 at 15:25
  • @Chris I am sorry, I am still confused. Can you edit my code for copy ? Also when I remove nextval() from PSQL PK column I am getting now `ERROR: null value in column "id" of relation "certs" violates not-null constraint` – NoobCoder Feb 21 '23 at 04:11
  • You cannot just remove Identity from the db table without giving JPA or your application some other way to assign the ID values. As for cloning/copying - this is a well known pattern of creating a new instance of an object (shallow or deep) that has the same basic value object. Try the example I linked. You'll have to decide how to handle ID values in the object - if the new DB should assign a new value for the same data or if you want one ID to be used in both databases. – Chris Feb 21 '23 at 15:28

1 Answers1

0

For this to work, you'll need to create a copy of the instance you are persist/merging for use in the second persistence context - something more like:

Static final EntityManagerFactory emf = Persistence.createEntityManagerFactory("LDB");
// for postgres
Static final EntityManagerFactory eemf = Persistence.createEntityManagerFactory("PU_CSA");

public void update(LocalCertificate cert) {
    EntityManager locEm = emf.createEntityManager()
    try {
      EntityTransaction ta = locEm.getTransaction();
    
      ta.begin();
      locEm.merge(cert);
      ta.commit();
    } finally { locEm.close();}

    CopyGroup group = new CopyGroup();
    LocalCertificate copy = (LocalCertificate)em.unwrap( JpaEntityManager.class ).copy( cert, group );
    
    EntityManager llocEm = eemf.createEntityManager();
    try {
      EntityTransaction tta = llocEm.getTransaction();
    
      tta.begin();
      llocEm.merge(copy);
      tta.commit();
    } finally { llocEm.close();}
}

For persisting a new entity, you will have to do more as your model won't work as it is - You likely cannot use identity management in both derby and Postgres or your entities will be given different primary key values. To get around that, the simplest solution is to assign values yourself, something like:

@Table(name = "certs")
public class LocalCertificate implements Serializable {

  @Id
  private String id;
  ..
  
  public void assignId() {
    if (StringUtil.isBlank(id)) {
      id = UUID.randomUUID().toString();
    }
  }
 }

You then can use the same concept for persisting and just call assignId on the first object passed in, and use that assigned value in the copy passed into the second persistence unit as well.

Chris
  • 20,138
  • 2
  • 29
  • 43
  • What will be in the place of `Playlist` ? `JpaEntityManager.class` ? – NoobCoder Feb 22 '23 at 06:56
  • You are assigning it to a LocalCertificate, so the Object from copy needs to be caste to LocalCertificate. – Chris Feb 22 '23 at 15:53
  • Whatever you made changes on update() method, same needed to be done on store() method as well ? I made the changed accordingly and error is - `The attribute [id] of class [backend.dao.LocalCertificate] is mapped to a primary key column in the database. Updates are not allowed`. Do I need to create a class for `StringUtil` ? – NoobCoder Feb 23 '23 at 04:04
  • Show what you did and where the exception is thrown from as that is not possible if you actually made a copy to use in the second call. StringUtil can be any utility available that does a 'null or is empty check' on the string. The check really isn't needed, but I had unnecessarily put this in a prePersist method which would fire on the second persistence unit. – Chris Feb 23 '23 at 14:22
  • I have added the failing section in my question edit. Its in servlet class – NoobCoder Feb 27 '23 at 05:29
  • The key to my answer is the copy policy while in your code, you take a single newCert instance and pass it to persist and then that same instance to merge. You cannot use the same instance in multiple persistence units. Make a copy to reuse in the 'other' persistence unit. You haven't shown how you are assigning an Id to the entity either - the two rows you persist, if you left the @GeneratedValue annotation, will get assigned different ID values which will break merge on you. – Chris Feb 27 '23 at 16:20