跳至主要内容

Spring Data JPA


Spring Data JPA

Spring Data JPA(Maven archetype id is spring-data-jpa) provides an extra JpaRepository interface which is extended fromPagingAndSortingRepository. This module also provide QueryDSL integration.

Configuration

The configuration is very similar with the one motioned in before JPA post, just declare a DataSource bean, a EntityManagerFactroyBean, and a JpaTransactionManager, etc in your Spring configuration.
An extra step, you have to specify the package of the repositories in your configuration.
<jpa:repositories base-package="com.hantsylabs.example.spring.jpa"></jpa:repositories>
or use annotation @EableJpaRepositories to activate the JPA repository support.

JpaRepository

I will reuse the Conference entity as example to demonstrate the usage of JpaRepository API.
@Entity
@NamedQuery(name = "Conference.searchByMyNamedQuery", query = "from Conference where name=?")
public class Conference {
 @Id
 @GeneratedValue(strategy = GenerationType.AUTO)
 @Column(name = "id")
 private Long id;

 @Version
 @Column(name = "version")
 private Integer version;

 @NotNull
 private String name;

 @NotNull
 private String description;

 @NotNull
 @Temporal(TemporalType.TIMESTAMP)
 @DateTimeFormat(style = "M-")
 private Date startedDate;

 @NotNull
 @Temporal(TemporalType.TIMESTAMP)
 @DateTimeFormat(style = "M-")
 private Date endedDate;

 @NotNull
 private String slug;
 
 private Address address;
 
 @OneToMany(cascade = CascadeType.ALL, mappedBy = "conference")
 private Set<Signup> signups = new HashSet<Signup>();

 //getters and setters
}
An address is added and an one-to-many relation is included.
Address is a Embeddable class.
@Embeddable
public class Address {

 private String addressLine1;
 
 private String addressLine2;
 
 private String zipCode;
 
 private String city;
 
 private String country;

 //getters and setters
}
Signup is a standalone @Entity class.
@Entity
public class Signup {

 @Id
 @GeneratedValue(strategy = GenerationType.AUTO)
 @Column(name = "id")
 private Long id;

 @Version
 private Integer version;

 @NotNull
 private String firstName;

 @NotNull
 private String lastName;

 @NotNull
 @Email
 private String email;

 @NotNull
 private String phone;

 private String occupation;

 @Size(max = 2000)
 private String company;

 private String comment;

 @DateTimeFormat(style = "M-")
 private Date createdDate;

 @ManyToOne()
 private Conference conference;

 //getters and setters.
}
JpaRepository is a JPA specific Repository API which is extended thePagingAndSortingRepository from Spring Data Commons.
You can use the methods defined in the super interfaces(CrudRepository and PagingAndSortingRepository) freely.JpaRepository aslo provides some extra methods for JPA operations. Please review the JpaRepository class for more details.
@Repository
public interface ConferenceRepository extends JpaRepository<Conference, Long>{
 
}
You can declare a ConferenceRepository like this. The @Repositoryannotation on the class is not a must, because Spring Data recognizes the repositories by determining if they are extending theRepository interface.
For example, in order to save a Conference.
Firstly inject ConferenceRepository,
@Autowired
ConferenceRepository conferenceRepository
Then call save method directly.
Conference conference = newConference();
conference.setSlug("test-jud");
conference.setName("Test JUD");
conference.getAddress().setCountry("US");
conference = conferenceRepository.save(conference);
All query method based on naming convention motioned in before post also can be used here.
For example,
public Conference findBySlug(String slug);
It means it will query Conference by the slug property, and return an unique Conference object. The result is no difference from the following custom codes, but you do not need write one line code of the detailed implementation, Spring Date severs for you.
public Conference findBySlug(String slug){
 return em.createQuery("from Conference where slug=:slug").setParameter("slug", slug).getSingleResult();
}
You can combine multi properties for complex query, such asfindByNameAndSlug(String name, String Slug). More info about the rules, please read the official Spring Data JPA documentation.
It is not the all, Spring Data JPA provides more.

Query

