尝试Spring Data Jpa--告别CRUD

  前言    

  说到我们的web开发架构分层中,持久层是相对底层也是相对稳定的一层,奠定好根基后,我们才能专注于业务逻辑和视图开发。而自从ORM思想蔓延开来后,全自动ORM的Hibernate和半自动ORM的MyBatis几乎垄断了持久层(当然还有很多公司或者大牛自己封装的框架,不过相对占小部分),是发展过程中比较主流的两款持久层框架。前段时间也关注了很多有关领域驱动设计的内容,感觉对前面的传统架构分层冲击较大,尤其是业务逻辑层、持久层、实体ORM那块,引入了许多新概练,一时间也遇到了很多困惑,网上搜索资料发现领域驱动其实由来已久,目前也应用很多,但是想要完全掌握,并不是一件容易事。当然本文跟领域驱动并无直接关联,现在的问题是在面试题“Hibernate和MyBatis的区别”背景下,我们在持久层还有第三种典型选择吗,其实是有的,那就是本文的主角,Spring Data Jpa。  

    介绍

  说起Jpa,其实它并不是一个新概念,更不是说有了Spring Data Jpa才出现,它是Java Persistence API的简称,中文名Java持久层API,它是一种规范,例如Hibernate框架即实现了这种规范,Spring Data Jpa中便集成了Hibernate的模块。Spring Data,看名字很容易知道又是Spring系列的,除了Spring MVC在web层的成功,在持久层这块Spring也想拿下,大有想一统江湖的势头。另外去深入关注Spring Data内容,还会发现,不仅仅是RDBMS,还有Spring Data Redis、Spring Data Mongodb等等...本文内容主要是针对关系型数据库,本人在使用过程中,最看好的还是其在通用持久化方面的简易封装,基于层层的泛型继承,我们可以省略大量的简单增删改查方法的编码,另外提供的通过特定格式方法命名简化方法定义过程也很特别和好用。下面就基于Spring Data编写一个单独的简单持久层实例来展现其使用过程。    

  准备环境

  Eclipse + MySql + Maven

  基于传统几大框架的开发目前已经相对成熟很多了,但是就实际工作开发环境中,笔者最强烈的感受有一点,配置!配置文件实在太多了!特别是多工程组合集成的时候,漫天飞的XML和properties真是让人头大。所以建议现在学习过程中一定要尽量搞懂配置中每段配置语句的含义,哪个参数有什么作用,这样进入实际开发中才不会一时间无章可循。本文中配置尽量给出注释来阐述含义。

  在eclipse新建一个普通maven项目,quickstart类型,项目结构大致如下

  pom.xml依赖如下

