You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openjpa.apache.org by rm...@apache.org on 2020/04/29 18:47:40 UTC

[openjpa] 02/02: OPENJPA-2812 snake_case support in dbdictionary for db column names

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

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

commit d0875670dd470473e305aea00d66984735a8e3dc
Author: Romain Manni-Bucau <rm...@gmail.com>
AuthorDate: Wed Apr 29 20:47:28 2020 +0200

    OPENJPA-2812 snake_case support in dbdictionary for db column names
---
 .../openjpa/jdbc/meta/MappingDefaultsImpl.java     |  16 +-
 .../org/apache/openjpa/jdbc/sql/DBDictionary.java  |  37 +++++
 .../apache/openjpa/jdbc/sql/TestDBDictionary.java  |  35 +++++
 .../apache/openjpa/jdbc/sql/TestSnakeCaseDDL.java  | 162 +++++++++++++++++++++
 4 files changed, 246 insertions(+), 4 deletions(-)

diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/MappingDefaultsImpl.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/MappingDefaultsImpl.java
index ca1bf38..c9f872e 100644
--- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/MappingDefaultsImpl.java
+++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/meta/MappingDefaultsImpl.java
@@ -630,14 +630,22 @@ public class MappingDefaultsImpl
      * Correct the given column's name.
      */
     protected void correctName(Table table, Column col) {
+        DBIdentifier name = col.getIdentifier();
+        boolean corrected = false;
+        if (dict.javaToDbColumnNameProcessing != null) {
+            name = dict.processDBColumnName(name);
+            corrected = true;
+        }
         if (!_defMissing || _removeHungarianNotation)
         {
-            DBIdentifier name = col.getIdentifier();
             if (_removeHungarianNotation)
                 name = DBIdentifier.removeHungarianNotation(name);
-            DBIdentifier correctedName = dict.getValidColumnName(name, table);
-            col.setIdentifier(correctedName);
-            table.addCorrectedColumnName(correctedName, true);
+            corrected = true;
+        }
+        if (corrected) {
+            name = dict.getValidColumnName(name, table, false);
+            col.setIdentifier(name);
+            table.addCorrectedColumnName(name, true);
         }
     }
 
diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java
index 82ee861..26e13fc 100644
--- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java
+++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java
@@ -135,6 +135,8 @@ import org.apache.openjpa.util.StoreException;
 import org.apache.openjpa.util.UnsupportedException;
 import org.apache.openjpa.util.UserException;
 
