{"record":{"id":"4d5374364332a29e","repo":"hibernate/hibernate-orm","slug":"write-expression-in-columntransformer-for-prope","errorCode":null,"errorMessage":"Write expression in '@ColumnTransformer' for property '${propertyName}' and column '${logicalColumnName}' must contain exactly one placeholder character ('?')","messagePattern":"Write expression in '@ColumnTransformer' for property '(.+?)' and column '(.+?)' must contain exactly one placeholder character \\('\\?'\\)","errorType":"exception","errorClass":"AnnotationException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumn.java","lineNumber":324,"sourceCode":"\t\tmappingColumn.setUnique( unique );\n\t\t// if the column name is not determined, we will assign the\n\t\t// name to the unique key later this method gets called again\n\t\t// from linkValueUsingDefaultColumnNaming() in second pass\n\t\tif ( unique && nameDetermined ) {\n\t\t\t// assign a unique key name to the column\n\t\t\tgetParent().getTable().createUniqueKey( mappingColumn, getBuildingContext() );\n\t\t}\n\t\tfor ( var constraint : checkConstraints ) {\n\t\t\tmappingColumn.addCheckConstraint( constraint );\n\t\t}\n\t\tmappingColumn.setDefaultValue( defaultValue );\n\t\tmappingColumn.setOptions( options );\n\t\tmappingColumn.setComment( comment );\n\n\t\tif ( writeExpression != null ) {\n\t\t\tfinal int numberOfJdbcParams = StringHelper.count( writeExpression, '?' );\n\t\t\tif ( numberOfJdbcParams != 1 ) {\n\t\t\t\tthrow new AnnotationException(\n\t\t\t\t\t\t\"Write expression in '@ColumnTransformer' for property '\" + propertyName\n\t\t\t\t\t\t+ \"' and column '\" + logicalColumnName + \"'\"\n\t\t\t\t\t\t+ \" must contain exactly one placeholder character ('?')\"\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tmappingColumn.setResolvedCustomRead( readExpression );\n\t\tmappingColumn.setCustomWrite( writeExpression );\n\t}\n\n\tpublic boolean isNameDeferred() {\n\t\treturn mappingColumn == null || isEmpty( mappingColumn.getName() );\n\t}\n\n\t/**\n\t * Attempt to infer the column name from the explicit {@code name} given by the annotation and the property or field\n\t * name. In the case of a {@link jakarta.persistence.JoinColumn}, this is impossible, due to the rules implemented in","sourceCodeStart":306,"sourceCodeEnd":342,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumn.java#L306-L342","documentation":"@ColumnTransformer(writeExpression = ...) supplies the SQL fragment Hibernate uses when binding a value for that column, and the expression must contain exactly one '?' placeholder which Hibernate substitutes with the bound parameter. AnnotatedColumn.bind counts '?' characters in the write expression during binding and throws this AnnotationException if the count is not exactly 1.","triggerScenarios":"A @ColumnTransformer writeExpression with zero placeholders (a constant expression like \"upper('x')\") or with two or more (like \"? || '-' || ?\"), including accidental '?' characters inside string literals of the expression.","commonSituations":"Writing encryption/computed-column expressions; copy-pasting SQL from a trigger definition that used multiple bind parameters; dialect-specific fragments ported from code that previously built SQL by hand.","solutions":["Rewrite the expression so it contains exactly one '?' - put constants inline and keep a single parameter slot, e.g. \"upper(?)\" or \"my_function('const', ?)\"","If you need zero parameters (constant write), use @ColumnDefault or a generation strategy instead of @ColumnTransformer","If you need multiple parameters, wrap them in a database function and call it with the single '?', e.g. \"my_concat(?)\"","Check for stray '?' inside string literals and remove them"],"exampleFix":"// before: wrong placeholder counts\n@ColumnTransformer(writeExpression = \"concat(?, '-')\")        // ok (1), but:\n@ColumnTransformer(writeExpression = \"concat(?, ?, ?)\")      // 3 -> error\n@ColumnTransformer(writeExpression = \"now()\")                // 0 -> error\n\n// after: exactly one placeholder per write expression\n@ColumnTransformer(writeExpression = \"concat(?, '-x')\")\nprivate String code;\n\n// constant writes belong elsewhere\n@ColumnDefault(\"now()\")\n\n// multi-arg logic belongs in a DB function called with the single bind\n@ColumnTransformer(writeExpression = \"my_transform(?)\")","handlingStrategy":"validation","validationCode":"// Before boot / in unit tests: each write expression must contain exactly one '?'\nstatic boolean writeExpressionsValid(Class<?> entity) {\n    for (Field f : entity.getDeclaredFields()) {\n        ColumnTransformer t = f.getAnnotation(ColumnTransformer.class);\n        if (t != null && !t.writeExpression().isEmpty()) {\n            long count = t.writeExpression().chars().filter(c -> c == '?').count();\n            if (count != 1) return false;\n        }\n    }\n    return true;\n}","typeGuard":null,"tryCatchPattern":"try {\n    sessionFactory = metadata.buildSessionFactory();\n} catch (AnnotationException e) {\n    if (e.getMessage().contains(\"must contain exactly one placeholder\")) {\n        throw new IllegalStateException(\"@ColumnTransformer writeExpression needs exactly one '?'\", e);\n    }\n    throw e;\n}","preventionTips":["Treat the write expression as a single-parameter SQL template","Push constants into the expression text and multi-value logic into database functions","Unit-test annotation shapes on entities so typos fail before boot"],"tags":["hibernate","column-transformer","write-expression","placeholder","annotation","mapping"],"backgroundTag":"sql-placeholder-mismatch","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}