You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openjpa.apache.org by so...@apache.org on 2026/10/25 01:23:03 UTC

(openjpa) branch master updated: [OPENJPA-3020] Choose the LOB type of a converted attribute from the converter (#222)

This is an automated email from the ASF dual-hosted git repository.

solomax pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openjpa.git


The following commit(s) were added to refs/heads/master by this push:
     new a6faa27ff [OPENJPA-3020] Choose the LOB type of a converted attribute from the converter (#222)
a6faa27ff is described below

commit a6faa27ff99c779ace3067a03f9c7deb60c55b47
Author: Richard Zowalla <13...@users.noreply.github.com>
AuthorDate: Sun Oct 25 02:22:57 2026 +0100

    [OPENJPA-3020] Choose the LOB type of a converted attribute from the converter (#222)
    
    * [OPENJPA-3020] Choose the LOB type of a converted attribute from the converter
    
    @Lob and <lob/> made a CLOB for a String, char[] or Character[] attribute
    and a BLOB otherwise, before the converter was known, and the converter's
    template column did not override it. A converter to String on any other
    attribute type therefore got a BLOB, and a converter to byte[] on a String
    a CLOB. The converter handlers now set the LOB type from the converter's
    database type. A byte[] database type is also read as bytes, so the
    converter no longer gets a java.sql.Blob.
    
    * [OPENJPA-3020] Note the LOB type change of converted attributes in the migration considerations
    
    * [OPENJPA-3020] Add braces to single statement blocks
---
 .../jdbc/meta/strats/ConverterElementHandler.java  |   4 +-
 .../jdbc/meta/strats/ConverterValueHandler.java    |  42 ++++++-
 .../persistence/conversion/ItemConverter.java      |  39 ++++++
 .../persistence/conversion/LobConvertedEntity.java | 133 +++++++++++++++++++++
 .../conversion/StringBytesConverter.java           |  39 ++++++
 .../persistence/conversion/TestLobConverted.java   | 121 +++++++++++++++++++
 .../conversion/TestLobConvertedXml.java            |  89 ++++++++++++++
 .../conversion/XmlLobConvertedEntity.java          |  74 ++++++++++++
 .../src/test/resources/META-INF/persistence.xml    |   8 ++
 .../persistence/conversion/lob-converted-orm.xml   |  37 ++++++
 .../src/doc/manual/migration_considerations.xml    |  12 ++
 11 files changed, 595 insertions(+), 3 deletions(-)

diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/ConverterElementHandler.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/ConverterElementHandler.java
index e8aad8614..d019b549d 100644
--- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/ConverterElementHandler.java
+++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/ConverterElementHandler.java
@@ -28,7 +28,6 @@ import org.apache.openjpa.jdbc.meta.ValueMapping;
 import org.apache.openjpa.jdbc.schema.Column;
 import org.apache.openjpa.jdbc.schema.ColumnIO;
 import org.apache.openjpa.jdbc.sql.DBDictionary;
-import org.apache.openjpa.meta.JavaTypes;
 import org.apache.openjpa.util.MetaDataException;
 
 /**
@@ -66,9 +65,10 @@ public class ConverterElementHandler extends AbstractValueHandler {
         DBIdentifier colName = DBIdentifier.newColumn(name,
             dict != null ? dict.delimitAll() : false);
 
+        ConverterValueHandler.adjustLobColumns(vm, _dbType);
         Column col = new Column();
         col.setIdentifier(colName);
-        col.setJavaType(JavaTypes.getTypeCode(_dbType));
+        col.setJavaType(ConverterValueHandler.getColumnJavaType(_dbType));
         return new Column[]{ col };
     }
 
diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/ConverterValueHandler.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/ConverterValueHandler.java
index 57b65a9c5..649f3cc56 100644
--- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/ConverterValueHandler.java
+++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/strats/ConverterValueHandler.java
@@ -18,7 +18,10 @@
  */
 package org.apache.openjpa.jdbc.meta.strats;
 
+import java.sql.Types;
+
 import org.apache.openjpa.jdbc.identifier.DBIdentifier;
+import org.apache.openjpa.jdbc.meta.JavaSQLTypes;
 import org.apache.openjpa.jdbc.meta.ValueMapping;
 import org.apache.openjpa.jdbc.schema.Column;
 import org.apache.openjpa.jdbc.schema.ColumnIO;
@@ -54,12 +57,49 @@ public class ConverterValueHandler extends AbstractValueHandler {
         DBIdentifier colName = DBIdentifier.newColumn(name,
             dict != null ? dict.delimitAll() : false);
 
+        adjustLobColumns(vm, _dbType);
         Column col = new Column();
         col.setIdentifier(colName);
-        col.setJavaType(JavaTypes.getTypeCode(_dbType));
+        col.setJavaType(getColumnJavaType(_dbType));
         return new Column[]{ col };
     }
 
+    /**
+     * The column java type for the converter's database type. A byte array
+     * is read and written as bytes, not as an object.
+     */
+    static int getColumnJavaType(Class<?> dbType) {
+        if (dbType == byte[].class) {
+            return JavaSQLTypes.BYTES;
+        }
+        return JavaTypes.getTypeCode(dbType);
+    }
+
+    /**
+     * A {@code @Lob} or {@code <lob/>} column gets its CLOB or BLOB type from
+     * the declared type of the attribute, because the mapping parsers run
+     * before the converter is known. With a converter the stored value has
+     * the converter's database type, so choose the kind of LOB from that.
+     * Columns that are not LOBs, and LOBs whose database type is neither
+     * String nor byte[], are left alone.
+     */
+    static void adjustLobColumns(ValueMapping vm, Class<?> dbType) {
+        int lobType;
+        if (dbType == String.class) {
+            lobType = Types.CLOB;
+        } else if (dbType == byte[].class) {
+            lobType = Types.BLOB;
+        } else {
+            return;
+        }
+
+        for (Column col : vm.getValueInfo().getColumns()) {
+            if (col.getType() == Types.BLOB || col.getType() == Types.CLOB) {
+                col.setType(lobType);
+            }
+        }
+    }
+
     @Override
     public boolean isVersionable(ValueMapping vm) {
         return true;
diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/ItemConverter.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/ItemConverter.java
new file mode 100644
index 000000000..3c848f97f
--- /dev/null
+++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/ItemConverter.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openjpa.persistence.conversion;
+
+import jakarta.persistence.AttributeConverter;
+import jakarta.persistence.Converter;
+
+/**
+ * Converts a single, mutable {@link Item} to its name.
+ */
+@Converter
+public class ItemConverter implements AttributeConverter<Item, String> {
+
+    @Override
+    public String convertToDatabaseColumn(Item attribute) {
+        return attribute == null ? null : attribute.getName();
+    }
+
+    @Override
+    public Item convertToEntityAttribute(String dbData) {
+        return dbData == null ? null : new Item(dbData);
+    }
+}
diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/LobConvertedEntity.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/LobConvertedEntity.java
new file mode 100644
index 000000000..f1e281856
--- /dev/null
+++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/LobConvertedEntity.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openjpa.persistence.conversion;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.persistence.Convert;
+import jakarta.persistence.ElementCollection;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Lob;
+
+/**
+ * Converted LOB attributes. The kind of LOB follows the converter's database
+ * type, not the attribute type. {@code plainBytes} is converted to bytes
+ * without being a LOB; the last fields have no converter.
+ */
+@Entity
+public class LobConvertedEntity {
+
+    @Id
+    private long id;
+
+    @Lob
+    @Convert(converter = ItemListConverter.class)
+    private List<Item> items = new ArrayList<>();
+
+    @Lob
+    @Convert(converter = ItemConverter.class)
+    private Item item;
+
+    @ElementCollection
+    @Lob
+    @Convert(converter = StringBytesConverter.class)
+    private List<String> elements = new ArrayList<>();
+
+    @Lob
+    @Convert(converter = StringBytesConverter.class)
+    private String bytes;
+
+    @Convert(converter = StringBytesConverter.class)
+    private String plainBytes;
+
+    @Lob
+    private String text;
+
+    @Lob
+    private byte[] data;
+
+    @Lob
+    private Item serial;
+
+    public long getId() {
+        return id;
+    }
+
+    public void setId(long id) {
+        this.id = id;
+    }
+
+    public List<Item> getItems() {
+        return items;
+    }
+
+    public Item getItem() {
+        return item;
+    }
+
+    public void setItem(Item item) {
+        this.item = item;
+    }
+
+    public List<String> getElements() {
+        return elements;
+    }
+
+    public String getBytes() {
+        return bytes;
+    }
+
+    public void setBytes(String bytes) {
+        this.bytes = bytes;
+    }
+
+    public String getPlainBytes() {
+        return plainBytes;
+    }
+
+    public void setPlainBytes(String plainBytes) {
+        this.plainBytes = plainBytes;
+    }
+
+    public String getText() {
+        return text;
+    }
+
+    public void setText(String text) {
+        this.text = text;
+    }
+
+    public byte[] getData() {
+        return data;
+    }
+
+    public void setData(byte[] data) {
+        this.data = data;
+    }
+
+    public Item getSerial() {
+        return serial;
+    }
+
+    public void setSerial(Item serial) {
+        this.serial = serial;
+    }
+}
diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/StringBytesConverter.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/StringBytesConverter.java
new file mode 100644
index 000000000..0894f0dd2
--- /dev/null
+++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/StringBytesConverter.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openjpa.persistence.conversion;
+
+import java.nio.charset.StandardCharsets;
+
+import jakarta.persistence.AttributeConverter;
+import jakarta.persistence.Converter;
+
+/** Stores a string as its UTF-8 bytes. */
+@Converter
+public class StringBytesConverter implements AttributeConverter<String, byte[]> {
+
+    @Override
+    public byte[] convertToDatabaseColumn(String attribute) {
+        return attribute == null ? null : attribute.getBytes(StandardCharsets.UTF_8);
+    }
+
+    @Override
+    public String convertToEntityAttribute(byte[] dbData) {
+        return dbData == null ? null : new String(dbData, StandardCharsets.UTF_8);
+    }
+}
diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/TestLobConverted.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/TestLobConverted.java
new file mode 100644
index 000000000..c4ef5ffee
--- /dev/null
+++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/TestLobConverted.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openjpa.persistence.conversion;
+
+import java.sql.Types;
+import java.util.Arrays;
+
+import jakarta.persistence.EntityManager;
+
+import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
+import org.apache.openjpa.jdbc.meta.ClassMapping;
+import org.apache.openjpa.jdbc.meta.FieldMapping;
+import org.apache.openjpa.jdbc.meta.JavaSQLTypes;
+import org.apache.openjpa.jdbc.schema.Column;
+import org.apache.openjpa.persistence.test.SingleEMFTestCase;
+
+/**
+ * A {@code @Lob} attribute with a converter is a CLOB or a BLOB depending on
+ * the converter's database type. The kind of LOB used to follow the attribute
+ * type, so a list or a bean converted to a string got a BLOB (BYTEA on
+ * PostgreSQL) and a string converted to bytes got a CLOB.
+ */
+public class TestLobConverted extends SingleEMFTestCase {
+
+    @Override
+    public void setUp() {
+        setUp(LobConvertedEntity.class, DROP_TABLES);
+    }
+
+    private Column column(String field, boolean element) {
+        ClassMapping cm = ((JDBCConfiguration) emf.getConfiguration())
+            .getMappingRepositoryInstance()
+            .getMapping(LobConvertedEntity.class, null, true);
+        FieldMapping fm = (FieldMapping) cm.getField(field);
+        Column[] cols = element ? fm.getElementMapping().getColumns() : fm.getColumns();
+        assertEquals(1, cols.length);
+        return cols[0];
+    }
+
+    public void testListConvertedToStringIsClob() {
+        assertEquals(Types.CLOB, column("items", false).getType());
+    }
+
+    public void testItemConvertedToStringIsClob() {
+        assertEquals(Types.CLOB, column("item", false).getType());
+    }
+
+    public void testElementConvertedToBytesIsBlob() {
+        assertEquals(Types.BLOB, column("elements", true).getType());
+    }
+
+    public void testStringConvertedToBytesIsBlob() {
+        assertEquals(Types.BLOB, column("bytes", false).getType());
+    }
+
+    /**
+     * A converter to a byte array is read as bytes, with or without
+     * {@code @Lob}. The column used to be typed as an object, so reading
+     * the value back passed the driver's Blob to the converter.
+     */
+    public void testStringConvertedToBytesWithoutLobIsReadAsBytes() {
+        assertEquals(JavaSQLTypes.BYTES, column("plainBytes", false).getJavaType());
+    }
+
+    public void testLobsWithoutConverterAreUnchanged() {
+        assertEquals(Types.CLOB, column("text", false).getType());
+        assertEquals(Types.BLOB, column("data", false).getType());
+        assertEquals(Types.BLOB, column("serial", false).getType());
+    }
+
+    public void testRoundTrip() {
+        try (EntityManager em = emf.createEntityManager()) {
+            em.getTransaction().begin();
+            LobConvertedEntity e = new LobConvertedEntity();
+            e.setId(1L);
+            e.getItems().add(new Item("one"));
+            e.getItems().add(new Item("two"));
+            e.setItem(new Item("single"));
+            e.getElements().add("first");
+            e.getElements().add("second");
+            e.setBytes("binary");
+            e.setPlainBytes("plain");
+            e.setText("text");
+            e.setData(new byte[] { 1, 2, 3 });
+            e.setSerial(new Item("serialized"));
+            em.persist(e);
+            em.getTransaction().commit();
+        }
+        try (EntityManager em = emf.createEntityManager()) {
+            LobConvertedEntity found = em.find(LobConvertedEntity.class, 1L);
+            assertEquals(2, found.getItems().size());
+            assertEquals("one", found.getItems().get(0).getName());
+            assertEquals("two", found.getItems().get(1).getName());
+            assertEquals("single", found.getItem().getName());
+            assertEquals(2, found.getElements().size());
+            assertTrue(found.getElements().contains("first"));
+            assertTrue(found.getElements().contains("second"));
+            assertEquals("binary", found.getBytes());
+            assertEquals("plain", found.getPlainBytes());
+            assertEquals("text", found.getText());
+            assertTrue(Arrays.equals(new byte[] { 1, 2, 3 }, found.getData()));
+            assertEquals("serialized", found.getSerial().getName());
+        }
+    }
+}
diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/TestLobConvertedXml.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/TestLobConvertedXml.java
new file mode 100644
index 000000000..ee7b9728b
--- /dev/null
+++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/TestLobConvertedXml.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openjpa.persistence.conversion;
+
+import java.sql.Types;
+
+import jakarta.persistence.EntityManager;
+
+import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
+import org.apache.openjpa.jdbc.meta.ClassMapping;
+import org.apache.openjpa.jdbc.meta.FieldMapping;
+import org.apache.openjpa.persistence.test.SingleEMFTestCase;
+
+/**
+ * Converted attributes declared as LOBs in a mapping file, with the
+ * converters on the fields. The mapping file is read before the annotations,
+ * so the kind of LOB can only follow the converter once the field is mapped.
+ */
+public class TestLobConvertedXml extends SingleEMFTestCase {
+
+    @Override
+    public void setUp() {
+        setUp(XmlLobConvertedEntity.class, DROP_TABLES);
+    }
+
+    @Override
+    protected String getPersistenceUnitName() {
+        return "lob-converted-xml";
+    }
+
+    private int columnType(String field) {
+        ClassMapping cm = ((JDBCConfiguration) emf.getConfiguration())
+            .getMappingRepositoryInstance()
+            .getMapping(XmlLobConvertedEntity.class, null, true);
+        FieldMapping fm = (FieldMapping) cm.getField(field);
+        assertEquals(1, fm.getColumns().length);
+        return fm.getColumns()[0].getType();
+    }
+
+    public void testListConvertedToStringIsClob() {
+        assertEquals(Types.CLOB, columnType("items"));
+    }
+
+    public void testItemConvertedToStringIsClob() {
+        assertEquals(Types.CLOB, columnType("item"));
+    }
+
+    public void testStringConvertedToBytesIsBlob() {
+        assertEquals(Types.BLOB, columnType("bytes"));
+    }
+
+    public void testRoundTrip() {
+        try (EntityManager em = emf.createEntityManager()) {
+            em.getTransaction().begin();
+            XmlLobConvertedEntity e = new XmlLobConvertedEntity();
+            e.setId(1L);
+            e.getItems().add(new Item("one"));
+            e.getItems().add(new Item("two"));
+            e.setItem(new Item("single"));
+            e.setBytes("binary");
+            em.persist(e);
+            em.getTransaction().commit();
+        }
+        try (EntityManager em = emf.createEntityManager()) {
+            XmlLobConvertedEntity found = em.find(XmlLobConvertedEntity.class, 1L);
+            assertEquals(2, found.getItems().size());
+            assertEquals("one", found.getItems().get(0).getName());
+            assertEquals("two", found.getItems().get(1).getName());
+            assertEquals("single", found.getItem().getName());
+            assertEquals("binary", found.getBytes());
+        }
+    }
+}
diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/XmlLobConvertedEntity.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/XmlLobConvertedEntity.java
new file mode 100644
index 000000000..7c8052376
--- /dev/null
+++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/conversion/XmlLobConvertedEntity.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.openjpa.persistence.conversion;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.persistence.Convert;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+
+/**
+ * Converted attributes that a mapping file declares as LOBs, while the
+ * converters stay on the fields.
+ */
+@Entity
+public class XmlLobConvertedEntity {
+
+    @Id
+    private long id;
+
+    @Convert(converter = ItemListConverter.class)
+    private List<Item> items = new ArrayList<>();
+
+    @Convert(converter = ItemConverter.class)
+    private Item item;
+
+    @Convert(converter = StringBytesConverter.class)
+    private String bytes;
+
+    public long getId() {
+        return id;
+    }
+
+    public void setId(long id) {
+        this.id = id;
+    }
+
+    public List<Item> getItems() {
+        return items;
+    }
+
+    public Item getItem() {
+        return item;
+    }
+
+    public void setItem(Item item) {
+        this.item = item;
+    }
+
+    public String getBytes() {
+        return bytes;
+    }
+
+    public void setBytes(String bytes) {
+        this.bytes = bytes;
+    }
+}
diff --git a/openjpa-persistence-jdbc/src/test/resources/META-INF/persistence.xml b/openjpa-persistence-jdbc/src/test/resources/META-INF/persistence.xml
index 7514d1fe6..b89699669 100644
--- a/openjpa-persistence-jdbc/src/test/resources/META-INF/persistence.xml
+++ b/openjpa-persistence-jdbc/src/test/resources/META-INF/persistence.xml
@@ -432,6 +432,14 @@
         </properties>
     </persistence-unit>
 
+    <persistence-unit name="lob-converted-xml">
+        <mapping-file>org/apache/openjpa/persistence/conversion/lob-converted-orm.xml</mapping-file>
+        <class>org.apache.openjpa.persistence.conversion.XmlLobConvertedEntity</class>
+        <properties>
+            <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema"/>
+        </properties>
+    </persistence-unit>
+
 	<persistence-unit name="nonstandard-mapping-xml">
         <mapping-file>META-INF/nonstandard-orm.xml</mapping-file>
 
diff --git a/openjpa-persistence-jdbc/src/test/resources/org/apache/openjpa/persistence/conversion/lob-converted-orm.xml b/openjpa-persistence-jdbc/src/test/resources/org/apache/openjpa/persistence/conversion/lob-converted-orm.xml
new file mode 100644
index 000000000..96f50659e
--- /dev/null
+++ b/openjpa-persistence-jdbc/src/test/resources/org/apache/openjpa/persistence/conversion/lob-converted-orm.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<entity-mappings xmlns="https://jakarta.ee/xml/ns/persistence/orm"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence/orm https://jakarta.ee/xml/ns/persistence/orm/orm_3_2.xsd"
+    version="3.2">
+    <entity class="org.apache.openjpa.persistence.conversion.XmlLobConvertedEntity">
+        <attributes>
+            <basic name="items">
+                <lob/>
+            </basic>
+            <basic name="item">
+                <lob/>
+            </basic>
+            <basic name="bytes">
+                <lob/>
+            </basic>
+        </attributes>
+    </entity>
+</entity-mappings>
diff --git a/openjpa-project/src/doc/manual/migration_considerations.xml b/openjpa-project/src/doc/manual/migration_considerations.xml
index 0cee386f0..2609406d3 100644
--- a/openjpa-project/src/doc/manual/migration_considerations.xml
+++ b/openjpa-project/src/doc/manual/migration_considerations.xml
@@ -913,6 +913,18 @@
                     to a column default can therefore not be queried for its null attributes by
                     <literal>IS NULL</literal>; compare against the substitute value instead.
                 </para>
+                <para>
+                    For a <literal>@Lob</literal> (or <literal>&lt;lob/&gt;</literal>) attribute with a
+                    converter, the kind of LOB now follows the converter's database type: a converter
+                    to <classname>String</classname> yields a CLOB (<literal>TEXT</literal> on
+                    PostgreSQL and MySQL) and a converter to <literal>byte[]</literal> a BLOB
+                    (<literal>BYTEA</literal> on PostgreSQL). In 4.1.x the attribute type decided,
+                    so a converter to <classname>String</classname> on any attribute type other than
+                    <classname>String</classname> produced a BLOB, and a converter to
+                    <literal>byte[]</literal> on a <classname>String</classname> attribute a CLOB.
+                    Tables created by 4.1.x for such attributes no longer match the mapping; alter
+                    the column type, or pin it with <literal>@Column(columnDefinition=...)</literal>.
+                </para>
                 <para>
                     Audit existing <literal>@Converter(autoApply=true)</literal> classes: attributes of
                     the matching type that were stored unconverted by 4.1.x are now written and read