<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    
    <!-- Spring 系列 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.2.3.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-orm</artifactId>
        <version>4.2.3.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-jpa</artifactId>
        <version>1.9.1.RELEASE</version>
    </dependency>
 
    <!-- Hibernate系列 -->
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>4.3.11.Final</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>4.3.11.Final</version>
    </dependency>
 
    <!-- MySQL -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.37</version>
    </dependency>
    <dependency>
        <groupId>commons-dbcp</groupId>
        <artifactId>commons-dbcp</artifactId>
        <version>1.4</version>
    </dependency>
  </dependencies>

  Spring Data Jpa 告别CRUD

  第一步、配置文件(当然实际开发中,我们不会将配置这样集中在一个文件中,同时数据源配置等关键参数往往会通过properties文件去设置然后引入)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"  
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
       http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd 
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
     
     <!-- Spring的bean扫描机制,会根据bean注解例如@Service等去实例化相应bean -->
     <context:component-scan base-package="com.sdj"></context:component-scan>
     
     <!-- 这句代码是告诉jpa我们的持久层接口都在哪些包下面 -->
    <jpa:repositories base-package="com.sdj.repository"/>
     
     <!-- 这里使用dbcp配置数据源,能实现连接池功能 -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://192.168.0.100:3306/sdj"/>
        <property name="username" value="root"/>
        <property name="password" value="abc123"/>
    </bean>
     
     <!-- 实体管理器工厂配置,关联数据源,指定实体类所在包等等 -->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="packagesToScan" value="com.sdj.domain"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="database" value="MYSQL"/>
                <property name="generateDdl" value="false"/>
                <property name="showSql" value="true"/>
            </bean>
        </property>
        <property name="jpaProperties">
            <props>
            <prop key="hibernate.hbm2ddl.auto">none</prop> <!-- 如果想要自动生成数据表,这里的配置是关键 -->
            <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop> 
            <!-- <prop key="hibernate.dialect">org.hibernate.dialect.OracleDialect</prop> -->
            <prop key="hibernate.connection.charSet">UTF-8</prop>
            <prop key="hibernate.format_sql">true</prop>
            </props>
        </property>
    </bean>
 
    <!--配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    
    <!--启用事务注解来管理事务-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

  上面Spring配置文件中,实体管理器工厂配置是比较复杂的部分,下面具体到参数逐个介绍

  dataSource指定数据源

  packagesToScan,与前面的component-scan类似,这里也是一种扫描机制,不过前面是扫描bean,这里既然是实体管理器,不难理解是扫描实体类,即指定实体类所在的包,这里为com.sdj.domain

  jpaVendorAdapter,这里对应Jpa持久化实现厂商Hibernate,同时指定其专用特性,包括如下

    database,指定数据库,这里为MYSQL

    generateDdl,是否自动生成DDL,这里为false

    showSql,是否在控制台打印SQL语句,这点在调试时比较有用,能看到具体发送了哪些SQL

  jpaProperties,jpa属性设置,有如下

    hibernate.hbm2ddl.auto,根据需要可以设置为validate | update | create | create-drop,当然也可以设置为none,设置的时候要小心,使用不到会有丢失数据的危险,例如这里如果我们想要根据实体类自动生成数据表,可以设置为update,不用的话这里设置为none

    hibernate.dialect,指定数据库方言,这里为MySql数据库类型的

    hibernate.connection.charSet,指定链接字符编码,解决乱码问题

    hibernate.format_sql,前面指定控制台会打印SQL,这里是指定将其格式化打印,更加清晰美观一点

  第二步、实体类Person

package com.sdj.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="TB_PERSON")
public class Person {
    private Integer id;        //主键
    private String name;    //姓名
    private String gender;    //性别
    private String addr;    //地址
    
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    public Integer getId() {
        return id;
    }
    @Column(name="NAME")
    public String getName() {
        return name;
    }
    public String getGender() {
        return gender;
    }
    public String getAddr() {
        return addr;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setGender(String gender) {
        this.gender = gender;
    }
    public void setAddr(String addr) {
        this.addr = addr;
    }

}

  如果仔细观察实体中系列注解,会发现其来源是hibernate-jpa,这也是前面提到的hibernate实现jpa规范内容。常用注解解释如下

  @Entity,指定该类为一个数据库映射实体类、

  @Table,指定与该实体类对应的数据表

  @Id和@Column,都是为实体类属性关联数据表字段,区别是Id是对应主键字段,另外还可以指定其对应字段名(不指定默认与属性名一致)、长度等等...如果不加这两个注解也是会以属性名默认关联到数据库,如果不想关联可以加上下面的@Transient

  @GeneratedValue,指定主键生成策略

  @Transient,表名该属性并非数据库表的字段映射

  @OneToMany、@ManyToOne、@ManyToMany等,均为关联映射注解系列,用于指定关联关系,一对多、多对一等等

  另外,@Id、@Column、@Transient等注解往往是加在属性的get方法上。

  第三步、持久层接口PersonRepository

package com.sdj.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

import com.sdj.domain.Person;

public interface PersonRepository extends JpaRepository<Person, Integer> {
    List<Person> findByName(String name);
}

  我们发现这里持久层代码反而是最简洁的,我们的注意点如下:

  1.在这个针对Person实体的dao接口中我们并未定义常规通用的那些增删改查等方法,只定义了一个特定的根据姓名查找人的方法,同时继承了一个JpaRepository接口。

