Related
I have a table with a column of type JSON in my PostgreSQL DB (9.2). I have a hard time to map this column to a JPA2 Entity field type.
I tried to use String but when I save the entity I get an exception that it can't convert character varying to JSON.
What is the correct value type to use when dealing with a JSON column?
#Entity
public class MyEntity {
private String jsonPayload; // this maps to a json column
public MyEntity() {
}
}
A simple workaround would be to define a text column.
If you're interested, here are a few code snippets to get the Hibernate custom user type in place. First extend the PostgreSQL dialect to tell it about the json type, thanks to Craig Ringer for the JAVA_OBJECT pointer:
import org.hibernate.dialect.PostgreSQL9Dialect;
import java.sql.Types;
/**
* Wrap default PostgreSQL9Dialect with 'json' type.
*
* #author timfulmer
*/
public class JsonPostgreSQLDialect extends PostgreSQL9Dialect {
public JsonPostgreSQLDialect() {
super();
this.registerColumnType(Types.JAVA_OBJECT, "json");
}
}
Next implement org.hibernate.usertype.UserType. The implementation below maps String values to the json database type, and vice-versa. Remember Strings are immutable in Java. A more complex implementation could be used to map custom Java beans to JSON stored in the database as well.
package foo;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.usertype.UserType;
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
/**
* #author timfulmer
*/
public class StringJsonUserType implements UserType {
/**
* Return the SQL type codes for the columns mapped by this type. The
* codes are defined on <tt>java.sql.Types</tt>.
*
* #return int[] the typecodes
* #see java.sql.Types
*/
#Override
public int[] sqlTypes() {
return new int[] { Types.JAVA_OBJECT};
}
/**
* The class returned by <tt>nullSafeGet()</tt>.
*
* #return Class
*/
#Override
public Class returnedClass() {
return String.class;
}
/**
* Compare two instances of the class mapped by this type for persistence "equality".
* Equality of the persistent state.
*
* #param x
* #param y
* #return boolean
*/
#Override
public boolean equals(Object x, Object y) throws HibernateException {
if( x== null){
return y== null;
}
return x.equals( y);
}
/**
* Get a hashcode for the instance, consistent with persistence "equality"
*/
#Override
public int hashCode(Object x) throws HibernateException {
return x.hashCode();
}
/**
* Retrieve an instance of the mapped class from a JDBC resultset. Implementors
* should handle possibility of null values.
*
* #param rs a JDBC result set
* #param names the column names
* #param session
* #param owner the containing entity #return Object
* #throws org.hibernate.HibernateException
*
* #throws java.sql.SQLException
*/
#Override
public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
if(rs.getString(names[0]) == null){
return null;
}
return rs.getString(names[0]);
}
/**
* Write an instance of the mapped class to a prepared statement. Implementors
* should handle possibility of null values. A multi-column type should be written
* to parameters starting from <tt>index</tt>.
*
* #param st a JDBC prepared statement
* #param value the object to write
* #param index statement parameter index
* #param session
* #throws org.hibernate.HibernateException
*
* #throws java.sql.SQLException
*/
#Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
if (value == null) {
st.setNull(index, Types.OTHER);
return;
}
st.setObject(index, value, Types.OTHER);
}
/**
* Return a deep copy of the persistent state, stopping at entities and at
* collections. It is not necessary to copy immutable objects, or null
* values, in which case it is safe to simply return the argument.
*
* #param value the object to be cloned, which may be null
* #return Object a copy
*/
#Override
public Object deepCopy(Object value) throws HibernateException {
return value;
}
/**
* Are objects of this type mutable?
*
* #return boolean
*/
#Override
public boolean isMutable() {
return true;
}
/**
* Transform the object into its cacheable representation. At the very least this
* method should perform a deep copy if the type is mutable. That may not be enough
* for some implementations, however; for example, associations must be cached as
* identifier values. (optional operation)
*
* #param value the object to be cached
* #return a cachable representation of the object
* #throws org.hibernate.HibernateException
*
*/
#Override
public Serializable disassemble(Object value) throws HibernateException {
return (String)this.deepCopy( value);
}
/**
* Reconstruct an object from the cacheable representation. At the very least this
* method should perform a deep copy if the type is mutable. (optional operation)
*
* #param cached the object to be cached
* #param owner the owner of the cached object
* #return a reconstructed object from the cachable representation
* #throws org.hibernate.HibernateException
*
*/
#Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return this.deepCopy( cached);
}
/**
* During merge, replace the existing (target) value in the entity we are merging to
* with a new (original) value from the detached entity we are merging. For immutable
* objects, or null values, it is safe to simply return the first parameter. For
* mutable objects, it is safe to return a copy of the first parameter. For objects
* with component values, it might make sense to recursively replace component values.
*
* #param original the value from the detached entity being merged
* #param target the value in the managed entity
* #return the value to be merged
*/
#Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
return original;
}
}
Now all that's left is annotating the entities. Put something like this at the entity's class declaration:
#TypeDefs( {#TypeDef( name= "StringJsonObject", typeClass = StringJsonUserType.class)})
Then annotate the property:
#Type(type = "StringJsonObject")
public String getBar() {
return bar;
}
Hibernate will take care of creating the column with json type for you, and handle the mapping back and forth. Inject additional libraries into the user type implementation for more advanced mapping.
Here's a quick sample GitHub project if anyone wants to play around with it:
https://github.com/timfulmer/hibernate-postgres-jsontype
See PgJDBC bug #265.
PostgreSQL is excessively, annoyingly strict about data type conversions. It won't implicitly cast text even to text-like values such as xml and json.
The strictly correct way to solve this problem is to write a custom Hibernate mapping type that uses the JDBC setObject method. This can be a fair bit of hassle, so you might just want to make PostgreSQL less strict by creating a weaker cast.
As noted by #markdsievers in the comments and this blog post, the original solution in this answer bypasses JSON validation. So it's not really what you want. It's safer to write:
CREATE OR REPLACE FUNCTION json_intext(text) RETURNS json AS $$
SELECT json_in($1::cstring);
$$ LANGUAGE SQL IMMUTABLE;
CREATE CAST (text AS json) WITH FUNCTION json_intext(text) AS IMPLICIT;
AS IMPLICIT tells PostgreSQL it can convert without being explicitly told to, allowing things like this to work:
regress=# CREATE TABLE jsontext(x json);
CREATE TABLE
regress=# PREPARE test(text) AS INSERT INTO jsontext(x) VALUES ($1);
PREPARE
regress=# EXECUTE test('{}')
INSERT 0 1
Thanks to #markdsievers for pointing out the issue.
Maven dependency
The first thing you need to do is to set up the following Hibernate Types Maven dependency in your project pom.xml configuration file:
<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>hibernate-types-52</artifactId>
<version>${hibernate-types.version}</version>
</dependency>
Domain model
Now, you need to declare the JsonType on either class level or in a package-info.java package-level descriptor, like this:
#TypeDef(name = "json", typeClass = JsonType.class)
And, the entity mapping will look like this:
#Type(type = "json")
#Column(columnDefinition = "jsonb")
private Location location;
If you're using Hibernate 5 or later, then the JSON type is registered automatically by the Postgre92Dialect.
Otherwise, you need to register it yourself:
public class PostgreSQLDialect extends PostgreSQL91Dialect {
public PostgreSQL92Dialect() {
super();
this.registerColumnType( Types.JAVA_OBJECT, "jsonb" );
}
}
In case someone is interested, you can use JPA 2.1 #Convert / #Converter functionality with Hibernate. You would have to use the pgjdbc-ng JDBC driver though. This way you don't have to use any proprietary extensions, dialects and custom types per field.
#javax.persistence.Converter
public static class MyCustomConverter implements AttributeConverter<MuCustomClass, String> {
#Override
#NotNull
public String convertToDatabaseColumn(#NotNull MuCustomClass myCustomObject) {
...
}
#Override
#NotNull
public MuCustomClass convertToEntityAttribute(#NotNull String databaseDataAsJSONString) {
...
}
}
...
#Convert(converter = MyCustomConverter.class)
private MyCustomClass attribute;
I tried many methods I found on the Internet, most of them are not working, some of them are too complex. The below one works for me and is much more simple if you don't have that strict requirements for PostgreSQL type validation.
Make PostgreSQL jdbc string type as unspecified, like
<connection-url>
jdbc:postgresql://localhost:test?stringtype=unspecified
</connection-url>
I had a similar problem with Postgres (javax.persistence.PersistenceException: org.hibernate.MappingException: No Dialect mapping for JDBC type: 1111) when executing native queries (via EntityManager) that retrieved json fields in the projection although the Entity class has been annotated with TypeDefs.
The same query translated in HQL was executed without any problem.
To solve this I had to modify JsonPostgreSQLDialect this way:
public class JsonPostgreSQLDialect extends PostgreSQL9Dialect {
public JsonPostgreSQLDialect() {
super();
this.registerColumnType(Types.JAVA_OBJECT, "json");
this.registerHibernateType(Types.OTHER, "myCustomType.StringJsonUserType");
}
Where myCustomType.StringJsonUserType is the class name of the class implementing the json type (from above, Tim Fulmer answer) .
There is an easier to to do this which doesn't involve creating a function by using WITH INOUT
CREATE TABLE jsontext(x json);
INSERT INTO jsontext VALUES ($${"a":1}$$::text);
ERROR: column "x" is of type json but expression is of type text
LINE 1: INSERT INTO jsontext VALUES ($${"a":1}$$::text);
CREATE CAST (text AS json)
WITH INOUT
AS ASSIGNMENT;
INSERT INTO jsontext VALUES ($${"a":1}$$::text);
INSERT 0 1
I was running into this and didn't want to enable stuff via connection string, and allow implicit conversions. At first I tried to use #Type, but because I'm using a custom converter to serialize/deserialize a Map to/from JSON, I couldn't apply a #Type annotation. Turns out I just needed to specify columnDefinition = "json" in my #Column annotation.
#Convert(converter = HashMapConverter.class)
#Column(name = "extra_fields", columnDefinition = "json")
private Map<String, String> extraFields;
All the above solution did not work for me. Finally I made use of native queries to insert the data.
Step -1 Create an abstract class AbstractEntity which will implements Persistable
with annotation #MappedSuperclass (part of javax.persistence)
Step -2 In this class create your sequence generator because you can not generate a sequencer with the native queries. #Id #GeneratedValues #Column private Long seqid;
Dont forget - Your entity class should extends your abstract class. (helping your sequence to work as well it may works on date as well(check for date i am not sure))
Step- 3 In repo interface write the native query.
value="INSERT INTO table(?,?)values(:?,:cast(:jsonString as json))",nativeQuery=true
Step - 4 This will convert your java string object to json and insert/store in database and also you will be able to increment the sequence on each insertion as well.
I got casting error when I worked using converter. Also type-52 personally I avoided to use that in my project.
Please upvote my ans if it works for you guys.
I ran into this issue when I migrated my projects from MySQL 8.0.21 to Postgres 13. My project uses Spring boot with the Hibernate types dependency version 2.7.1. In my case the solution was simple.
All I needed to do was change that and it worked.
Referenced from the Hibernate Types Documentation page.
I encountered the column "roles" is of type json but expression is of type character varying exception with the following entity with Postgres:
#Entity
#TypeDefs(#TypeDef(name = "json", typeClass = JsonBinaryType.class))
#Data
#AllArgsConstructor
#NoArgsConstructor
#Builder
#EqualsAndHashCode(of = "extId")
public class ManualTaskUser {
#Id
private String extId;
#Type(type = "json")
#Column(columnDefinition = "json")
private Set<Role> roles;
}
It should be mentioned that Role is an enum and not a POJO.
In the generated SQL I could see that the Set was correctly serialized like this: ["SYSTEM","JOURNEY","ADMIN","OBJECTION","DEVOPS","ASSESSMENT"].
Changing the typeClass in the TypeDef annotation from JsonStringType to JsonBinaryType solved the problem! Thanks to Joseph Waweru for the hint!
I'm trying to use QueryDSL with Spring Data JPA, I want to use findAll with pagination but the count is always performed, also if return type is a List.
I don't need this count because it is really slow and I could loose the benefit of pagination.
Any solutions for this problem?
This is the count(), it requires about 30 seconds on MySQL:
Mysql too slow on simple query between two tables
In any case I don't want repeat the count for each page I require, this information is required just for first call.
Since QuerydslPredicateExecutor does not support returning Slice as the return value of findAll(Predicate, Pageable), so the Count Query seems unavoidable. But you can define a new base repository interface and implement the findAll method in a way that it does not issue a count query for pagination. For starters, you should define an interface which will be used as the base interface for all other Repositories:
/**
* Interface for adding one method to all repositories.
*
* <p>The main motivation of this interface is to provide a way
* to paginate list of items without issuing a count query
* beforehand. Basically we're going to get one element more
* than requested and form a {#link Page} object out of it.</p>
*/
#NoRepositoryBean
public interface SliceableRepository<T, ID extends Serializable>
extends JpaRepository<T, ID>,
QuerydslPredicateExecutor<T> {
Page<T> findAll(Predicate predicate, Pageable pageable);
}
Then, implement this interface like:
public class SliceableRepositoryImpl<T, ID extends Serializable>
extends QueryDslJpaRepository<T, ID>
implements SliceableRepository<T, ID> {
private static final EntityPathResolver DEFAULT_ENTITY_PATH_RESOLVER = SimpleEntityPathResolver.INSTANCE;
private final EntityPath<T> path;
private final PathBuilder<T> builder;
private final Querydsl querydsl;
public SliceableRepositoryImpl(JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
path = DEFAULT_ENTITY_PATH_RESOLVER.createPath(entityInformation.getJavaType());
this.builder = new PathBuilder<>(path.getType(), path.getMetadata());
this.querydsl = new Querydsl(entityManager, builder);
}
#Override
public Page<T> findAll(Predicate predicate, Pageable pageable) {
int oneMore = pageable.getPageSize() + 1;
JPQLQuery query = createQuery(predicate)
.offset(pageable.getOffset())
.limit(oneMore);
Sort sort = pageable.getSort();
query = querydsl.applySorting(sort, query);
List<T> entities = query.list(path);
int size = entities.size();
if (size > pageable.getPageSize())
entities.remove(size - 1);
return new PageImpl<>(entities, pageable, pageable.getOffset() + size);
}
}
Basically, this implementation would fetch one more element than requested size and use the result for constructing a Page. Then you should tell Spring Data to use this implementation as the repository base class:
#EnableJpaRepositories(repositoryBaseClass = SliceableRepositoryImpl.class)
And finally extend the SliceableRepository as your base interface:
public SomeRepository extends SliceableRepository<Some, SomeID> {}
FYI there is a spring jira issue:
https://jira.spring.io/browse/DATAJPA-289
Let's vote for this improvement
In case anyone lands here looking for how to achieve the same affect in Spring Data MongoDB as Ali did above for Spring Data JPA, here's my solution modeled on his:
import java.io.Serializable;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
import org.springframework.data.mongodb.repository.support.QueryDslMongoRepository;
import org.springframework.data.mongodb.repository.support.SpringDataMongodbQuery;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.QSort;
import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
import org.springframework.data.repository.core.EntityInformation;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.mongodb.AbstractMongodbQuery;
/**
* Custom extension of {#link QueryDslMongoRepository} that avoids unnecessary MongoDB "count"
* operations
* <p>
* {#link QueryDslPredicateExecutor#findAll(Predicate, Pageable)} returns a {#link Page} at
* potentially great expense because determining the {#link Page}'s "totalElements" property
* requires doing a potentially expensive MongoDB "count" operation. We'd prefer a "findAll"-like
* method that returns a {#link Slice} (which doesn't have a "totalElements" property) but no such
* method exists. See {#link #findAll(Predicate, Pageable)} for more details.
*
* #see https://github.com/spring-projects/spring-data-commons/issues/1011
* #see https://stackoverflow.com/questions/37254385/querydsl-springdata-jpa-findall-how-to-avoid-count
*/
public class MyQueryDslMongoRepository<T, ID extends Serializable> extends QueryDslMongoRepository<T, ID>
implements MyAbstractRepository<T, ID> {
private final PathBuilder<T> builder;
private final EntityInformation<T, ID> entityInformation;
private final MongoOperations mongoOperations;
public BTQueryDslMongoRepository(MongoEntityInformation<T, ID> entityInformation, MongoOperations mongoOperations) {
this(entityInformation, mongoOperations, SimpleEntityPathResolver.INSTANCE);
}
public BTQueryDslMongoRepository(MongoEntityInformation<T, ID> entityInformation, MongoOperations mongoOperations,
EntityPathResolver resolver) {
super(entityInformation, mongoOperations, resolver);
EntityPath<T> path = resolver.createPath(entityInformation.getJavaType());
this.builder = new PathBuilder<T>(path.getType(), path.getMetadata());
this.entityInformation = entityInformation;
this.mongoOperations = mongoOperations;
}
/**
* An override of our superclass method to return a fake but cheaper-to-compute {#link Page}
* that's adequate for our purposes.
*/
#Override
public Page<T> findAll(Predicate predicate, Pageable pageable) {
int pageSize = pageable.getPageSize();
SpringDataMongodbQuery<T> query = new SpringDataMongodbQuery<T>(mongoOperations, entityInformation.getJavaType())
.where(predicate)
.offset(pageable.getOffset())
.limit(pageSize + 1);
applySorting(query, pageable.getSort());
List<T> entities = query.fetch();
int numFetched = entities.size();
if (numFetched > pageSize) {
entities.remove(numFetched - 1);
}
return new PageImpl<T>(entities, pageable, pageable.getOffset() + numFetched);
}
/**
* Applies the given {#link Sort} to the given {#link MongodbQuery}.
* <p>
* Copied from {#link QueryDslMongoRepository}
*/
private AbstractMongodbQuery<T, SpringDataMongodbQuery<T>> applySorting(
AbstractMongodbQuery<T, SpringDataMongodbQuery<T>> query, Sort sort) {
if (sort == null) {
return query;
}
// TODO: find better solution than instanceof check
if (sort instanceof QSort) {
List<OrderSpecifier<?>> orderSpecifiers = ((QSort) sort).getOrderSpecifiers();
query.orderBy(orderSpecifiers.toArray(new OrderSpecifier<?>[orderSpecifiers.size()]));
return query;
}
for (Order order : sort) {
query.orderBy(toOrder(order));
}
return query;
}
/**
* Transforms a plain {#link Order} into a QueryDsl specific {#link OrderSpecifier}.
* <p>
* Copied from {#link QueryDslMongoRepository}
*/
#SuppressWarnings({ "rawtypes", "unchecked" })
private OrderSpecifier<?> toOrder(Order order) {
Expression<Object> property = builder.get(order.getProperty());
return new OrderSpecifier(
order.isAscending() ? com.querydsl.core.types.Order.ASC : com.querydsl.core.types.Order.DESC, property);
}
}
#NoRepositoryBean
public interface MyAbstractRepository<T, ID extends Serializable> extends Repository<T, ID>,
QueryDslPredicateExecutor<T> {
#Override
Page<T> findAll(Predicate predicate, Pageable pageable);
}
The above works for Spring Data MongoDB 1.10.23 but I assume can be modified to be made to work for more modern versions.
Based on the answer of Ali Dehghani we build the following for querydsl 4.2.1, because the querydsl syntax changed in the current version 4.x
Repository Interface:
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Predicate;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
public interface SliceableRepository<T> {
Slice<T> findAllSliced(EntityPath<T> entityPath, Predicate predicate, Pageable pageable);
}
Repository Implementation:
(Must be named "<Interface-Name>Impl")
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.JPQLQuery;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import javax.persistence.EntityManager;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.data.jpa.repository.support.Querydsl;
public class SliceableRepositoryImpl<T> implements SliceableRepository<T> {
private final EntityManager entityManager;
private final JPAQueryFactory jpaQueryFactory;
public SliceableRepositoryImpl(EntityManager entityManager) {
this.entityManager = entityManager;
this.jpaQueryFactory = new JPAQueryFactory(entityManager);
}
#Override
public Slice<T> findAllSliced(final EntityPath<T> entityPath, final Predicate predicate,
final Pageable pageable) {
final Querydsl querydsl = new Querydsl(entityManager,
new PathBuilder<>(entityPath.getType(), entityPath.getMetadata()));
final int oneMore = pageable.getPageSize() + 1;
final JPAQuery<T> query = this.jpaQueryFactory.selectFrom(entityPath)
.where(predicate)
.offset(pageable.getOffset())
.limit(oneMore);
final JPQLQuery<T> querySorted = querydsl.applySorting(pageable.getSort(), query);
final List<T> entities = querySorted.fetch();
final int size = entities.size();
// If there was one more result than requested from the pageable,
// then the slice gets "hasNext"=true
final boolean hasNext = size > pageable.getPageSize();
if (hasNext) {
entities.remove(size - 1);
}
return new SliceImpl<>(entities, pageable, hasNext);
}
}
Use the new repository as fragment in your other repositories:
public SomeRepository extends JpaRepository<Some, Long>, SliceableRepository<Some> {
}
#EnableJpaRepositories(repositoryBaseClass = SliceableRepositoryImpl.class) is NOT needed
Then use it like:
public class MyService {
#Autowired
private final SomeRepository someRepository;
public void doSomething() {
Predicate predicate = ...
Pageable pageable = ...
// QSome is the generated model class from querydsl
Slice<Some> result = someRepository.findAllSliced(QSome.some, predicate, pageable);
}
}
I have a very basic JAX-RS service (the BookService class below) which allows for the creation of entities of type Book (also below). POSTing the payload
{
"acquisitionDate": 1418849700000,
"name": "Funny Title",
"numberOfPages": 100
}
successfully persists the Book and returns 201 CREATED. However, including an id attribute with whichever non-null value on the payload triggers an org.hibernate.PersistentObjectException with the message detached entity passed to persist. I understand what this means, and including an id on the payload when creating an object (in this case) makes no sense. However, I'd prefer to prevent this exception from bubbling all the way up and present my users with, for instance, a 400 BAD REQUEST in this case (or, at least, ignore the attribute altogether). However, there are two main concerns:
The exception that arrives at create is an EJBTransactionRolledbackException and I'd have to crawl all the way down the stack trace to discover the root cause;
The root cause is org.hibernate.PersistentObjectException - I'm deploying to Wildfly which uses Hibernate, but I want to maintain my code portable, so I don't really want to catch this specific exception.
To my understanding, there are two possible solutions:
Use book.setId(null) before bookRepo.create(book). This would ignore the fact that the id attribute carries a value and proceed with the request.
Check if book.getId() != null and throw something like IllegalArgumentException that could be mapped to a 400 status code. Seems the preferable solution.
However, coming from other frameworks (like Django Rest Framework, for example) I'd really prefer this to be handled by the framework itself... My question then is, is there any built-in way to achieve this behaviour that I may be missing?
This is the BookService class:
#Stateless
#Path("/books")
public class BookService {
#Inject
private BookRepo bookRepo;
#Context
UriInfo uriInfo;
#Consumes(MediaType.APPLICATION_JSON)
#Path("/")
#POST
#Produces(MediaType.APPLICATION_JSON)
public Response create(#Valid Book book) {
bookRepo.create(book);
return Response.created(getBookUri(book)).build();
}
private URI getBookUri(Book book) {
return uriInfo.getAbsolutePathBuilder()
.path(book.getId().toString()).build();
}
}
This is the Book class:
#Entity
#Table(name = "books")
public class Book {
#Column(nullable = false)
#NotNull
#Temporal(TemporalType.TIMESTAMP)
private Date acquisitionDate;
#Column(nullable = false, updatable = false)
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Id
private Integer id;
#Column(nullable = false)
#NotNull
#Size(max = 255, min = 1)
private String name;
#Column(nullable = false)
#Min(value = 1)
#NotNull
private Integer numberOfPages;
(getters/setters/...)
}
This is the BookRepo class:
#Stateless
public class BookRepo {
#PersistenceContext(unitName = "book-repo")
protected EntityManager em;
public void create(Book book) {
em.persist(book);
}
}
I don't know if this is really the answer you're looking for, but I was just playing around with the idea and implemented something.
The JAX-RS 2 spec defines a model for bean validation, so I thought maybe you could tap into that. All bad validations will get mapped to a 400. You stated "I'd prefer to prevent this exception from bubbling all the way up and present my users with, for instance, a 400 BAD REQUEST", but with bad validation you will get that anyway. So however you plan to handle validation exceptions (if at all), you can do the same here.
Basically I just created a constraint annotation to validate for a null value in the id field. You can define the id field's name in the annotation through the idField annotation attribute, so you are not restricted to id. Also this can be used for other objects too, so you don't have to repeatedly check the value, as you suggested in your second solution.
You can play around with it. Just thought I'd throw this option out there.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
#Constraint(validatedBy = NoId.NoIdValidator.class)
#Target({ElementType.PARAMETER})
#Retention(RUNTIME)
public #interface NoId {
String message() default "Cannot have value for id attribute";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String idField() default "id";
public static class NoIdValidator implements ConstraintValidator<NoId, Object> {
private String idField;
#Override
public void initialize(NoId annotation) {
idField = annotation.idField();
}
#Override
public boolean isValid(Object bean, ConstraintValidatorContext cvc) {
boolean isValid = false;
try {
Field field = bean.getClass().getDeclaredField(idField);
if (field == null) {
isValid = true;
} else {
field.setAccessible(true);
Object value = field.get(bean);
if (value == null) {
isValid = true;
}
}
} catch (NoSuchFieldException
| SecurityException
| IllegalArgumentException
| IllegalAccessException ex) {
Logger.getLogger(NoId.class.getName()).log(Level.SEVERE, null, ex);
}
return isValid;
}
}
}
Usage:
#POST
#Consumes(MediaType.APPLICATION_JSON)
public Response createBook(#Valid #NoId(idField = "id") Book book) {
book.setId(1);
return Response.created(URI.create("http://blah.com/books/1"))
.entity(book).build();
}
Note the default idField is id, so if you don't specify it, it will look for the id field in the object class. You can also specify the message as you would any other constraint annotation:
#NoId(idField = "bookId", message = "bookId must not be specified")
// default "Cannot have value for id attribute"
Situation:
I have a persistable class with variable of java.util.Date type:
import java.util.Date;
#Entity
#Table(name = "prd_period")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Period extends ManagedEntity implements Interval {
#Column(name = "startdate_", nullable = false)
private Date startDate;
}
Corresponding table in DB:
CREATE TABLE 'prd_period' (
'id_' bigint(20) NOT NULL AUTO_INCREMENT,
...
'startdate_' datetime NOT NULL
)
Then I save my Period object to DB:
Period p = new Period();
Date d = new Date();
p.setStartDate();
myDao.save(p);
After then if I'm trying to extract my object from DB, it is returned with variable startDate of Timestamp type - and all the places where I'm trying to use equals(...) are returning false.
Question: are there any means to force Hibernate to return dates as object of java.util.Date type instead of Timestamp without explicit modification of every such variable (e.g it must be able just work, without explicit modification of existed variables of java.util.Date type)?
NOTE:
I found number of explicit solutions, where annotations are used or setter is modified - but I have many classes with Date-variables - so I need some centralized solution and all that described below is not good enough:
Using annotation #Type: - java.sql.Date will be returned
#Column
#Type(type="date")
private Date startDate;
Using annotation #Temporal(TemporalType.DATE) - java.sql.Date will be returned
#Temporal(TemporalType.DATE)
#Column(name=”CREATION_DATE”)
private Date startDate;
By modifying setter (deep copy) - java.util.Date will be returned
public void setStartDate(Date startDate) {
if (startDate != null) {
this.startDate = new Date(startDate.getTime());
} else {
this.startDate = null;
}
}
By creation of my own type: - java.util.Date will be returned
Details are given here:
http://blogs.sourceallies.com/2012/02/hibernate-date-vs-timestamp/
So, I spent some time with this issue and found a solution. It is not pretty one, but at least a start point - maybe someone will supplement this with some useful comments.
Some info about mapping that I found in process:
Class that contains basic mapping of Hibernate types to property types is org.hibernate.type.TypeFactory. All this mappings are stored in unmodifiable map
private static final Map BASIC_TYPES;
...
basics.put( java.util.Date.class.getName(), Hibernate.TIMESTAMP );
...
BASIC_TYPES = Collections.unmodifiableMap( basics );
As you can see with java.util.Date type assosited with Hibernate type org.hibernate.type.TimestampType
Next interesting moment - creation of Hibernate org.hibernate.cfg.Configuration - object that contains all info about mapped classes. This classes and their properties can be extracted like this:
Iterator clsMappings = cfg.getClassMappings();
while(clsMappings.hasNext()){
PersistentClass mapping = (PersistentClass) clsMappings.next();
handleProperties(mapping.getPropertyIterator(), map);
}
Vast majority of properties are the objects of org.hibernate.mapping.SimpleValue types. Our point of interest is the method SimpleValue.getType() - in this method is defined what type will be used to convert properties values back-and-forth while working with DB
Type result = TypeFactory.heuristicType(typeName, typeParameters);
At this point I understand that I am unable to modify BASIC_TYPES - so the only way - to replace SimpleValue object to the properties of java.util.Date types to my custom Object that will be able to know the exact type to convert.
The solution:
Create custom container entity manager factory by extending HibernatePersistence class and overriding its method createContainerEntityManagerFactory:
public class HibernatePersistenceExtensions extends HibernatePersistence {
#Override
public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map map) {
if ("true".equals(map.get("hibernate.use.custom.entity.manager.factory"))) {
return CustomeEntityManagerFactoryFactory.createCustomEntityManagerFactory(info, map);
} else {
return super.createContainerEntityManagerFactory(info, map);
}
}
}
Create Hibernate configuration object, modify value ojects for java.util.Date properties and then create custom entity manager factory.
public class ReattachingEntityManagerFactoryFactory {
#SuppressWarnings("rawtypes")
public static EntityManagerFactory createContainerEntityManagerFactory(
PersistenceUnitInfo info, Map map) {
Ejb3Configuration cfg = new Ejb3Configuration();
Ejb3Configuration configured = cfg.configure( info, map );
handleClassMappings(cfg, map);
return configured != null ? configured.buildEntityManagerFactory() : null;
}
#SuppressWarnings("rawtypes")
private static void handleClassMappings(Ejb3Configuration cfg, Map map) {
Iterator clsMappings = cfg.getClassMappings();
while(clsMappings.hasNext()){
PersistentClass mapping = (PersistentClass) clsMappings.next();
handleProperties(mapping.getPropertyIterator(), map);
}
}
private static void handleProperties(Iterator props, Map map) {
while(props.hasNext()){
Property prop = (Property) props.next();
Value value = prop.getValue();
if (value instanceof Component) {
Component c = (Component) value;
handleProperties(c.getPropertyIterator(), map);
} else {
handleReturnUtilDateInsteadOfTimestamp(prop, map);
}
}
private static void handleReturnUtilDateInsteadOfTimestamp(Property prop, Map map) {
if ("true".equals(map.get("hibernate.return.date.instead.of.timestamp"))) {
Value value = prop.getValue();
if (value instanceof SimpleValue) {
SimpleValue simpleValue = (SimpleValue) value;
String typeName = simpleValue.getTypeName();
if ("java.util.Date".equals(typeName)) {
UtilDateSimpleValue udsv = new UtilDateSimpleValue(simpleValue);
prop.setValue(udsv);
}
}
}
}
}
As you can see I just iterate over every property and substitute SimpleValue-object for UtilDateSimpleValue for properties of type java.util.Date. This is very simple class - it implements the same interface as SimpleValue object, e.g org.hibernate.mapping.KeyValue. In constructor original SimpleValue object is passed - so every call to UtilDateSimpleValue is redirected to the original object with one exception - method getType(...) return my custom Type.
public class UtilDateSimpleValue implements KeyValue{
private SimpleValue value;
public UtilDateSimpleValue(SimpleValue value) {
this.value = value;
}
public SimpleValue getValue() {
return value;
}
#Override
public int getColumnSpan() {
return value.getColumnSpan();
}
...
#Override
public Type getType() throws MappingException {
final String typeName = value.getTypeName();
if (typeName == null) {
throw new MappingException("No type name");
}
Type result = new UtilDateUserType();
return result;
}
...
}
And the last step is implementation of UtilDateUserType. I just extend original org.hibernate.type.TimestampType and override its method get() like this:
public class UtilDateUserType extends TimestampType{
#Override
public Object get(ResultSet rs, String name) throws SQLException {
Timestamp ts = rs.getTimestamp(name);
Date result = null;
if(ts != null){
result = new Date(ts.getTime());
}
return result;
}
}
That is all. A little bit tricky, but now every java.util.Date property is returned as java.util.Date without any additional modifications of existing code (annotations or modifying setters). As I find out in Hibernate 4 or above there is a much more easier way to substitute your own type (see details here: Hibernate TypeResolver). Any suggestions or criticism are welcome.
A simple alternative to using a custom UserType is to construct a new java.util.Date in the setter for the date property in your persisted bean, eg:
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.Column;
#Entity
public class Purchase {
private Date date;
#Column
public Date getDate() {
return this.date;
}
public void setDate(Date date) {
// force java.sql.Timestamp to be set as a java.util.Date
this.date = new Date(date.getTime());
}
}
Approaches 1 and 2 obviously don't work, because you get java.sql.Date objects, per JPA/Hibernate spec, and not java.util.Date. From approaches 3 and 4, I would rather choose the latter one, because it's more declarative, and will work with both field and getter annotations.
You have already laid out the solution 4 in your referenced blog post, as #tscho was kind to point out. Maybe defaultForType (see below) should give you the centralized solution you were looking for. Of course will will still need to differentiate between date (without time) and timestamp fields.
For future reference I will leave the summary of using your own Hibernate UserType here:
To make Hibernate give you java.util.Date instances, you can use the #Type and #TypeDef annotations to define a different mapping of your java.util.Date java types to and from the database.
See the examples in the core reference manual here.
Implement a UserType to do the actual plumbing (conversion to/from java.util.Date), named e.g. TimestampAsJavaUtilDateType
Add a #TypeDef annotation on one entity or in a package-info.java - both will be available globally for the session factory (see manual link above). You can use defaultForType to apply the type conversion on all mapped fields of type java.util.Date.
#TypeDef
name = "timestampAsJavaUtilDate",
defaultForType = java.util.Date.class, /* applied globally */
typeClass = TimestampAsJavaUtilDateType.class
)
Optionally, instead of defaultForType, you can annotate your fields/getters with #Type individually:
#Entity
public class MyEntity {
[...]
#Type(type="timestampAsJavaUtilDate")
private java.util.Date myDate;
[...]
}
P.S. To suggest a totally different approach: we usually just don't compare Date objects using equals() anyway. Instead we use a utility class with methods to compare e.g. only the calendar date of two Date instances (or another resolution such as seconds), regardless of the exact implementation type. That as worked well for us.
Here is solution for Hibernate 4.3.7.Final.
pacakge-info.java contains
#TypeDefs(
{
#TypeDef(
name = "javaUtilDateType",
defaultForType = java.util.Date.class,
typeClass = JavaUtilDateType.class
)
})
package some.pack;
import org.hibernate.annotations.TypeDef;
import org.hibernate.annotations.TypeDefs;
And JavaUtilDateType:
package some.other.or.same.pack;
import java.sql.Timestamp;
import java.util.Comparator;
import java.util.Date;
import org.hibernate.HibernateException;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.type.AbstractSingleColumnStandardBasicType;
import org.hibernate.type.LiteralType;
import org.hibernate.type.StringType;
import org.hibernate.type.TimestampType;
import org.hibernate.type.VersionType;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.JdbcTimestampTypeDescriptor;
import org.hibernate.type.descriptor.sql.TimestampTypeDescriptor;
/**
* Note: Depends on hibernate implementation details hibernate-core-4.3.7.Final.
*
* #see
* <a href="http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch06.html#types-custom">Hibernate
* Documentation</a>
* #see TimestampType
*/
public class JavaUtilDateType
extends AbstractSingleColumnStandardBasicType<Date>
implements VersionType<Date>, LiteralType<Date> {
public static final TimestampType INSTANCE = new TimestampType();
public JavaUtilDateType() {
super(
TimestampTypeDescriptor.INSTANCE,
new JdbcTimestampTypeDescriptor() {
#Override
public Date fromString(String string) {
return new Date(super.fromString(string).getTime());
}
#Override
public <X> Date wrap(X value, WrapperOptions options) {
return new Date(super.wrap(value, options).getTime());
}
}
);
}
#Override
public String getName() {
return "timestamp";
}
#Override
public String[] getRegistrationKeys() {
return new String[]{getName(), Timestamp.class.getName(), java.util.Date.class.getName()};
}
#Override
public Date next(Date current, SessionImplementor session) {
return seed(session);
}
#Override
public Date seed(SessionImplementor session) {
return new Timestamp(System.currentTimeMillis());
}
#Override
public Comparator<Date> getComparator() {
return getJavaTypeDescriptor().getComparator();
}
#Override
public String objectToSQLString(Date value, Dialect dialect) throws Exception {
final Timestamp ts = Timestamp.class.isInstance(value)
? (Timestamp) value
: new Timestamp(value.getTime());
// TODO : use JDBC date literal escape syntax? -> {d 'date-string'} in yyyy-mm-dd hh:mm:ss[.f...] format
return StringType.INSTANCE.objectToSQLString(ts.toString(), dialect);
}
#Override
public Date fromStringValue(String xml) throws HibernateException {
return fromString(xml);
}
}
This solution mostly relies on TimestampType implementation with adding additional behaviour through anonymous class of type JdbcTimestampTypeDescriptor.
There are some classes in the Java platform libraries that do extend an instantiable
class and add a value component. For example, java.sql.Timestamp
extends java.util.Date and adds a nanoseconds field. The equals implementation
for Timestamp does violate symmetry and can cause erratic behavior if
Timestamp and Date objects are used in the same collection or are otherwise intermixed.
The Timestamp class has a disclaimer cautioning programmers against
mixing dates and timestamps. While you won’t get into trouble as long as you
keep them separate, there’s nothing to prevent you from mixing them, and the
resulting errors can be hard to debug. This behavior of the Timestamp class was a
mistake and should not be emulated.
check out this link
http://blogs.sourceallies.com/2012/02/hibernate-date-vs-timestamp/
Just add the this annotation #Temporal(TemporalType.DATE) for a java.util.Date field in your entity class.
More information available in this stackoverflow answer.
I ran into a problem with this as well as my JUnit assertEquals were failing comparing Dates to Hibernate emitted 'java.util.Date' types (which as described in the question are really Timestamps). It turns out that by changing the mapping to 'date' rather than 'java.util.Date' Hibernate generates java.util.Date members. I am using an XML mapping file with Hibernate version 4.1.12.
This version emits 'java.util.Timestamp':
<property name="date" column="DAY" type="java.util.Date" unique-key="KONSTRAINT_DATE_IDX" unique="false" not-null="true" />
This version emits 'java.util.Date':
<property name="date" column="DAY" type="date" unique-key="KONSTRAINT_DATE_IDX" unique="false" not-null="true" />
Note, however, if Hibernate is used to generate the DDL, then these will generate different SQL types (Date for 'date' and Timestamp for 'java.util.Date').
Use #Type annotation on Hibernate entity Filed to customize your mapping with DB object and java object
Visit: https://www.baeldung.com/hibernate-custom-types
I have a field, say, user_name, that should be unique in a table.
What is the best way for validating it using Spring/Hibernate validation?
One of the possible solutions is to create custom #UniqueKey constraint (and corresponding validator); and to look-up the existing records in database, provide an instance of EntityManager (or Hibernate Session)to UniqueKeyValidator.
EntityManagerAwareValidator
public interface EntityManagerAwareValidator {
void setEntityManager(EntityManager entityManager);
}
ConstraintValidatorFactoryImpl
public class ConstraintValidatorFactoryImpl implements ConstraintValidatorFactory {
private EntityManagerFactory entityManagerFactory;
public ConstraintValidatorFactoryImpl(EntityManagerFactory entityManagerFactory) {
this.entityManagerFactory = entityManagerFactory;
}
#Override
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
T instance = null;
try {
instance = key.newInstance();
} catch (Exception e) {
// could not instantiate class
e.printStackTrace();
}
if(EntityManagerAwareValidator.class.isAssignableFrom(key)) {
EntityManagerAwareValidator validator = (EntityManagerAwareValidator) instance;
validator.setEntityManager(entityManagerFactory.createEntityManager());
}
return instance;
}
}
UniqueKey
#Constraint(validatedBy={UniqueKeyValidator.class})
#Target({ElementType.TYPE})
#Retention(RUNTIME)
public #interface UniqueKey {
String[] columnNames();
String message() default "{UniqueKey.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
#Target({ ElementType.TYPE })
#Retention(RUNTIME)
#Documented
#interface List {
UniqueKey[] value();
}
}
UniqueKeyValidator
public class UniqueKeyValidator implements ConstraintValidator<UniqueKey, Serializable>, EntityManagerAwareValidator {
private EntityManager entityManager;
#Override
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
}
private String[] columnNames;
#Override
public void initialize(UniqueKey constraintAnnotation) {
this.columnNames = constraintAnnotation.columnNames();
}
#Override
public boolean isValid(Serializable target, ConstraintValidatorContext context) {
Class<?> entityClass = target.getClass();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();
Root<?> root = criteriaQuery.from(entityClass);
List<Predicate> predicates = new ArrayList<Predicate> (columnNames.length);
try {
for(int i=0; i<columnNames.length; i++) {
String propertyName = columnNames[i];
PropertyDescriptor desc = new PropertyDescriptor(propertyName, entityClass);
Method readMethod = desc.getReadMethod();
Object propertyValue = readMethod.invoke(target);
Predicate predicate = criteriaBuilder.equal(root.get(propertyName), propertyValue);
predicates.add(predicate);
}
} catch (Exception e) {
e.printStackTrace();
}
criteriaQuery.where(predicates.toArray(new Predicate[predicates.size()]));
TypedQuery<Object> typedQuery = entityManager.createQuery(criteriaQuery);
List<Object> resultSet = typedQuery.getResultList();
return resultSet.size() == 0;
}
}
Usage
#UniqueKey(columnNames={"userName"})
// #UniqueKey(columnNames={"userName", "emailId"}) // composite unique key
//#UniqueKey.List(value = {#UniqueKey(columnNames = { "userName" }), #UniqueKey(columnNames = { "emailId" })}) // more than one unique keys
public class User implements Serializable {
private String userName;
private String password;
private String emailId;
protected User() {
super();
}
public User(String userName) {
this.userName = userName;
}
....
}
Test
public void uniqueKey() {
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("default");
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
ValidatorContext validatorContext = validatorFactory.usingContext();
validatorContext.constraintValidatorFactory(new ConstraintValidatorFactoryImpl(entityManagerFactory));
Validator validator = validatorContext.getValidator();
EntityManager em = entityManagerFactory.createEntityManager();
User se = new User("abc", poizon);
Set<ConstraintViolation<User>> violations = validator.validate(se);
System.out.println("Size:- " + violations.size());
em.getTransaction().begin();
em.persist(se);
em.getTransaction().commit();
User se1 = new User("abc");
violations = validator.validate(se1);
System.out.println("Size:- " + violations.size());
}
I think it is not wise to use Hibernate Validator (JSR 303) for this purpose.
Or better it is not the goal of Hibernate Validator.
The JSR 303 is about bean validation. This means to check if a field is set correct. But what you want is in a much wider scope than a single bean. It is somehow in a global scope (regarding all Beans of this type). -- I think you should let the database handle this problem. Set a unique constraint to the column in your database (for example by annotate the field with #Column(unique=true)) and the database will make sure that the field is unique.
Anyway, if you really want to use JSR303 for this, than you need to create your own Annotation and own Validator. The Validator have to access the Database and check if there is no other entity with the specified value. - But I believe there would be some problems to access the database from the Validator in the right session.
One possibility is to annotate the field as #NaturalId
You could use the #Column attribute which can be set as unique.
I've found kind of a tricky solution.
First, I've implemented the unique contraint to my MySql database :
CREATE TABLE XMLTAG
(
ID INTEGER NOT NULL AUTO_INCREMENT,
LABEL VARCHAR(64) NOT NULL,
XPATH VARCHAR(128),
PRIMARY KEY (ID),
UNIQUE UQ_XMLTAG_LABEL(LABEL)
) ;
You see that I manage XML Tags that are defined by a unique label and a text field named "XPath".
Anyway, the second step is to simply catch the error raised when the user tries to do a bad update. A bad update is when trying to replace the current label by an existing label. If you leave the label untouched, no problemo. So, in my controller :
#RequestMapping(value = "/updatetag", method = RequestMethod.POST)
public String updateTag(
#ModelAttribute("tag") Tag tag,
#Valid Tag validTag,
BindingResult result,
ModelMap map) {
if(result.hasErrors()) { // you don't care : validation of other
return "editTag"; // constraints like #NotEmpty
}
try {
tagService.updateTag(tag); // try to update
return "redirect:/tags"; // <- if it works
}
catch (DataIntegrityViolationException ex) { // if it doesn't work
result.rejectValue("label", "Unique.tag.label"); // pass an error message to the view
return "editTag"; // same treatment as other validation errors
}
}
This may conflict with the #Unique pattern but you can use this dirty method to valid the adding too.
Note : there is still one problem : if other validation errors are catched before the exception, the message about unicity will not be displayed.
This code is based on the previous one implemented using EntityManager.
In case anyone need to use Hibernate Session.
Custom Annotation using Hibernate Session.
#UniqueKey.java
import java.lang.annotation.*;
import javax.validation.Constraint;
import javax.validation.Payload;
#Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = UniqueKeyValidator.class)
#Documented
public #interface UniqueKey {
String columnName();
Class<?> className();
String message() default "{UniqueKey.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
UnqieKeyValidator.java
import ch.qos.logback.classic.gaffer.PropertyUtil;
import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
#Transactional
#Repository
public class UniqueKeyValidator implements ConstraintValidator<UniqueKey, String> {
#Autowired
private SessionFactory sessionFactory;
public Session getSession() {
return sessionFactory.getCurrentSession();
}
private String columnName;
private Class<?> entityClass;
#Override
public void initialize(UniqueKey constraintAnnotation) {
this.columnNames = constraintAnnotation.columnNames();
this.entityClass = constraintAnnotation.className();
}
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
Class<?> entityClass = this.entityClass;
System.out.println("class: " + entityClass.toString());
Criteria criteria = getSession().createCriteria(entityClass);
try {
criteria.add(Restrictions.eq(this.columnName, value));
} catch (Exception e) {
e.printStackTrace();
}
return criteria.list().size()==0;
}
}
Usage
#UniqueKey(columnNames="userName", className = UserEntity.class)
// #UniqueKey(columnNames="userName") // unique key