I have implemented my basic requirements, which work well in one simple scenario as mentioned below code snippet. But for new requirements what is the best way out there I need help.
New requirement: Statuses in numeric format are used on other services but in request-response status representation are these user-friendly string ["Not Started", "In Progress", "Completed"]
#AllArgsConstructor
#Getter
public enum StatusEnum {
NOT_STARTED(1,"Not Started"),
IN_PROGRESS(2, "In Progress"),
COMPLETED(3, "Completed");
private final int key;
private final String value;
}
Below is my MapStruct logic to convert enum to string and visa-versa conversion logic. This works fine for basic requirements. But what is the logic of the new requirement?
ActionItem.java:
private Constants.StatusEnum status;
Basic Requirements works with below implementation:
#AllArgsConstructor
#Getter
public enum StatusEnum {
NOT_STARTED("Not Started"),
IN_PROGRESS("In Progress"),
COMPLETED("Completed");
private final String value;
}
#Mapper
public interface ActionItemMapper extents BaseMapper {
#Mapping(source = "status", target = "status", qualifiedByName = "statusEnumToString")
ActionItemResponse toActionItemResponse(ActionItem actionItem);
}
#Mapper
public interface BaseMapper {
#Named("statusEnumToString")
default String statusEnumToString(Constants.StatusEnum statusEnum) {
return statusEnum.getValue();
}
#Named("statusStringToEnum")
default Constants.StatusEnum statusStringToEnum(String status) {
return List.of(Constants.StatusEnum.values()).stream().filter(s -> s.getValue().equals(status)).findAny()
.orElse(null);
}
}
I got the solution.
#AllArgsConstructor
#Getter
public enum StatusEnum {
NOT_STARTED(1, "Not Started"),
IN_PROGRESS(2, "In Progress"),
COMPLETED(3, "Completed");
private final String key;
private final String value;
}
#Mapper
public interface ActionItemMapper extents BaseMapper {
#Mapping(source = "status", target = "status", qualifiedByName = "statusEnumToString")
ActionItemResponse toActionItemResponse(ActionItem actionItem);
}
#Mapper
public interface BaseMapper {
#Named("statusEnumKeyToValue")
default String statusEnumKeyToValue(Integer status) {
String value = null;
for (Constants.StatusEnum statusEnum: Constants.StatusEnum.values()) {
if (statusEnum.getKey().equals(status)) {
value = statusEnum.getValue();
break;
}
}
if (value == null) {
throw new IllegalArgumentException("No status value found for status key " +status);
}
return value;
}
#Named("statusEnumValueToKey")
default Integer statusEnumValueToKey(String status) {
return statusStringToEnum(status).getKey();
}
default Constants.StatusEnum statusStringToEnum(String status) {
return List.of(Constants.StatusEnum.values()).stream().filter(s -> s.getValue().equals(status)).findAny()
.orElseThrow()
}
}
I am trying to have a class that has a certain list of objects (specified by another class) persisted in the database as a string (use JPA Converter - all good).
And then I want to use Specification to search inside that string.
What is the best way to create the predicates? I don't seem to understand the connection bettween the AttributeConverter and the Expression in the Specification.
The parent class:
#Entity #Table
public class A {
#Column #Id #GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#Column
private String name;
#Enumerated(EnumType.STRING)
private SomeType type;
#Column(length=1000) #Convert(converter = BConverter.class)
private List<B> bList;
private Integer no;
}
The listed object class:
public class B{
private String type;
private Integer quantity;
}
The converter:
#Converter
public class BConverter implements AttributeConverter<List<B>, String> {
private static final String SEPARATOR = ":";
private static final String LIST_SEPARATOR = ";";
#Override public String convertToDatabaseColumn(List<B> bList) {
return bList.stream().map(e->convertToString(e)).collect(Collectors.joining(LIST_SEPARATOR));
}
#Override public List<B> convertToEntityAttribute(String str) {
if(str==null || str.isEmpty() ) return null;
return Arrays.stream(str.split(LIST_SEPARATOR)).map(e->convertFromString(e)).collect(Collectors.toList());
}
private String convertToString(B b){
if(entity==null) return null;
return b.getType().toString() +SEPARATOR+ b.getQuantity().toString();
}
private B convertFromString(String subStr){
if(subStr==null || subStr.isEmpty() ) return null;
String[] pair = subStr.split(SEPARATOR);
return new B(pair[0],Integer.valueOf(pair[1]));
}
}
In the database should look something like:
Table A:
id: 1;
name: "Some Name";
type: "THISTYPE";
blist: "TYPE1:11;TYPE2:22";
no: 0;
id: 2;
name: "Other Name";
type: "THISTYPE";
blist: "TYPE1:45;TYPE2:56";
no: 12;
I would then like to create Specifications to search over this table for the attributes inside the bList.
For example, search by an entity that contains a B object where type=TYPE1 and a quantity>=30.
public static Specification<A> customSpecification(String type, Integer value) {
return (root, query, builder) -> ///?????????
}
Is there a way to use such specifications where the DB attribute is a String but JAVA only sees the objects?
(Using Spring 3.1 and hibernate 3.3)
I am using an IdClass with an entity that maps to a table that has 3 columns as a composite key.
My tests are failing throwing a runtime exception MappingException complaining that hibernate cannot determine the type for one of my columns used as part for the composite key. in this case it is the set column (aka in the db table as "set_id").
Here is a cut down version of my entity
#Entity
#Table(name = "the_table")
#IdClass(CompositeKey.class)
public class MyEntity {
#Id
#Column(name = "page_id")
private Integer pageId;
#Id
#Column(name = "xml_id")
private Integer xmlId;
#Id
#ManyToOne
#JoinColumn(name = "set_id")
private CustomSet set;
public CustomSet getSet() {
return set;
}
public void setSet(CustomSet set) {
this.set = set;
}
public Integer getPageId() {
return pageId;
}
public void setPageId(Integer pageId) {
this.pageId = pageId;
}
public Integer getXmlId() {
return xmlId;
}
public void setXmlId(Integer xmlId) {
this.xmlId = xmlId;
}
}
Here is the composite key id class
public class CompositeKey implements Serializable {
private Integer pageId;
private Integer xmlId;
private CustomSet set;
public CompositeKey(){}
public CompositeKey(Integer pageId, Integer xmlId, CustomSet set){
this.pageId = pageId;
this.xmlId = xmlId;
this.set = set;
}
public Integer getPageId() {
return pageId;
}
public Integer getXmlId() {
return xmlId;
}
public CustomSet getSet() {
return set;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CompositeKey)) return false;
CompositeKey that = (CompositeKey) o;
if (!getPageId().equals(that.getPageId())) return false;
if (!getXmlId().equals(that.getXmlId())) return false;
return getSet().equals(that.getSet());
}
#Override
public int hashCode() {
int result = getPageId().hashCode();
result = 31 * result + getXmlId().hashCode();
result = 31 * result + getSet().hashCode();
return result;
}
}
I found that the answer was simple enough, I needed to annotate the "set" column in the composite key class with the same mapping type as in the entity.
Also because I have table column names that are different in to the variable names in the code, I had to add the extra column annotations to the variables in the composite key class as well.
here is the code change I made to the CompositeKey...
#Column(name = "page_id")
private Integer pageId;
#Column(name = "xml_id")
private Integer xmlId;
#ManyToOne
#JoinColumn(name = "set_id")
private CustomSet set;
That's it, now hibernate knows what type of mapping and column names to use with that database table. I had assumed it would pick that all up from the entity, but I guess not.
I had a similar issue, and I solved it by just adding the annotation #ManyToOne and #JoinColumn, to one of the Primary Keys (I was unfortunately missing) on the main class. Posting here just in case someone did the same mistake.
Actually adding annotations for the attributes on the composite key caused errors for me. I don't thinks that would be the correct approach.
i'm using spring data jpa with hibernate as provider.
i'm trying to persist my enums on varchar(enum.tostring) instead of (0,1,2)
my enum class:
public enum MagasinType {
PRINCIPAL {
#Override
public String toString() {
return "Principale".toUpperCase();
}
},
SECONDARY {
#Override
public String toString() {
return "Secondaire".toUpperCase();
}
},
MOBILE {
#Override
public String toString() {
return "Mobile".toUpperCase();
}
};
public abstract String toString();
}
my converter
#Converter(autoApply = true)
public class MagasinConverter implements attributeConverter <MagasinType,String>{
#Override
public String convertToDatabaseColumn(MagasinType magasinType) {
switch (magasinType){
case MOBILE:return "MOBILE";
case PRINCIPAL:return "PRINCIPAL";
case SECONDARY:return "SECONDARY";
default:throw new IllegalArgumentException("valeur incorrecte" + magasinType);
}
}
#Override
public MagasinType convertToEntityAttribute(String s) {
switch (s){
case "MOBILE": return MagasinType.MOBILE;
case "SECONDARY": return MagasinType.SECONDARY;
case "PRINCIPAL": return MagasinType.PRINCIPAL;
default:throw new IllegalArgumentException("valeur incorrecte" + s);
}}}
my entity
#Entity
#Table(name = "MAGASIN")
public class Magasin extends AbstractEntity {
#Column(name = "LIBELLE", nullable = false)
private String libelle;
#Column(name = "DESCR")
private String descr;
#Convert(converter = MagasinConverter.class)
private MagasinType type;
#Column(name = "LOCATION")
private String localisation;
#Version
private long version;
//getters setters omitted
}
my java config : https://gist.github.com/anonymous/480ef7a58cdcc50e7481
my app.properties : https://gist.github.com/anonymous/685eaca98fcba9c33872
and finally my test method : https://gist.github.com/anonymous/8bb60fee39a201558e19
please help me on it, i want to use #convert new jpa2.1 feature instead of
#enumerated
i tried to put the annotation on the getter and it works.
now i can call the #convert to convert enums to strings and visversa when pulling from database.
the same problem happened when i added #manytoOne on my class attribute, i got a weired problem, no column was added to the table entity.
but when i annotated the getter. every thing was ok.
please take a look at my github repo to further infos
https://github.com/zirconias/RFID_REWRITE
I am trying to map a PostgreSQL custom type,named transmission_result, to a Hibernate/JPA POJO. The PostgreSQL custom type is more or less an enum type of string values.
I have created a custom EnumUserType called PGEnumUserType as well as an enum class representing the PostgreSQL enumerated values. When I run this against a real database, I receive the following error:
'ERROR: column "status" is of type transmission_result but expression is of type
character varying
Hint: You will need to rewrite or cast the expression.
Position: 135 '
Upon seeing this, I figured I needed to change my SqlTypes to Types.OTHER. But doing so breaks my integration tests (using HyperSQL in memory DB) with the message:
'Caused by: java.sql.SQLException: Table not found in statement
[select enrollment0_."id" as id1_47_0_,
enrollment0_."tpa_approval_id" as tpa2_47_0_,
enrollment0_."tpa_status_code" as tpa3_47_0_,
enrollment0_."status_message" as status4_47_0_,
enrollment0_."approval_id" as approval5_47_0_,
enrollment0_."transmission_date" as transmis6_47_0_,
enrollment0_."status" as status7_47_0_,
enrollment0_."transmitter" as transmit8_47_0_
from "transmissions" enrollment0_ where enrollment0_."id"=?]'
I'm not sure why changing the sqlType results in this error. Any help is appreciated.
JPA/Hibernate Entity:
#Entity
#Access(javax.persistence.AccessType.PROPERTY)
#Table(name="transmissions")
public class EnrollmentCycleTransmission {
// elements of enum status column
private static final String ACCEPTED_TRANSMISSION = "accepted";
private static final String REJECTED_TRANSMISSION = "rejected";
private static final String DUPLICATE_TRANSMISSION = "duplicate";
private static final String EXCEPTION_TRANSMISSION = "exception";
private static final String RETRY_TRANSMISSION = "retry";
private Long transmissionID;
private Long approvalID;
private Long transmitterID;
private TransmissionStatusType transmissionStatus;
private Date transmissionDate;
private String TPAApprovalID;
private String TPAStatusCode;
private String TPAStatusMessage;
#Column(name = "id")
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
public Long getTransmissionID() {
return transmissionID;
}
public void setTransmissionID(Long transmissionID) {
this.transmissionID = transmissionID;
}
#Column(name = "approval_id")
public Long getApprovalID() {
return approvalID;
}
public void setApprovalID(Long approvalID) {
this.approvalID = approvalID;
}
#Column(name = "transmitter")
public Long getTransmitterID() {
return transmitterID;
}
public void setTransmitterID(Long transmitterID) {
this.transmitterID = transmitterID;
}
#Column(name = "status")
#Type(type = "org.fuwt.model.PGEnumUserType" , parameters ={#org.hibernate.annotations.Parameter(name = "enumClassName",value = "org.fuwt.model.enrollment.TransmissionStatusType")} )
public TransmissionStatusType getTransmissionStatus() {
return this.transmissionStatus ;
}
public void setTransmissionStatus(TransmissionStatusType transmissionStatus) {
this.transmissionStatus = transmissionStatus;
}
#Column(name = "transmission_date")
public Date getTransmissionDate() {
return transmissionDate;
}
public void setTransmissionDate(Date transmissionDate) {
this.transmissionDate = transmissionDate;
}
#Column(name = "tpa_approval_id")
public String getTPAApprovalID() {
return TPAApprovalID;
}
public void setTPAApprovalID(String TPAApprovalID) {
this.TPAApprovalID = TPAApprovalID;
}
#Column(name = "tpa_status_code")
public String getTPAStatusCode() {
return TPAStatusCode;
}
public void setTPAStatusCode(String TPAStatusCode) {
this.TPAStatusCode = TPAStatusCode;
}
#Column(name = "status_message")
public String getTPAStatusMessage() {
return TPAStatusMessage;
}
public void setTPAStatusMessage(String TPAStatusMessage) {
this.TPAStatusMessage = TPAStatusMessage;
}
}
Custom EnumUserType:
public class PGEnumUserType implements UserType, ParameterizedType {
private Class<Enum> enumClass;
public PGEnumUserType(){
super();
}
public void setParameterValues(Properties parameters) {
String enumClassName = parameters.getProperty("enumClassName");
try {
enumClass = (Class<Enum>) Class.forName(enumClassName);
} catch (ClassNotFoundException e) {
throw new HibernateException("Enum class not found ", e);
}
}
public int[] sqlTypes() {
return new int[] {Types.VARCHAR};
}
public Class returnedClass() {
return enumClass;
}
public boolean equals(Object x, Object y) throws HibernateException {
return x==y;
}
public int hashCode(Object x) throws HibernateException {
return x.hashCode();
}
public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException {
String name = rs.getString(names[0]);
return rs.wasNull() ? null: Enum.valueOf(enumClass,name);
}
public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException {
if (value == null) {
st.setNull(index, Types.VARCHAR);
}
else {
st.setString(index,((Enum) value).name());
}
}
public Object deepCopy(Object value) throws HibernateException {
return value;
}
public boolean isMutable() {
return false; //To change body of implemented methods use File | Settings | File Templates.
}
public Serializable disassemble(Object value) throws HibernateException {
return (Enum) value;
}
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return cached;
}
public Object replace(Object original, Object target, Object owner) throws HibernateException {
return original;
}
public Object fromXMLString(String xmlValue) {
return Enum.valueOf(enumClass, xmlValue);
}
public String objectToSQLString(Object value) {
return '\'' + ( (Enum) value ).name() + '\'';
}
public String toXMLString(Object value) {
return ( (Enum) value ).name();
}
}
Enum class:
public enum TransmissionStatusType {
accepted,
rejected,
duplicate,
exception,
retry}
If you have following post_status_info enum type in PostgreSQL:
CREATE TYPE post_status_info AS ENUM (
'PENDING',
'APPROVED',
'SPAM'
)
You can easily map Java Enum to a PostgreSQL Enum column type using the following custom Hibernate Type:
public class PostgreSQLEnumType extends org.hibernate.type.EnumType {
public void nullSafeSet(
PreparedStatement st,
Object value,
int index,
SharedSessionContractImplementor session)
throws HibernateException, SQLException {
if(value == null) {
st.setNull( index, Types.OTHER );
}
else {
st.setObject(
index,
value.toString(),
Types.OTHER
);
}
}
}
To use it, you need to annotate the field with the Hibernate #Type annotation as illustrated in the following example:
#Entity(name = "Post")
#Table(name = "post")
#TypeDef(
name = "pgsql_enum",
typeClass = PostgreSQLEnumType.class
)
public static class Post {
#Id
private Long id;
private String title;
#Enumerated(EnumType.STRING)
#Column(columnDefinition = "post_status_info")
#Type( type = "pgsql_enum" )
private PostStatus status;
//Getters and setters omitted for brevity
}
That's it, it works like a charm. Here's a test on GitHub that proves it.
I figured it out. I needed to use setObject instead of setString in the nullSafeSet function and pass in the Types.OTHER as the java.sql.type to let jdbc know that it was a postgres type.
public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException {
if (value == null) {
st.setNull(index, Types.VARCHAR);
}
else {
// previously used setString, but this causes postgresql to bark about incompatible types.
// now using setObject passing in the java type for the postgres enum object
// st.setString(index,((Enum) value).name());
st.setObject(index,((Enum) value), Types.OTHER);
}
}
The following might also help to have Postgres convert strings silently to your SQL enum type so you can use #Enumerated(STRING) and don't need #Type.
CREATE CAST (character varying as post_status_type) WITH INOUT AS IMPLICIT;
A quick solution will be
jdbc:postgresql://localhost:5432/postgres?stringtype=unspecified
?stringtype=unspecified is the answer
build.gradle.kts
dependencies {
api("javax.persistence", "javax.persistence-api", "2.2")
api("org.hibernate", "hibernate-core", "5.4.21.Final")
}
In Kotlin it is important to make a generic extension with EnumType<Enum<*>>()
PostgreSQLEnumType.kt
import org.hibernate.type.EnumType
import java.sql.Types
class PostgreSQLEnumType : EnumType<Enum<*>>() {
#Throws(HibernateException::class, SQLException::class)
override fun nullSafeSet(
st: PreparedStatement,
value: Any,
index: Int,
session: SharedSessionContractImplementor) {
st.setObject(
index,
value.toString(),
Types.OTHER
)
}
}
Custom.kt
import org.hibernate.annotations.Type
import org.hibernate.annotations.TypeDef
import javax.persistence.*
#Entity
#Table(name = "custom")
#TypeDef(name = "pgsql_enum", typeClass = PostgreSQLEnumType::class)
data class Custom(
#Id #GeneratedValue #Column(name = "id")
val id: Int,
#Enumerated(EnumType.STRING) #Column(name = "status_custom") #Type(type = "pgsql_enum")
val statusCustom: StatusCustom
)
enum class StatusCustom {
FIRST, SECOND
}
A simpler option that I don't recommend is the first option in Arthur's answer which adds a parameter in the connection URL to the db so that the enum data type is not lost. I believe that the responsibility of mapping the data type between the backend server and the database is precisely the backend.
<property name="connection.url">jdbc:postgresql://localhost:5432/yourdatabase?stringtype=unspecified</property>
Source
As TypeDef has disappeared in Hibernate 6, and we thus need to annotate each affected property anyway, I've found that using
#ColumnTransformer(write="?::transmission_result ")
to force a type cast works, without any Hibernate usertype classes needed.