In some cases, maybe the convention based method can not satisfy your requirement. Using Spring Data JPA, you have several ways to build complex query.
Spring Data JPA provides a @Query annotation(in packageorg.springframework.data.jpa.repository) to execute custom JPQL, simply add it on the method.
For example,
@Query("from Conference where name=?")
public Conference searchByConferenceName(String name);
It also supports named parameter, you must add an extra @Param to specify the parameter name to the method argument.
@Query("from Conference where name=:name")
public Conference searchByNamedConferenceName(@Param("name") String name);
If the method declaration is annotated with @Query but without any attributes, or without any annotation, for example.
@Query
public Conference searchByMyNamedQuery(String name);
And the method name also does not follow the convention motioned before, it will search if there is a @NamedQuery defined on theConference class, the name attribute value should be in form of<Entity ClassName>.<queryMethodName>.
@NamedQuery(name = "Conference.searchByMyNamedQuery", query = "from Conference where name=?")
Example codes of using these APIs.
confs = conferenceRepository.searchByConferenceName("Test JUD");

confs = conferenceRepository.searchByNamedConferenceName("Test JUD");

confs = conferenceRepository.searchByMyNamedQuery("Test JUD");

confs = conferenceRepository.searchByDescription("Boston");

confs = conferenceRepository.findByDescriptionLike("%Boston%");

Modifying Query

You can execute a modifying query(such as perform a batch deletion or update) on the method by adding an extra annotation @Modifying.
For example, to update the description of an specific Conferenceidentified by id.
@Query("update Conference conf set conf.description=?1 where conf.id=?2 ")
@Modifying
public void modifyConferenceDescrition(String description, Long id);

Custom JPQL Query

You can also use the traditional way(I means use JPA without Spring Data JPA) to execute a custom JPQL in your implementation class.
  1. Define your own interface.
 public interface ConferenceRepositoryCustom {
 List<Conference> searchByDescription(String like);
 }
 
  1. Extend the ConferenceRepositoryCustom interface.
 @Repository
 public interface ConferenceRepository extends ConferenceRepositoryCustom, 
  JpaRepository<Conference, Long>{
 }
 
  1. Provide an implementation class ofConferenceRepositoryCustom.
 public class ConferenceRepositoryImpl implements ConferenceRepositoryCustom {

 @PersistenceContext
 EntityManager em;

 @Override
 public List searchByDescription(String d) {
  return em.createQuery("from Conference where description like :description", Conference.class)
  .setParameter("description", "%"+d+"%")
  .getResultList();
 }

 }
 
The codes seem a little wired, Spring Data JPA must find some way to combine the official interface and you home use interface, and expose it to the caller.
Now you can inject ConferenceRepository and use both methods of them.
@Autowired
ConferenceRepository conferenceRepository;

//save ...from standard JpaRepository
conferenceRepository.save(...);

//searchByDescription... from your ConferenceRepositoryCustom
conferenceRepository.searchByDescription(..);
By default, the Custom postfix in your custom interface is a must, but you can change it to any characters as you expected. In the<jpa:repositories, specify a repository-impl-postfix attribute.
<jpa:repositories base-package="com.hantsylabs.example.spring.jpa" repository-impl-postfix="MyCust"></jpa:repositories>
Now, you can name your custom interface asConferenceRepositoryMyCust.

Specification and Criteria API

Spring Data JPA provides a simple Specification interface to envelope the usage of the JPA Criteria API which is introduced in JPA 2.0.
  1. Modify ConferenceRepository and add another interfaceJpaSpecificationExecutor to extend.
 @Repository
 public interface ConferenceRepository extends
 JpaRepository,
 JpaSpecificationExecutor{
 }
 
JpaSpecificationExecutor provides some methods which can accept aSpecification as method argument.
  1. Write your own Specification class.
Open the Specification class, you will find it just includes one methodtoPredicate, which returns a WHERE clause in form of Predicate for given Root and CriteriaQuery.
 Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb);
 
For example, we want to find all upcoming references, we can create a Specification for it.
 public class UpcomingConferences implements Specification<Conference> {

 @Override
 public Predicate toPredicate(Root<Conference> root, CriteriaQuery<?> query,
   CriteriaBuilder cb) {
  return cb.greaterThan(root.get("startedDate").as(Date.class),
    cb.currentTimestamp());
 }
 }
 
  1. Now you can call findAll method from JpaSpecificationExecutor in your Service or Controller directly.
 @Override
 public List findUpcomingConferences() {
 return conferenceRepository.findAll(new UpcomingConferences());
 }
 
As you see, the where clause logic of query is moved to a certainSpecification class, the codes becomes more maintainable than before.

Typesafe Criteria API

