开发者

Trim string field in JPA

I have a db table with column of datatype char(20). I'm not allowed to change it to a varchar.

I'm writing a JPA entity mapped to t开发者_如何转开发his table. I would like the string field representing this column in my entity class to always contain the trimmed value, not the 20-character value padded with spaces that exists in the db.

I can't see any easy way to do this. (an annotation would rock!). At the moment I'm just returning a trimmed value from my getter(), but this feels like a kludge.

A google search is offering no help on this. Any ideas?


Or you can use lifecycle annotations:

@Entity
public class MyEntity {

    @PostLoad
    protected void repair(){
        if(myStringProperty!=null)myStringProperty=myStringProperty.trim();
    }

    private String myStringProperty;
    public String getMyStringProperty() {
        return myStringProperty;
    }
    public void setMyStringProperty(String myStringProperty) {
        this.myStringProperty = myStringProperty;
    }

}

If this occurs on multiple entities you can create a custom annotation and write a dedicated EntityListener.

Annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Trim {}

Listener

public class TrimListener {

    private final Map<Class<?>, Set<Field>> trimProperties = 
        new HashMap<Class<?>, Set<Field>>();

    @PostLoad
    public void repairAfterLoad(final Object entity) throws Exception {
        for (final Field fieldToTrim : getTrimProperties(entity.getClass())) {
            final String propertyValue = (String) fieldToTrim.get(entity);
            if (propertyValue != null)
                fieldToTrim.set(entity, propertyValue.trim());
        }
    }

    private Set<Field> getTrimProperties(Class<?> entityClass) throws Exception {
        if (Object.class.equals(entityClass))
            return Collections.emptySet();
        Set<Field> propertiesToTrim = trimProperties.get(entityClass);
        if (propertiesToTrim == null) {
            propertiesToTrim = new HashSet<Field>();
            for (final Field field : entityClass.getDeclaredFields()) {
                if (field.getType().equals(String.class)
                    && field.getAnnotation(Trim.class) != null) {
                    field.setAccessible(true);
                    propertiesToTrim.add(field);
                }
            }
            trimProperties.put(entityClass, propertiesToTrim);
        }
        return propertiesToTrim;
    }

}

Now annotate all relevant String fields with @Trim and register the Listener as default entity listener in your persistence.xml:

<persistence-unit ..>
    <!-- ... -->
    <default-entity-listeners>
      com.somepackage.TrimListener
      and.maybe.SomeOtherListener
    </default-entity-listeners>
</persistence-unit>

 


The accepted answer (using JPA entity listeners / @Trim annotation) is a dangerous one. Calling the setter on the retrieved entity appears to mark the entity as dirty. When I tried this myself at a root entity level (using Spring3 / hibernate), it triggered tons of extraneous updates to related entities that were otherwise not modified during the transaction. It was a real mess in production, and tracking it down to this being the cause took time.

In the end I opted to go with the more straightforward approach of trimming each of the fields manually on-demand (in a custom entity-to-domain mapper, or in the entity getter) similar to Edwin's answer.


It's an old question but it was very useful for me to get to my answer. In my case, the easiest way was to create a simple "javax.persistence.Converter", like this:

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

    @Override
    public String convertToDatabaseColumn(String attribute) {
        return attribute;
    }

    @Override
    public String convertToEntityAttribute(String dbData) {
        return dbData.trim();
    }

}

And you can use it like this:

