Null embedded object creates non null columns

I use wildfly 37 with Hibernate 6.6.19.Final.

I have an embeddable object to store price values, either in cash or in percentage (for a financial Bond for example). An empty currency means a price in percent.

@Embeddable

public class PriceStrData implements Serializable {
	@NotNull
	@Column(name="amount")
	private Double amount;

	@Column(name="currency")
	@Convert(converter = CurrencyEmptyConverter.class)
	private Currency currency;
}

And the currency converter:

@Converter
public class CurrencyEmptyConverter  implements AttributeConverter<Currency, String> {

	@Override
	public String convertToDatabaseColumn(Currency currency) {
		return currency == null ? "" : currency.getCurrencyCode();
	}
}

And my entity has:

	@Embedded
	@AttributeOverride(name = "amount", column = @Column(name="clean_unit_price_value"))
	@AttributeOverride(name = "currency", column = @Column(name="clean_unit_price_currency", length=3))
	private PriceStrData cleanUnitPrice;

The problem is : if cleanUnitPrice is null then the currency converter is called and the clean_unit_price_currency column contains an empty string and the clean_unit_price_value is null.

This is very strange as I expected both columns null if cleanUnitPrice is null. That is, no conversion should be applied for a null value.

So is it “normal” to call the converter if the embedded value is null ?

Is it a bug ?

It looks like it is the expected behaviour. See AbstractEmbeddableMapping:

        @Override
        public <X, Y> int breakDownJdbcValues(
                        @Nullable Object domainValue,
                        int offset,
                        @Nullable X x,
                        @Nullable Y y,
                        @Nonnull JdbcValueBiConsumer<X, Y> valueConsumer, @Nullable SharedSessionContractImplementor session) {
                int span = 0;
                if ( domainValue == null ) {
                        for ( int i = 0; i < attributeMappings.size(); i++ ) {
                                final var attribute = attributeMappings.get( i );
                                span += attribute.breakDownJdbcValues( null, offset + span, x, y, valueConsumer, session );
                        }
                }

Obviously we need to bind the null somehow for the parameter bindings, but it sounds wrong to me that we call the converter.
Please try to create a reproducer with our test case template and if you are able to reproduce the issue, create a bug ticket in our issue tracker and attach that reproducer.

I’ll try to test the case with other JPA provider. Payara for example.

I wonder if the JPA specs say something about a null embeddable.

For Hibernate, I know now that it is not a bug, only an unexpected behaviour, for me.

Calling the converter for a null value is expected, yes. I understand that it is somewhat of an edge case, since a missing @Embeddable object might lead you to think that you can’t convert a value if the owner doesn’t exist. Let us know how EclipseLink behaves, and if it differs, we might consider adding compatibility.

I tested with payara7 and wildfly 37. The test is very simple.

Converter class stores an empty string instead of null.

@Converter
public class StringConverter implements AttributeConverter<String, String> {

	@Override
	public String convertToDatabaseColumn(String attribute) {
		return attribute == null ? "" : attribute;
	}

	@Override
	public String convertToEntityAttribute(String dbData) {
		if(dbData == null) return null;
		dbData = dbData.strip();
		return (dbData.isEmpty()) ? null : dbData;
	}
}

the embeddable class:

@Embeddable
public class Person {

	@Convert(converter = StringConverter.class)
    private String name;

    //getters/setters
}

The entity class:

@Entity
public class Hello {
	@Id
	private Long id;

	@Embedded
	private Person person;

    //getters/setters
}

And the test case:

	public void test() {
		{
		long id = 1;
		em.createQuery("delete from Hello").executeUpdate();
		em.flush();

		//person null
		Hello hello = new Hello();
		hello.setId(id++);
		em.persist(hello);

		//empty person
		hello = new Hello();
		hello.setId(id++);
		hello.setPerson(new Person());
		em.persist(hello);

		//person with empty name
		hello = new Hello();
		hello.setId(id++);
		hello.setPerson(new Person());
		hello.getPerson().setName("");
		em.persist(hello);

		//person with regular name
		hello = new Hello();
		hello.setId(id++);
		hello.setPerson(new Person());
		hello.getPerson().setName("foo");
		em.persist(hello);

		em.flush();
		em.clear();
		}

    	List<Hello> list = em.createQuery("select o from Hello o", Hello.class).getResultList();
   		String msgPersonNull = "id %s, person is null";
   		String msg = "id %s, name '%s'";
    	for(Hello o : list) {
    		Long id = o.getId();
    		Person person = o.getPerson();
    		if(person == null) {
				logger.info(msgPersonNull.formatted(id));
    		} else {
    			String name = person.getName();
				logger.info(msg.formatted(id, name));
    		}
    	}
	}

EclipseLink and Hibernate answers are different. The sql query is

select id, coalesce(name, 'null') as name from Hello

EclipseLink answers:

id|name|
--+----+
 1|null|
 2|    |
 3|    |
 4|foo |
id 1, person is null
id 2, name 'null'
id 3, name 'null'
id 4, name 'foo'

Hibernate answers

id|name|
--+----+
 1|    |
 2|    |
 3|    |
 4|foo |
id 1, person is null
id 2, person is null
id 3, person is null
id 4, name 'foo'

In my opinion EclipseLink behaves as “expected” as it builds a null Person only when all the columns are null.

I hope this helps.

Yes, that is very helpful. Would you please be so kind an create a Jira ticket for this and attach the reproducer?