+import static java.util.Locale.ROOT;
+
 
 /**
  * Class which allows the creation of SQL dynamically, in a
@@ -223,6 +225,7 @@ public class DBDictionary
     public String selectWords = null;
     public String fixedSizeTypeNames = null;
     public String schemaCase = SCHEMA_CASE_UPPER;
+    public String javaToDbColumnNameProcessing;
     public boolean setStringRightTruncationOn = true;
     public boolean fullResultCollectionInOrderByRelation = false;
     public boolean disableSchemaFactoryColumnTypeErrors = false; //OPENJPA-2627
@@ -3292,6 +3295,32 @@ public class DBDictionary
         return toDBName(getColumnIdentifier(column));
     }
 
+    public String toSnakeCase(final String name) {
+        final StringBuilder out = new StringBuilder(name.length() + 3);
+        final boolean isDelimited = name.startsWith(getLeadingDelimiter()) && name.endsWith(getTrailingDelimiter());
+        final String toConvert;
+        if (isDelimited) {
+            toConvert = name.substring(2, name.length() - 1);
+            out.append(name.substring(0, 2).toLowerCase(ROOT));
+        } else {
+            toConvert = name.substring(1);
+            out.append(Character.toLowerCase(name.charAt(0)));
+        }
+        for (final char c : toConvert.toCharArray()) {
+            if (!Character.isLetter(c)) { // delimiter
+                out.append(c);
+            } else if (Character.isUpperCase(c)) {
+                out.append('_').append(Character.toLowerCase(c));
+            } else {
+                out.append(c);
+            }
+        }
+        if (toConvert.length() != name.length() - 1) {
+            out.append(name.charAt(name.length() - 1));
+        }
+        return out.toString();
+    }
+
     /**
      * Returns the full name of the table, including the schema (delimited
      * by {@link #catalogSeparator}).
@@ -3389,6 +3418,14 @@ public class DBDictionary
         return getValidColumnName(DBIdentifier.newColumn(name), table, true).getName();
     }
 
+    public DBIdentifier processDBColumnName(final DBIdentifier name) {
+        if ("snake_case".equalsIgnoreCase(javaToDbColumnNameProcessing)) {
+            return DBIdentifier.newColumn(toSnakeCase(name.getName()));
+        }
+        throw new IllegalArgumentException(
+                "Unsupported javaToDbColumnNameProcessing value: '" + javaToDbColumnNameProcessing + "'");
+    }
+
     /**
      * Make any necessary changes to the given column name to make it valid
      * for the current DB.  The column name will be made unique for the
diff --git a/openjpa-jdbc/src/test/java/org/apache/openjpa/jdbc/sql/TestDBDictionary.java b/openjpa-jdbc/src/test/java/org/apache/openjpa/jdbc/sql/TestDBDictionary.java
new file mode 100644
index 0000000..291058a
--- /dev/null
+++ b/openjpa-jdbc/src/test/java/org/apache/openjpa/jdbc/sql/TestDBDictionary.java
@@ -0,0 +1,35 @@
+/*
+ * 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.jdbc.sql;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class TestDBDictionary {
+    @Test
+    public void snakeCase() {
+        final DBDictionary dictionary = new DBDictionary();
+        assertEquals("foo", dictionary.toSnakeCase("foo"));
+        assertEquals("foo_bar", dictionary.toSnakeCase("fooBar"));
+        assertEquals("fooba_r", dictionary.toSnakeCase("FoobaR"));
+        assertEquals("o_f_o_ob", dictionary.toSnakeCase("oFOOb"));
+        assertEquals("\"foo_bar\"", dictionary.toSnakeCase("\"fooBar\""));
+    }
+}
diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/sql/TestSnakeCaseDDL.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/sql/TestSnakeCaseDDL.java
new file mode 100644
index 0000000..053ccfa
--- /dev/null
+++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jdbc/sql/TestSnakeCaseDDL.java
@@ -0,0 +1,162 @@
+/*
+ * 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.jdbc.sql;
+
+import org.apache.commons.dbcp2.BasicDataSource;
+import org.apache.derby.jdbc.EmbeddedDriver;
+import org.apache.openjpa.persistence.PersistenceProviderImpl;
+import org.apache.openjpa.persistence.PersistenceUnitInfoImpl;
+import org.junit.Test;
+
+import javax.persistence.Entity;
+import javax.persistence.EntityManager;
+import javax.persistence.EntityManagerFactory;
+import javax.persistence.Id;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static java.util.Collections.singleton;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class TestSnakeCaseDDL {
+    @Test
+    public void ddlInSnakeCase() throws SQLException {
+        final PersistenceUnitInfoImpl persistenceUnitInfo = new PersistenceUnitInfoImpl();
+        persistenceUnitInfo.setExcludeUnlistedClasses(true);
+        persistenceUnitInfo.addManagedClassName(MyEntity1.class.getName());
+        persistenceUnitInfo.addManagedClassName(MyEntity2.class.getName());
+        final BasicDataSource ds = new BasicDataSource();
+        ds.setDriver(new EmbeddedDriver());
+        ds.setUrl("jdbc:derby:memory:ddlInSnakeCase;create=true");
+        persistenceUnitInfo.setJtaDataSource(ds);
+        persistenceUnitInfo.setProperty("openjpa.jdbc.DBDictionary", "derby(javaToDbColumnNameProcessing=snake_case)");
+        persistenceUnitInfo.setProperty("openjpa.RuntimeUnenhancedClasses", "supported");
+        new PersistenceProviderImpl().generateSchema(persistenceUnitInfo, new HashMap<>());
+        final Collection<String> createdTables = new HashSet<>();
+        final Map<String, Collection<String>> columns = new HashMap<>();
+        try (final Connection connection = ds.getConnection()) {
+            try (final ResultSet tables = connection.getMetaData()
+                    .getTables(null, null, "TestSnakeCaseDDL$MyEntity%", null)) {
+                while (tables.next()) {
+                    final String table = tables.getString(3);
+                    createdTables.add(table);
+                }
+            }
+            for (final String table : createdTables) {
+                try (final Statement statement = connection.createStatement()) {
+                    try (final ResultSet rs = statement.executeQuery("select * from \"" + table + "\"")) {
+                        final ResultSetMetaData metaData = rs.getMetaData();
+                        final Set<String> columnNames = new HashSet<>();
+                        columns.put(table, columnNames);
+                        for (int i = 1; i <= metaData.getColumnCount(); i++) {
+                            columnNames.add(metaData.getColumnName(i));
+                        }
+                    }
+                }
+            }
+        }
+        final EntityManagerFactory entityManagerFactory = new PersistenceProviderImpl()
+                .createContainerEntityManagerFactory(persistenceUnitInfo, new HashMap());
+        try {
+            {
+                final EntityManager em = entityManagerFactory.createEntityManager();
+                em.getTransaction().begin();
+                try {
+                    final MyEntity1 entity = new MyEntity1();
+                    entity.setFooBar("1");
+                    entity.setThisField(123);
+                    em.persist(entity);
+                    em.getTransaction().commit();
+                } catch (final RuntimeException re) {
+                    em.getTransaction().rollback();
+                    throw re;
+                } finally {
+                    em.close();
+                }
+            }
+            {
+                final EntityManager em = entityManagerFactory.createEntityManager();
+                try {
+                    final MyEntity1 myEntity1 = em.find(MyEntity1.class, "1");
+                    assertNotNull(myEntity1);
+                    assertEquals("1", myEntity1.getFooBar());
+                    assertEquals(123, myEntity1.getThisField());
+                } finally {
+                    em.close();
+                }
+            }
+            try (final Connection connection = ds.getConnection();
+                 final Statement statement = connection.createStatement();
+                 final ResultSet rs = statement.executeQuery("select foo_bar, this_field from \"TestSnakeCaseDDL$MyEntity1\"")) {
+                assertTrue (rs.next());
+                assertEquals("1", rs.getString(1));
+                assertEquals(123, rs.getInt(2));
+                assertFalse(rs.next());
+            }
+        } finally {
+            entityManagerFactory.close();
+        }
+        ds.close();
+        assertEquals(2, columns.get("TestSnakeCaseDDL$MyEntity1").size());
+        assertTrue(columns.get("TestSnakeCaseDDL$MyEntity1").contains("FOO_BAR"));
+        assertTrue(columns.get("TestSnakeCaseDDL$MyEntity1").contains("THIS_FIELD"));
+        assertEquals(singleton("ANOTHER_FIELD"), columns.get("TestSnakeCaseDDL$MyEntity2"));
+    }
+
+    @Entity
+    public static class MyEntity1 {
+        @Id
+        private String fooBar;
+
+        private int thisField;
+
+        public int getThisField() {
+            return thisField;
+        }
+
+        public void setThisField(int thisField) {
+            this.thisField = thisField;
+        }
+
+        public String getFooBar() {
+            return fooBar;
+        }
+
+        public void setFooBar(String fooBar) {
+            this.fooBar = fooBar;
+        }
+    }
+
+    @Entity
+    public static class MyEntity2 {
+        @Id
+        private String anotherField;
+    }
+}