@Entity
@Table(name = "ViewAddress")
public class PostalAddress extends DbObject { 
    @Convert(converter = StringTrimConverter.class)
    private String street;
    @Convert(converter = StringTrimConverter.class)
    private String number;
    (...)

It works just fine.


Put the annotation on the getter method, set the @Acesss to AccessType.Property and trim the field there using String.trim() method.

Or simply put the trim in the getter method and always access the field through it. It is not going to get any simpler than that.

If you do not mind using pure Hibernate and deviating from the JPA standard you can use Hibernate @ColumnTransformer provided that you have a database function to do the work of

You can find how to do it in the Hibernate reference:

http://docs.jboss.org/hibernate/core/3.6/reference/en-US/html/mapping.html#mapping-column-read-and-write

I hope that helps!


I am using this method, which makes the trimming transparent without having to use annotations in every string field. In the same package that you have your session factory class (the one you use to get Sessions, e.g org.blablabla.yourpackage.etc.SessionGetter.getSession(), you must create a file named package-info.java and put this content inside it:

@TypeDefs({
        @TypeDef(name = "trimmedStringType",
                defaultForType = String.class,
                typeClass = StringUserType.class)
})
package org.blablabla.yourpackage.etc;

import org.hibernate.annotations.TypeDef;
import org.hibernate.annotations.TypeDefs;

Then you create the class StringUserType in this same package:

import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.type.StandardBasicTypes;
import org.hibernate.usertype.EnhancedUserType;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;

public class StringUserType implements EnhancedUserType, Serializable {

    private static final int[] SQL_TYPES = new int[]{Types.VARCHAR};

    @Override
    public int[] sqlTypes() {
        return SQL_TYPES;
    }

    @Override
    public Class returnedClass() {
        return String.class;
    }

    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        if (x == y) {
            return true;
        }
        if (x == null || y == null) {
            return false;
        }
        String dtx = (String) x;
        String dty = (String) y;
        return dtx.equals(dty);
    }

    @Override
    public int hashCode(Object object) throws HibernateException {
        return object.hashCode();
    }


    @Override
    public Object nullSafeGet(ResultSet resultSet, String[] names, SessionImplementor session, Object owner)
            throws HibernateException, SQLException {
        Object s = StandardBasicTypes.STRING.nullSafeGet(resultSet, names, session, owner);
        if (s == null) {
            return null;
        }
        return s.toString().trim();
    }

    @Override
    public void nullSafeSet(PreparedStatement preparedStatement, Object value, int index, SessionImplementor session)
            throws HibernateException, SQLException {
        if (value == null) {
            StandardBasicTypes.STRING.nullSafeSet(preparedStatement, null, index, session);
        } else {
            StandardBasicTypes.STRING.nullSafeSet(preparedStatement, value.toString().trim(), index, session);
        }
    }

    @Override
    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }

    @Override
    public boolean isMutable() {
        return false;
    }

    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        return (Serializable) value;
    }

    @Override
    public Object assemble(Serializable cached, Object value) throws HibernateException {
        return cached;
    }

    @Override
    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }

    @Override
    public String objectToSQLString(Object object) {
        throw new UnsupportedOperationException();
    }

    @Override
    public String toXMLString(Object object) {
        return object.toString();
    }

    @Override
    public Object fromXMLString(String string) {
        return string;
    }

}

And that's it, no need to create custom annotations in your beans, it will "magically" trim the strings whenever you get the objects from the database.


What JPA provider are you using?

If you are using EclipseLink CHAR fields are trimmed by default. You can disable this through the session trimStrings property (ensure you have not set this).


Accepted answer works except registering the listener in persistence.xml. I did it with orm.xml.

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm 
http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd"
             version="2.1">
<persistence-unit-metadata>
    <persistence-unit-defaults>
        <entity-listeners>
            <entity-listener class="org.example.TrimListener">
         </entity-listener>
        </entity-listeners>
    </persistence-unit-defaults>
  </persistence-unit-metadata>
</entity-mappings>


All you have to do is put this on your controller and it works as expected you dont need a listener or anything of that

@InitBinder
public void initBinder(WebDataBinder binder) {
    binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
}


If your domain requirement states it needs trimmed information, you need to store the data in trimmed value. I see nothing wrong about it.

Domain Model
An object model of the domain that incorporates both behavior and data. (PEAA - Martin Fowler)

If you explicitly have to enforce the business rule at the database level, one option is that you have a choice of writing a trigger, you can use built-in SQL trim method. But it will be like using a rocket to crack an egg.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