JPA 2.0 provides new Criteria API, and also provides a type safe MetaModel to access the entity properties.
You have to use the compiler APT processor to generate the MetaModel classes at compile time. Almost all JPA providers(EclipseLink, Hibernate, OpenJPA) provide similar tools for generating the metamodel classes.
For Hibernate, it is provided in hibernate-jpamodelgen.
You can simply add theorg.hibernate.jpamodelgen.JPAMetaModelEntityProcessor in the APT configuration of the maven compiler plugin.
Or use a standalone plugin to configure it to get more flexibility.
<plugin>
 <groupId>org.bsc.maven</groupId>
 <artifactId>maven-processor-plugin</artifactId>
 <version>2.0.5</version>
 <executions>
  <execution>
   <id>process</id>
   <goals>
    <goal>process</goal>
   </goals>
   <phase>generate-sources</phase>
   <configuration>
    <processors>
     <processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>
    </processors>
    <outputDirectory>${project.build.directory}/generated-sources/java/</outputDirectory>
   </configuration>
  </execution>
 </executions>
 <dependencies>
  <dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-jpamodelgen</artifactId>
   <version>1.2.0.Final</version>
  </dependency>
 </dependencies>
</plugin>
Run mvn compile, the MetaModel classes will be generated under the folder generated-sources/java.
The metamodel class of Conference is named Conference_.
@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Conference.class)
public abstract class Conference_ {

 public static volatile SingularAttribute<Conference, Long> id;
 public static volatile SingularAttribute<Conference, Date> endedDate;
 public static volatile SetAttribute<Conference, Signup> signups;
 public static volatile SingularAttribute<Conference, Address> address;
 public static volatile SingularAttribute<Conference, Date> startedDate;
 public static volatile SingularAttribute<Conference, String> description;
 public static volatile SingularAttribute<Conference, String> name;
 public static volatile SingularAttribute<Conference, String> slug;
 public static volatile SingularAttribute<Conference, Integer> version;

}
Now you can replace the hard code "startedDate" in beforeUpcomingConferences with the typesafe variant.
@Override
public Predicate toPredicate(Root<Conference> root,
 CriteriaQuery<?> query,
 CriteriaBuilder cb) {
 return cb.greaterThan(root.get(Conference_startedDate).as(Date.class),
    cb.currentTimestamp());
}

QueryDSL integration

