jOOQ / jOOQ

jOOQ is the best way to write SQL in Java
https://www.jooq.org
Other
6.38k stars 1.22k forks source link

Codegen: Custom strategy's getJavaMemberName ignored for TableField variable when <binding>/<converter> is used #18399

Closed Architha18 closed 1 week ago

Architha18 commented 1 week ago

Expected behavior

When using a custom GeneratorStrategy that overrides getJavaMemberName to change the generated Java identifier for a database column (e.g., removing a suffix like __ENCRYPTED), the override is correctly applied when generating getter and setter methods in the TableRecord class (e.g., MyTableRecord.java).

However, the getJavaMemberName override appears to be ignored when generating the TableField variable declaration within the Table class itself (e.g., MyTable.java), but only when a <binding> or <converter> is also applied to the same column via <forcedType>. The TableField variable retains the name derived from the original database column name, ignoring the strategy's getJavaMemberName result for that specific declaration.

Actual behavior

The getJavaMemberName override in the custom GeneratorStrategy should consistently determine the Java identifier used for the TableField variable declaration in the Table class, even when a <binding> or <converter> is applied via <forcedType>. In the example provided, the field pan__ENCRYPTED should be generated as pan in the Anchor.java table class, consistent with the getPan/setPan methods in AnchorRecord.java.

Minimal Reproducible Example:

pom.xml Configuration:

            <plugin>
                <groupId>org.jooq</groupId>
                <artifactId>jooq-codegen-maven</artifactId>
                <version>${jooq.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <generator>
                        <strategy>
                            <name>com.capvel.platform.core.jooq.CodeGeneratorStrategy</name>
                        </strategy>
                        <database>
                            <name>org.jooq.meta.xml.XMLDatabase</name>
                            <properties>
                                <property>
                                    <key>dialect</key>
                                    <value>POSTGRES</value>
                                </property>
                                <!-- Specify the path to your XML file here -->
                                <property>
                                    <key>xmlFile</key>
                                    <value>target/generated-resources/cms/resources/sql_information_schema.xml</value>
                                </property>
                            </properties>
                            <dateAsTimestamp>true</dateAsTimestamp>
                            <forcedTypes>
                                <forcedType>
                                    <userType>java.time.Instant</userType>
                                    <converter>com.capvel.platform.core.utils.JooqInstantConverter</converter>
                                    <includeTypes>TIMESTAMP</includeTypes>
                                </forcedType>
                                <forcedType>
                                    <name>DOUBLE</name>
                                    <includeExpression>.*</includeExpression>
                                    <includeTypes>DOUBLE PRECISION</includeTypes>
                                </forcedType>
                                <!-- Converter for fields of type EncryptedString -->
                                <forcedType>
                                    <userType>com.capvel.platform.core.types.EncryptedString</userType>
                                    <binding>com.capvel.platform.core.jooq.EncryptedStringBinding</binding>
                                    <types>TEXT</types>
                                    <includeExpression>.*__ENCRYPTED</includeExpression>
                                    <includeTypes>TEXT</includeTypes>
                                </forcedType>
                                <forcedType>
                                    <name>VARCHAR</name>
                                    <includeExpression>.*</includeExpression>
                                    <includeTypes>TEXT</includeTypes>
                                </forcedType>
                            </forcedTypes>
                        </database>
                        <generate>
                            <pojos>true</pojos>
                            <javadoc>true</javadoc>
                            <pojosToString>true</pojosToString>
                            <serializablePojos>true</serializablePojos>
                        </generate>
                        <target>
                            <packageName>com.capvel.customdoc.metadata.generated</packageName>
                            <directory>target/generated-sources/jooq</directory>
                        </target>
                    </generator>
                </configuration>
            </plugin>

src/main/resources/schema.xml:

  - tableName: "Anchor"
    fieldName: "pan"
    columnName: "pan"
    dataType: "TEXT"
    javaDataType: "java.lang.String"
    fieldDataType: "ENCRYPTED"
    isNameField: false
    isVisible: false
    isStandard: false
    isUnique: true
    isNullable: false
    defaultValue: 
    showChoiceAsBadge: false
    choices:
    relatedDocumentApiName: ""
    index: "3"

com.capvel.platform.core.jooq.CodeGeneratorStrategy.java:

public class CodeGeneratorStrategy extends KeepNamesGeneratorStrategy {

  private String removeEncryptedSuffix(String name) {
    final String SUFFIX = "__ENCRYPTED";
    return name.endsWith(SUFFIX) 
        ? name.substring(0, name.length() - SUFFIX.length()) 
        : name;
  }

  @Override
  public String getJavaSetterName(Definition definition, GeneratorStrategy.Mode mode) {
    String cleanName = removeEncryptedSuffix(definition.getOutputName());
    return "set" + StringUtils.toCamelCase(cleanName);
  }

  @Override
  public String getJavaGetterName(Definition definition, Mode mode) {
    String cleanName = removeEncryptedSuffix(definition.getOutputName());
    return "get" + StringUtils.toCamelCase(cleanName);
  }

  @Override
  public String getJavaMethodName(Definition definition, Mode mode) {
    String cleanName = removeEncryptedSuffix(definition.getOutputName());
    return "call" + StringUtils.toCamelCase(cleanName);
  }

  @Override
  public String getJavaMemberName(Definition definition, GeneratorStrategy.Mode mode) {
    String cleanName = removeEncryptedSuffix(definition.getOutputName());
    return StringUtils.toCamelCase(cleanName);
  }

  @Override
  public String getJavaClassName(Definition definition, GeneratorStrategy.Mode mode) {
    String baseName = definition.getOutputName();
    return switch (mode) {
      case POJO -> baseName + "DTO";
      case RECORD -> baseName + "Record";
      default -> baseName;
    };
  }
}

Dummy User Type (EncryptedString.java):

public class EncryptedString {
    private final String value;}

Dummy Binding (EncryptedStringBinding.java):

public class EncryptedStringBinding implements Binding<String, EncryptedString>

Steps to reproduce the problem

Set up a minimal Maven project with the files above. Run mvn clean generate-sources. Observe the generated files.

Actual Generated Code Snippets:

Anchor.java (Table class):

// Note: Field name is still pan__ENCRYPTED, ignoring getJavaMemberName
public final TableField<AnchorRecord, EncryptedString> pan__ENCRYPTED = createField(DSL.name("pan__ENCRYPTED"), SQLDataType.CLOB.nullable(false), this, "", new EncryptedStringBinding()); 

AnchorRecord.java (Record class):

// Note: Getter/Setter names ARE correct, respecting the strategy
public void setPan(EncryptedString value) { set(9, value); } 
public EncryptedString getPan() { return (EncryptedString) get(9); } 

The behavior seems tied to the presence of <binding> or <converter> in the <forcedType> for the same column.

jOOQ Version

JOOQ Version: 3.19.10

Database product and version

org.jooq.meta.xml.XMLDatabase

Java Version

openjdk version "17.0.14" 2025-01-21 LTS

JDBC / R2DBC driver name and version (include name if unofficial driver)

No response

lukaseder commented 1 week ago

A TableField isn't a "member" as per the understanding of the GeneratorStrategy. It's an "identifier." See: https://www.jooq.org/doc/latest/manual/code-generation/codegen-object-naming/codegen-generatorstrategy/

    /**
     * Override this to specifiy what identifiers in Java should look like.
     * This will just take the identifier as defined in the database.
     */
    @Override
    public String getJavaIdentifier(Definition definition) {
        // The DefaultGeneratorStrategy disambiguates some synthetic object names,
        // such as the MySQL PRIMARY key names, which do not really have a name
        // Uncomment the below code if you want to reuse that logic.
        // if (definition instanceof IndexDefinition)
        //     return super.getJavaIdentifier(definition);
        return definition.getOutputName();
    }

    /**
     * Override this method to define how Java members should be named. This is
     * used for POJOs and method arguments
     */
    @Override
    public String getJavaMemberName(Definition definition, Mode mode) {
        return definition.getOutputName();
    }

I guess both the manual and the Javadoc could be a bit more clear.

lukaseder commented 1 week ago

I guess both the manual and the Javadoc could be a bit more clear.

Though, I think it's explained well enough:

Image

Architha18 commented 1 week ago

Is there any chance of a condition within the jOOQ generator (possibly specific to XMLDatabase + bindings) where it decides not to call the configured strategy's getJavaIdentifier method for column definitions, perhaps falling back to some internal default that results in PascalCase (FieldName). @lukaseder

lukaseder commented 1 week ago

Is there any chance of a condition within the jOOQ generator (possibly specific to XMLDatabase + bindings) where it decides not to call the configured strategy's getJavaIdentifier method for column definitions, perhaps falling back to some internal default that results in PascalCase (FieldName). @lukaseder

I don't understnad why you'd want this. You can easily configure a generator strategy to do exactly that, why would that ever be a desirable default in "special cases?"

Architha18 commented 1 week ago

Hi @lukaseder (and team),

Thank you for the quick response and the clarification regarding getJavaIdentifier controlling the TableField variable name vs. getJavaMemberName for POJO members. That distinction is helpful.

Based on that clarification, I can confirm that my custom CodeGeneratorStrategy (code previously provided) does correctly override getJavaIdentifier. Inside this method, for column definitions, I have logic that removes the ENCRYPTED suffix and returns the result using StringUtils.toCamelCase(). For the panENCRYPTED column, this method is implemented to return the string "pan".

My POM configuration uses this strategy, XMLDatabase, the POSTGRES dialect, and applies an EncryptedStringBinding via to the pan__ENCRYPTED column (full relevant POM config also provided previously).

However, the key issue I'm encountering now is this:

To verify the strategy execution, I added System.out.println statements inside the getJavaIdentifier method (specifically within the if (definition instanceof ColumnDefinition) block) to log the exact value being returned.

When I run mvn clean generate-sources -X, the Maven output does not show these System.out.println messages executing for the panENCRYPTED column definition. Other parts of the strategy do seem active (e.g., getter/setter names in the Record class are generated correctly as getPan/setPan), but the specific call to getJavaIdentifier for the panENCRYPTED column definition appears to be skipped or bypassed entirely.

The generated code for the TableField variable in Anchor.java still reflects a name potentially derived from a default mechanism (Pan - PascalCase) rather than the value my strategy's getJavaIdentifier method is designed to return (pan - camelCase):

public final TableField<AnchorRecord, EncryptedString> Pan = createField(DSL.name("pan__ENCRYPTED"), 
SQLDataType.CLOB.nullable(false), this, "", new EncryptedStringBinding());

So, to clarify my earlier question which might have been confusing: My testing indicates the generator isn't just producing unexpected casing, but it seems to be failing to call the configured getJavaIdentifier method for this specific column definition under these conditions (jOOQ 3.19.10, XMLDatabase, via , custom strategy active).

Could this behaviour (the generator not invoking the strategy's getJavaIdentifier for a column definition when a binding is applied via XMLDatabase) be a potential bug or limitation?

I've already performed clean builds and checked the classpath, and the strategy class compiles fine. Let me know if providing the verbose -X logs or any other specific information would be helpful.

Thanks again for your time and help!

lukaseder commented 1 week ago

I'll be happy to help you further if you can provide a complete reproducer based on our template here: https://github.com/jOOQ/jOOQ-mcve. Using this template, it will be very simple to see what you're doing exactly, as the template helps create "minimal, complete, verifiable examples ("MCVE").

With a description or snippets pasted to github, it will be a lot more work (and often not even possible, because of accidental omissions, or errors when simplifying things) to analyse any problem.

Often, using the template also helps spot user errors, btw.