  2.不管继承接口也好,自定义方法也好,终究是接口,但是这里我们连实现类也没有。

  暂时先不走到业务逻辑Service层,一二三步走完,我们这个持久层程序已经可以运行了,下面我们编写测试方法来看看。

package com.test;

import java.util.List;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.sdj.domain.Person;
import com.sdj.repository.PersonRepository;

public class TestSDJ {
    
    @Test
    public void testDB() {
        @SuppressWarnings("resource")
        ApplicationContext context = new ClassPathXmlApplicationContext("application-root.xml");
        PersonRepository bean = (PersonRepository) context.getBean("personRepository");
        List<Person> list = bean.findAll();
        System.out.println(list);
    }
}

  运行测试类可以看到控制台输出如下结果

  

  首先是格式化打印出了SQL语句,可以清楚看出来是查询数据表所有记录,下面则是输出集合内容,这样一来我们成功查出了表中数据。

  疑问点如下:

  1.首先前面我们定义PersonRepository是一个接口,并且没有实现类,更没有bean注解,那么通过Spring我们为什么能拿到这样一个(接口名首字母小写)名字的bean,这个bean又具体是什么?

  2.我们的PersonRepository是一个接口,明没有这样的findAll()方法来查询表中所有记录,有人可能会很快想到其继承了CrudRepository接口,那么这个方法又是怎么实现的?

  JpaRepository这个接口是Spring Data提供的核心接口系列继承链中的一环,主要有如下四个

  Repository

public interface Repository<T, ID extends Serializable> {

}

  这是顶层接口,也是一个空接口,后面定义的泛型T对应我们的实体类,ID对应实体类中主键字段对应属性的类型,比如本文是数字主键类型Integer,这里即对应Integer。  

@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
    <S extends T> S save(S entity);

    T findOne(ID id);

    Iterable<T> findAll();

    ...
}

  CrudRepository继承Repository接口,CRUD大家应该都不陌生,增加(Create)、读取查询(Read)、更新(Update)和删除(Delete),这里即新增了增删改查等通用方法的定义。

public interface PagingAndSortingRepository<T, ID extends Serializable> extends CrudRepository<T, ID> {
      ...   
}
public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID> {
   ......    
}

  然后PagingAndSortingRepository继承CrudRepository,同时新增分页和排序等相关方法

  最后就是文中的JpaRepository继承PagingAndSortingRepository,同时定义了系列常用方法。

  不知不觉,我们可以看到JpaRepository这里已经基本涵盖了我们基础操作的相关方法集合了,例如测试类中的查找所有记录方法。但是问题没完,方法再多,终究是接口,既然是接口,你这些方法没有实现的话我们还是无法使用,但是我们在测试中已经发现成功了,那么它是怎么实现的呢。

  我们来关注一下JpaRepository的实现类SimpleJpaRepository,源代码类声明段落如下

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID extends Serializable> implements JpaRepository<T, ID>,
        JpaSpecificationExecutor<T> {

        ......
}

  可以发现,这个类中已经实现了前面四环的定义方法,终于齐集五环。
  @Repository,Spring系列bean注解之一,告诉系统这是一个持久层的bean示例;

  @NoRepositoryBean,与之相反,使用该注解标明,此接口不是一个Repository Bean,例如前面的JpaRepository等,都用上了该注解,但是我们自定义的PersonRepository并没有加,同时前面配置文件中 <jpa:repositories base-package="com.sdj.repository"/> ,随后Spring Data会自动帮我们实现该接口,并实例出bean,bean名字为接口名首字母小写后的字符串。

  下面我们在原先的测试类中加入如下代码

        ApplicationContext context = new ClassPathXmlApplicationContext("application-root.xml");
        String[] beanNames = context.getBeanDefinitionNames();
        for(String beanName:beanNames) {
            System.out.println(beanName);
        }
      ...

  重新运行测试类,我们除了能看到先前的输出信息,在前面还会看到如下输出

 

  这一行行的都是Spring容器中现有的bean示例名称,其中就有刚刚说到的"personRepository",所以我们才能根据这个名称拿到该bean示例。

  然后我们在Service层就可以注入持久层bean去组合业务逻辑操作了,通过@Autowired注入,同时不要忘记Service类上的@Service注解。