QueryDSL provides a series of fluent APIs for JPA, JDBC, Mongo, Lucence etc. The usage of integration steps is very similar with JPA Criteria API.
  1. Spring Data JPA provides a QueryDSL specificQueryDslPredicateExecutor for QueryDSL integration, makeConferenceRepository extend it.
 @Repository
 public interface ConferenceRepository extends ConferenceRepositoryCustom, 
 JpaRepository<Conference, Long>,
 JpaSpecificationExecutor<Conference>, 
 QueryDslPredicateExecutor<Conference>  {
 
QueryDslPredicateExecutor provides some methods which can accept a QueryDSL specific Predicate object as argument.
  1. You can build the Predicate via the Metamodels which can generated by QueryDSL APT tools.
Declare a the QueryDSL official maven plugin in your pom.xml.
 <plugin>
 <groupId>com.mysema.maven</groupId>
 <artifactId>apt-maven-plugin</artifactId>
 <version>1.0.9</version>
 <executions>
  <execution>
   <goals>
    <goal>process</goal>
   </goals>
   <configuration>
    <outputDirectory>target/generated-sources/java</outputDirectory>
    <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
   </configuration>
  </execution>
 </executions>
 <dependencies>
  <dependency>
   <groupId>com.mysema.querydsl</groupId>
   <artifactId>querydsl-apt</artifactId>
   <version>${querydsl.version}</version>
  </dependency>
  <dependency>
   <groupId>com.mysema.querydsl</groupId>
   <artifactId>querydsl-jpa</artifactId>
   <classifier>apt</classifier>
   <version>${querydsl.version}</version>
  </dependency>
 </dependencies>
 </plugin>
 
Run mvn compile to compile the project, it will generate the metamodel codes.
 @Generated("com.mysema.query.codegen.EntitySerializer")
 public class QConference extends EntityPathBase<Conference> {

    private static final long serialVersionUID = -226677720;

    private static final PathInits INITS = PathInits.DIRECT;

    public static final QConference conference = new QConference("conference");

    public final QAddress address;

    public final StringPath description = createString("description");

    public final DateTimePath<java.util.Date> endedDate = createDateTime("endedDate", java.util.Date.class);

    public final NumberPath<Long> id = createNumber("id", Long.class);

    public final StringPath name = createString("name");

    public final SetPath<Signup, QSignup> signups = this.<Signup, QSignup>createSet("signups", Signup.class, QSignup.class, PathInits.DIRECT);

    public final StringPath slug = createString("slug");

    public final DateTimePath<java.util.Date> startedDate = createDateTime("startedDate", java.util.Date.class);

    public final NumberPath<Integer> version = createNumber("version", Integer.class);

    public QConference(String variable) {
        this(Conference.class, forVariable(variable), INITS);
    }

    @SuppressWarnings("all")
    public QConference(Path<? extends Conference> path) {
        this((Class)path.getType(), path.getMetadata(), path.getMetadata().isRoot() ? INITS : PathInits.DEFAULT);
    }

    public QConference(PathMetadata<?> metadata) {
        this(metadata, metadata.isRoot() ? INITS : PathInits.DEFAULT);
    }

    public QConference(PathMetadata<?> metadata, PathInits inits) {
        this(Conference.class, metadata, inits);
    }

    public QConference(Class<? extends Conference> type, PathMetadata<?> metadata, PathInits inits) {
        super(type, metadata, inits);
        this.address = inits.isInitialized("address") ? new QAddress(forProperty("address")) : null;
    }

 }
 
  1. Now you can use this class to build your Predicate.
For example, query all in progress conferences.
 public static Predicate inProgressConferences() {
  QConference conf = QConference.conference;
  final Date now = new Date();
  BooleanBuilder builder = new BooleanBuilder();
  return builder.and(conf.
                         startedDate.before(now))
     .and(conf.endedDate.after(now))    
                .getValue();
 
 }
 
Call findAll(Predicate) method provided in QueryDslPredicateExecutorto get the result.
 conferenceRepository.findAll(inProgressConferences());
 
Some other query examples.
 List<Conference> confs = (List<Conference>) conferenceRepository
    .findAll(QConference.conference.address.country.eq("US"));

 confs = (List<Conference>) conferenceRepository
    .findAll(QConference.conference.name.eq("Test JUD"));

 confs = (List<Conference>) conferenceRepository
    .findAll(QConference.conference.description.contains("Boston"));

 confs = (List<Conference>) conferenceRepository
    .findAll(QConference.conference.signups.any().email
      .eq("test@test.com"));
 
As you see, compare to Specification and JPA Criteria API, QueryDSLPredicate is more friendly and more close to the nature language.

Summary

Personally, I think the QueryDSL integration is the most attractive feature in Spring Data JPA, it provides true typesafe, fluent APIs for JPA programming.

评论

此博客中的热门博文

AngularJS CakePHP Sample codes

Introduction This sample is a Blog application which has the same features with the official CakePHP Blog tutorial, the difference is AngularJS was used as frontend solution, and CakePHP was only use for building backend RESR API. Technologies AngularJS   is a popular JS framework in these days, brought by Google. In this example application, AngularJS and Bootstrap are used to implement the frontend pages. CakePHP   is one of the most popular PHP frameworks in the world. CakePHP is used as the backend REST API producer. MySQL   is used as the database in this sample application. A PHP runtime environment is also required, I was using   WAMP   under Windows system. Post links I assume you have some experience of PHP and CakePHP before, and know well about Apache server. Else you could read the official PHP introduction( php.net ) and browse the official CakePHP Blog tutorial to have basic knowledge about CakePHP. In these posts, I tried to follow the steps describ

JPA 2.1: Attribute Converter

JPA 2.1: Attribute Converter If you are using Hibernate, and want a customized type is supported in your Entity class, you could have to write a custom Hibernate Type. JPA 2.1 brings a new feature named attribute converter, which can help you convert your custom class type to JPA supported type. Create an Entity Reuse the   Post   entity class as example. @Entity @Table(name="POSTS") public class Post implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name="ID") private Long id; @Column(name="TITLE") private String title; @Column(name="BODY") private String body; @Temporal(javax.persistence.TemporalType.DATE) @Column(name="CREATED") private Date created; @Column(name="TAGS") private List<String> tags=new ArrayList<>(); } Create an attribute convert

Auditing with Hibernate Envers

Auditing with Hibernate Envers The approaches provided in JPA lifecyle hook and Spring Data auditing only track the creation and last modification info of an Entity, but all the modification history are not tracked. Hibernate Envers fills the blank table. Since Hibernate 3.5, Envers is part of Hibernate core project. Configuration Configure Hibernate Envers in your project is very simple, just need to add   hibernate-envers   as project dependency. <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-envers</artifactId> </dependency> Done. No need extra Event listeners configuration as the early version. Basic Usage Hibernate Envers provides a simple   @Audited   annotation, you can place it on an Entity class or property of an Entity. @Audited private String description; If   @Audited   annotation is placed on a property, this property can be tracked. @Entity @Audited public class Signup implements Serializa