✅ Short Answer
In Hibernate, user-defined types let you customize how your Java objects are mapped to database columns by implementing the UserType
(or the newer BasicType/AttributeConverter
in JPA).
They’re useful when you need to map non-standard data types (like custom value objects, complex types, or database-specific columns) that JPA doesn’t handle out of the box.
🔎 Detailed Explanation
🔹 What is a UserType?
- A custom type implementing
org.hibernate.usertype.UserType
gives you full control over:- How a Java value is read from the database.
- How it’s written to the database.
- How it’s compared, deep-copied, etc.
- You can use it to map:
✅ Value objects (e.g., Money, RGB Color).
✅ JSON columns (before Hibernate’s JSON support).
✅ Database types like PostgreSQL’shstore
orinet
.
public class MyCustomType implements UserType<Object> {
@Override
public int getSqlType() { ... } // JDBC SQL type code
@Override
public Object nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) { ... }
@Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) { ... }
@Override
public Class<Object> returnedClass() { ... }
@Override
public boolean equals(Object x, Object y) { ... }
@Override
public int hashCode(Object x) { ... }
@Override
public Object deepCopy(Object value) { ... }
@Override
public boolean isMutable() { ... }
}
🔹 Example: Mapping a Money type
Say you have:
public class Money {
private BigDecimal amount;
private String currency;
}
You want to map it to two DB columns: amount
and currency
.
1️⃣ Implement UserType:
public class MoneyUserType implements UserType<Money> {
@Override
public int[] sqlTypes() {
return new int[] { Types.NUMERIC, Types.VARCHAR };
}
@Override
public Class<Money> returnedClass() {
return Money.class;
}
@Override
public Money nullSafeGet(ResultSet rs, int[] positions, SharedSessionContractImplementor session, Object owner) throws SQLException {
BigDecimal amount = rs.getBigDecimal(positions[0]);
String currency = rs.getString(positions[1]);
return amount == null && currency == null ? null : new Money(amount, currency);
}
@Override
public void nullSafeSet(PreparedStatement st, Money value, int index, SharedSessionContractImplementor session) throws SQLException {
if (value == null) {
st.setNull(index, Types.NUMERIC);
st.setNull(index + 1, Types.VARCHAR);
} else {
st.setBigDecimal(index, value.getAmount());
st.setString(index + 1, value.getCurrency());
}
}
// ... implement equals(), hashCode(), deepCopy(), isMutable()
}
2️⃣ Annotate the entity field with @Type
:
@Entity
public class Product {
@Id
private Long id;
@Type(value = MoneyUserType.class)
private Money price;
}
✅ Hibernate will now use your custom type for reading and writing Money
fields.
🔹 Alternative: JPA AttributeConverter (simpler case)
If your custom type maps to a single DB column, you can use JPA’s @Converter
:
@Converter(autoApply = true)
public class StatusConverter implements AttributeConverter<StatusEnum, String> {
@Override
public String convertToDatabaseColumn(StatusEnum status) { ... }
@Override
public StatusEnum convertToEntityAttribute(String dbData) { ... }
}
✅ Use AttributeConverter
for simple mappings; use UserType
when:
- You map multiple columns.
- Need fine-grained control (e.g., custom comparison, mutability).
- Deal with DB-specific or non-standard JDBC types.
📌 Key Takeaways
✅ User-defined types let you customize how Java objects map to SQL beyond JPA’s built-ins.
✅ Implement UserType
when you need multi-column or complex mappings.
✅ Use AttributeConverter
for simpler cases with one DB column.