@Service
public class PersonServiceImpl implements PersonService {
    
    @Autowired
    PersonRepository personRepository;
       
        .....  
}

 

  这样一来,我们发现在常规的基础操作范围内,包括增删改查、分页查询、排序等等,我们不用编写一个方法,也不用写一条SQL语句,Spring Data Jpa都帮我们封装好了。但这只是一部分内容,例如前面接口中我们不是定义了一个findByName(),有人可能会说了,难不成这也能帮我们自动实现?就算能,那我再findByGenger()?到底能不能,这也引出了下面要说的内容。

  Query creation 让方法见名知意

  在前面的测试类查询方法改成如下:

 

List<Person> list = bean.findByName("小明");

 

  运行测试方法,控制台输出如下


  

  看SQL语句发现的确是根据name姓名去查的,也成功查到了结果。

  大家都知道增删改查,一个查字一片天,简单查也好,花样查也好,它都是我们持久层不可缺少的部分。

  除了前面提到了Spring Data对常规持久层操作的封装,它另外还提供了一种通过格式化命名创建查询的途径,使得我们在创建查询方法的时候有更多简单的实现方式和选择。

  这里的格式具体体现示例如下:

  查询方法命名都是findBy****这样的开头,后面跟字段或者字段加关键字的组合

  比如findByName等,相当于SQL:where name= ?都是规范的驼峰式命名。

  比如findByNameAndGender,相当于SQL:where name= ? and gender = ?

  这里的and就是一个keyword关键字,类似的还有许多,可以参考官方文档链接点击查看如下相关内容

  也就是说符合上述命名规范的自定义方法,Spring Data同样会帮助我们去实现这些方法,这样一来又方便了许多。

  但是如果我的命名不符合规范,我是否一定要实现这个接口并重写相关方法,这样其实也是可行的,不过Spring Data还提供了@Query方法注解,供我们自定义操作语句去实现它。例如上面的findByName方法,类似的我们在接口中新建一个任意方法名的如下方法:

@Query("from Person p where p.name = (:name)")
List<Person> abc(@Param("name")String name);

  然后在测试类中引用该方法,能实现与findByName相同的查询效果。  

  这里方法名给了个adc,同时方法上面注解定义了查询语句,用过Hibernate的HQL语句的应该比较熟悉,这不禁让人想起,当初Hibernate用的人用起来都说好啊好啊,面向对象思维啊,全自动啊,一句SQL都不用写啊,真牛逼啊!然后全是HQL.....

  Spring Data还有很多特性,如果有兴趣也可以继续深入学习一下。

  小结

  目前来看,除了主流的MyBatis、Hibernate,Spring Data Jpa也有不少公司在使用,而且Spring Boot系列中基于Spring Data的数据访问也有使用到,毕竟Spring系列。个人比较喜欢Spring Data Jpa的点在数据库通用操作的封装,以及这些便利命名方法,这使得我们在业务逻辑相对简单的情况下,能节省很多代码和时间。但是问题是我们大多时候我们要攻克去专注的往往是那些复杂的业务逻辑,而在这点上Spring Data Jpa并无明显优势,莫非这就是在知乎上搜素"Spring Data怎么样"连话题都搜不出来的原因....同时高封装会不会引发低可控,如同以前用Hibernate,它自动帮助我们发送SQL,但是简便的同时不会有像MyBatis那样看到自己写SQL的透明度来得直观,同时SQL优化等东西似乎没那么好掌控,这些应该是项目技术选型初始大致都会考虑到的一些问题吧,效率、性能、对开发人群的整体上手难度等等。总的来说,根据应用场景做出最适合项目的选择才是关键吧。

posted @ 2018-02-02 18:46  窗外天空晴朗  阅读(2455)  评论(3编辑  收藏  举报