Spring Data JPA详细使用教程

Spring Data JPA详细使用教程

简介

Spring Data提供了针对数据库(包括SQLNOSQL)的整合方案,对Hibernate JPAJedis等工具的api进行高级的封装,为我们提供简单方便地操作接口。

Spring Data JPASpring Data项目下的一个模块。整合了基于JPA的持久层框架(默认Hibernate JPA),并对API进行了封装,让我们可以更方便地操作数据库。使用过程中基本不需要编写DAO实现类,只要定义好接口就行了。

学习Spring Data JPA主要包括以下内容:

  1. entityManagerFactory、transactionManager的配置
  2. DAO接口的定义:实现Repository子接口
  3. 基于方法命名规则的查询
  4. 基于@Query注解的查询或更新:JPQL和SQL(对应Hibernate的HQL和SQL)
  5. Respository接口的继承体系
  6. CrudRepository的api使用
  7. PagingAndSortingRepository的api使用
  8. JpaSpecificationExecutor的api使用(对应Hibernate的QBC)
  9. 一对一、一对多、多对多、自关联的配置和操作
  10. 自定义Repository的使用

项目工程环境

JDK:1.8.0_201
maven:3.6.1
IDE:Spring Tool Suites4 for Eclipse:4.12
mysql:5.7
Hibernate:5.4.4.Final
c3p0:0.9.5.4

项目实现的需求

采用Spring Data JPA的API(均采用注解方式配置对象映射),针对三个实体进行增删改查操作:

  1. 用户:

  2. 角色:和用户是一对多关系

  3. 菜单:和角色是多对多关系,本身自关联

applicationContext.xml文件的配置

这个文件配置需要注意以下几点:

  1. 数据库的方言配置:一般使用MySQL5DialectMySQL55DialectMySQL57Dialect,分别对应不同版本的数据库;

  2. 数据库引擎:MyISAMInnoDB,默认是MyISAM,一般都需要修改。

  3. 自动建表:update/create-drop/create/none。一般使用update或者none

  4. 由于采用的连接池是c3p0,Spring在创建ComboPooledDataSource实例时会默认读取resources下的c3p0-config.xml,我们可以在该文件集中管理连接池的配置信息,向DBCP就不能这样操作。这就是使用c3p0的好处。

<?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:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:jpa="http://www.springframework.org/schema/data/jpa"
    xsi:schemaLocation="
	    http://www.springframework.org/schema/beans 
	    http://www.springframework.org/schema/beans/spring-beans.xsd
	    http://www.springframework.org/schema/context
	    http://www.springframework.org/schema/context/spring-context.xsd
	    http://www.springframework.org/schema/aop
	    http://www.springframework.org/schema/aop/spring-aop.xsd
	    http://www.springframework.org/schema/tx 
	    http://www.springframework.org/schema/tx/spring-tx.xsd
	    http://www.springframework.org/schema/data/jpa
	    http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
    ">
    
    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!-- 这里会自动读取resources下的c3p0-config.xml -->
    </bean>
    
    <!-- Spring 整合JPA 配置EntityManagerFactory -->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <!-- 指定Jpa持久化实现厂商类,这里以Hibernate为例 -->
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"></bean>
        </property>
        <!-- 指定JPA属性;如Hibernate中指定是否显示SQL的是否显示、方言等 -->
        <property name="jpaProperties">
	       <props>
		      <prop key="hibernate.dialect">org.hibernate.dialect.MySQL55Dialect</prop>
		      <prop key="hibernate.dialect.storage_engine">innodb</prop>
		      <prop key="hibernate.show_sql">true</prop>
		      <!-- <prop key="hibernate.format_sql">true</prop> -->
		      <prop key="hibernate.hbm2ddl.auto">update</prop>
	       </props>
        </property>
            
        <!-- 扫描实体的包 -->
        <property name="packagesToScan">
            <list>
                <value>cn.zzs.springdata.pojo</value>
            </list>
        </property>
    </bean>
    
    <!-- Spring Data JPA 的配置-->
    <!-- base-package:扫描dao 接口所在的包-->
    <jpa:repositories base-package="cn.zzs.springdata.dao"/>
    
    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    
    <!-- 配置开启注解事务处理 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <!-- 配置springIOC的注解扫描 -->
    <context:component-scan base-package="cn.zzs.springdata"/>
</beans>

对象映射的配置-注解

常规配置

这里需要注意主键的配置,支持IDENTITYSEQUENCETABLEAUTO

/**
 * @ClassName: User
 * @Description: 用户实体类
 * @author: zzs
 * @date: 2019年9月2日 上午11:14:53
 */
@Entity
@Table(name = "native_user")
public class User {
	/**
	 * 用户id
	 */
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY) //使用主键自动增长
	@Column(name = "user_id")
	private Long id;

	/**
	 * 用户名
	 */
	@Column(name = "user_name", unique = true)
	private String name;

	/**
	 * 用户年龄
	 */
	@Column(name = "user_age")
	private Integer age;

	/**
	 * 记录创建时间
	 */
	@Column(name = "gmt_create")
	private Date create;

	/**
	 * 记录最后一次修改时间
	 */
	@Column(name = "gmt_modified")
	private Date modified;

    //以下方法省略
}

如果是uuid的主键,配置方式如下:

	@Id
	@GenericGenerator(name = "uuid", strategy = "uuid")
	@GeneratedValue(generator = "uuid")
	@Column(name = "menu_id")
	private String id;

一对多配置

用户只有一个角色,一个角色下有多个用户。

一方
	/**
	 * 角色关联的用户
	 */
	@OneToMany(mappedBy = "role")
	@org.hibernate.annotations.ForeignKey(name = "none")
	private Set<User> users = new HashSet<User>();
多方
	/**
	 * 用户角色
	 */
	@ManyToOne
	@JoinColumn(name = "user_role_id", foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT))
	private Role role;

多对多配置

一个角色可以有多个权限菜单,一个菜单可以分配多个角色。

多方1
	/**
	 * 角色包含的权限菜单
	 */
	//@JoinTable:配置中间表信息
	//@joinColumns:建立当前表在中间表中的外键字段
	@ManyToMany
	@JoinTable(name = "native_role_menu", joinColumns = @JoinColumn(name = "role_id"), inverseJoinColumns = @JoinColumn(name = "menu_id"), foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT))
	private Set<Menu> menus = new HashSet<Menu>();
多方2
	/**
	 * 包含的角色
	 */
	@ManyToMany(mappedBy = "menus")
	@org.hibernate.annotations.ForeignKey(name = "none")
	private Set<Role> roles = new HashSet<Role>();

自关联配置

一个权限菜单有多个子菜单,且指向父菜单。

	/**
	 * 父菜单
	 */
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "menu_parent_id", foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT))
	private Menu parent;

	/**
	 * 子菜单
	 */
	@SuppressWarnings("deprecation")
	@OneToMany(targetEntity = Menu.class, cascade = { CascadeType.ALL }, mappedBy = "parent")
	@Fetch(FetchMode.SUBSELECT)
	@OrderBy("order")
	@org.hibernate.annotations.ForeignKey(name = "none")
	private Set<Menu> children = new HashSet<Menu>();

关于取消外键自动生成

实际项目中,我们都不会在数据库中建立外键,因为外键会降低数据库操作的效率。一般都是通过代码逻辑来控制。但是Hibernate会自动地更新外键,这里说说解决办法。

第一种比较简单,即配置hibernate.hbm2ddl.autonone,这样Hibernate就不会自动维护外键,当然,我们要提前建好表。这种方式其实对性能也是有好处的,推荐使用。

第二种就是本项目采取的方式。其实,主要是不想手动建表。这种方式需要在一方和多方都配置,缺点就是@org.hibernate.annotations.ForeignKey注解已经过时,但目前我还没找到替代方案。

	/**
	 * 多方的@JoinColumn中增加以下foreignKey属性
	 */
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "menu_parent_id", foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT))
	private Menu parent;

	/**
	 * 一方增加@org.hibernate.annotations.ForeignKey属性
	 */
	@SuppressWarnings("deprecation")
	@OneToMany(targetEntity = Menu.class, cascade = { CascadeType.ALL }, mappedBy = "parent")
	@Fetch(FetchMode.SUBSELECT)
	@OrderBy("order")
	@org.hibernate.annotations.ForeignKey(name = "none")
	private Set<Menu> children = new HashSet<Menu>();

DAO接口的定义

Spring Data JPA 会为我们生成DAO实现类,只要我们定义好接口就行了,具体就是实现Repository的子接口,当然,它的子接口有多个,建议实现JpaRepositoryImplementation,看看以下继承体系图就明白了。

/**
 * @ClassName: UserDao
 * @Description: 用户操作的接口
 * @author: zzs
 * @date: 2019年9月2日 上午11:30:11
 */
public interface UserDao extends JpaRepositoryImplementation<User, Long> {}

Repository接口的继承体系

[外链图片转存失败(img-jhVRlvks-1567493657463)(https://github.com/ZhangZiSheng001/spring-data-jpa-demo/blob/master/img/Repository.png)]

如上图所示,Respository的实现类通过继承不同的接口来获取相应的功能:

  1. CrudRepository:基本的CRUD操作。

  2. PagingAndSortingRepository:分页和排序操作。继承了CrudRepository

  3. JpaSpecificationExecutor:条件查询操作。

  4. JpaRepository:对父接口进行适配处理。继承了PagingAndSortingRepository

  5. JpaRepositoryImplementation:继承了JpaRepositoryJpaSpecificationExecutor

CrudRepository的API使用

	/**
	 * 测试添加或更新用户
	 */
	@Test
	@Transactional
	@Rollback(value = false) //spring的测试默认会回滚事务,这里关闭下
	public void testSaveOrUpdate() {
		User user = new User("zzs002", 18, new Date(), new Date());
		user.setRole(roleDao.findByName("销售经理"));
		//如果存在且有差异就更新,不存在就插入
		userDao.save(user);
	}

	/**
	 * 测试删除用户
	 */
	@Test
	@Transactional
	@Rollback //这里我不想删除,所以开启了回滚
	public void testDelete() {
		//获取用户对象
		User user = userDao.findByNameIs("zzs001");
		//删除用户
		userDao.delete(user);
	}

PagingAndSortingRepository的API使用

	/**
	 * 
	 * @Title: testPagingAndSortingRepository
	 * @Description: 测试PagingAndSortingRepository接口的方法:分页+排序
	 * @author: zzs
	 * @date: 2019年9月3日 上午2:18:26
	 * @return: void
	 */
	@Test
	@Transactional
	@Rollback(value = false) //spring的测试默认会回滚事务,这里关闭下
	public void testPagingAndSortingRepository() {
		//设置分页参数,排序参数
		Pageable pageable = PageRequest.of(0, 3, Direction.ASC, "age", "name");
		//执行查询,获得分页模型
		Page<User> page = userDao.findAll(pageable);
		//获取分页模型数据
		System.out.println("总记录数:" + page.getTotalElements());
		System.out.println("总页数:" + page.getTotalPages());
		List<User> list = page.getContent();
		if (list != null) {
			for (User user : list) {
				System.out.println(user);
			}
		}
	}

JpaSpecificationExecutor的API使用

这个接口功能还是非常强大的,可以满足多条件+分页+排序的需求。

	/**
	 * 
	 * @Title: testJpaSpecificationExecutor
	 * @Description: 测试JpaSpecificationExecutor接口的方法:多条件查询+分页+排序
	 * @author: zzs
	 * @date: 2019年9月3日 上午2:22:12
	 * @return: void
	 */
	@SuppressWarnings("serial")
	@Test
	@Transactional
	@Rollback(value = false) //spring的测试默认会回滚事务,这里关闭下
	public void testJpaSpecificationExecutor() {
		//设置查询条件
		Specification<User> spec = new Specification<User>() {
			@Override
			public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
				//创建多个查询条件
				Predicate pre1 = criteriaBuilder.like(root.get("name"), "zzs%");
				Predicate pre2 = criteriaBuilder.ge(root.get("age"), 17);
				return criteriaBuilder.and(pre1,pre2);
				//return criteriaBuilder.or(pre1, pre2);
			}
		};

		//设置分页参数和排序规则,并执行查询
		Page<User> page = userDao.findAll(spec, PageRequest.of(0, 3, Direction.ASC, "age", "name"));
		//遍历结果集
		System.out.println("总记录数:" + page.getTotalElements());
		System.out.println("总页数:" + page.getTotalPages());
		List<User> list = page.getContent();
		if (list != null) {
			for (User user : list) {
				System.out.println(user);
			}
		}

	}

基于方法名称命名规则查询

Spring Data JPA除了以上API,还支持按方法规则自定义查询。

方法定义规则

关键字方法命名sql where语句
AndfindByNameAndPwdwhere name= ? and pwd =? Or findByNameOrSex
Is,EqualfindById,findByIdEqual,findByIdIswhere id= ?
BetweenfindByIdBetweenwhere id between ? and ?
LessThanfindByIdLessThanwhere id < ?
LessThanEqualfindByIdLessThanEqualwhere id <= ?
GreaterThanfindByIdGreaterThanwhere id > ?
GreaterThanEqualfindByIdGreaterThanEqualwhere id > = ?
AfterfindByIdAfterwhere id > ?
BeforefindByIdBeforewhere id < ?
IsNullfindByNameIsNullwhere name is null
isNotNull,NotfindByNameNotNullwhere name is not
LikefindByNameLikewhere name like ?
NotLikefindByNameNotLikewhere name not like ?
StartingWithfindByNameStartingWithwhere name like ‘?%’
EndingWithfindByNameEndingWithwhere name like ‘%?’
ContainingfindByNameContainingwhere name like ‘%?%’
OrderByfindByIdOrderByXDescid=? order by x desc
NotfindByNameNotwhere name <> ?
InfindByIdIn(Collection<?> c)where id in (?)
NotInfindByIdNotIn(Collection<?> c)where id not in (?)
TruefindByAaaTruewhere aaa = true
FalsefindByAaaFalsewhere aaa = false
IgnoreCasefindByNameIgnoreCasewhere UPPER(name)=UPPER(?)

在接口中定义方法

注意:必须严格按照规则定义方法。

	/**
	 * 
	 * @Title: findByNameLike
	 * @Description: 根据指定字符模糊查询用户
	 * @author: zzs
	 * @date: 2019年9月2日 下午10:31:16
	 * @param name
	 * @return
	 * @return: List<User>
	 */
	List<User> findByNameLike(String name);

编写测试

	/**
	 * 测试根据name模糊查询用户(按方法命名规则定义方法)
	 * @throws Exception 
	 */
	@Test
	@Transactional
	@Rollback(value = false) //spring的测试默认会回滚事务,这里关闭下
	public void testFind() throws Exception {
		//查询用户
		List<User> list = userDao.findByNameLike("zzs%");
		if (list != null && list.size() != 0) {
			for (User user : list) {
				System.out.println(user);
				System.out.println(user.getRole());
			}
		}
	}

基于@Query 注解查询、更新

有两种,一种是HQL演变过来的JPQL,另一种是普通的SQL

在接口中注解定义方法

注意:JPQL占位符如果使用了?,会报错:IllegalArgumentException,因为JPQL并不支持?格式的占位符。

改用命名参数(:abc)或者(?123)两种方法,第二种占位符索引必须大于0。

这里我想通过JPQL实现分页查询的,但好像不行,因为JPQL并不支持limit等非通用语法,目前暂无解决方案。

	/**
	 * 
	 * @Title: findByAgeLessThanUseJPQL
	 * @Description: 根据年龄使用JPQL查询用户
	 * @author: zzs
	 * @date: 2019年9月2日 下午10:31:39
	 * @param age
	 * @return: List<User>
	 */
	@Query("from User where age < ?1 ")
	List<User> findByAgeLessThanUseJPQL(Integer age);

	/**
	 * 
	 * @Title: findByAgeLessThanWithPageUseSQL
	 * @Description: 根据年龄使用SQL分页查询用户并分页
	 * @author: zzs
	 * @date: 2019年9月2日 下午10:31:39
	 * @param age
	 * @param firstResult
	 * @param maxResults
	 * @return: List<User>
	 */
	@Query(value = "select * from jpa_user where user_age < ?1 limit ?2 , ?3 ", nativeQuery = true)
	List<User> findByAgeLessThanWithPageUseSQL(Integer age, Integer firstResult, Integer maxResults);

	/**
	 * 
	 * @Title: updateAgeByName
	 * @Description: 更新指定用户的年龄
	 * @author: zzs
	 * @date: 2019年9月3日 上午12:42:11
	 * @param age
	 * @param id
	 * @return: void
	 */
	@Query("update User set age = ?1 , modified=now() where name = ?2")
	@Modifying //表示当前语句为DML语句
	void updateAgeByName(Integer age, String name);

编写测试

	/**
	 * 测试@Query+JPQL更新用户
	 */
	@Test
	@Transactional
	@Rollback(value = false) //spring的测试默认会回滚事务,这里关闭下
	public void testUpdateAgeByName() {
		//修改用户
		userDao.updateAgeByName(22, "zzs001");
	}

	/**
	 * 测试使用@Query+JPQL查询用户
	 */
	@Test
	@Transactional
	@Rollback(value = false) //spring的测试默认会回滚事务,这里关闭下
	public void testFindByAgeLessThanUseJPQL() {
		//查询用户
		List<User> list = userDao.findByAgeLessThanUseJPQL(22);
		//遍历查询结果
		if (list != null && list.size() != 0) {
			for (User user : list) {
				System.out.println(user);
				System.out.println(user.getRole());
			}
		}
	}

	/**
	 * 测试使用@Query+SQL分页查询用户
	 */
	@Test
	@Transactional
	@Rollback(value = false) //spring的测试默认会回滚事务,这里关闭下
	public void testFindByAgeLessThanUseSQL() {
		//查询用户
		List<User> list = userDao.findByAgeLessThanWithPageUseSQL(22, 0, 3);
		//遍历查询结果
		if (list != null && list.size() != 0) {
			for (User user : list) {
				System.out.println(user);
				System.out.println(user.getRole());
			}
		}
	}

用户自定义Repository接口

编写自定义Respository接口

/**
 * @ClassName: MyRespository
 * @Description: 对象的自定义Respository
 * @author: zzs
 * @date: 2019年9月3日 上午12:54:22
 */
public interface MyRespository<T, ID> {
	/**
	 * 
	 * @Title: findByNameLikeWithPageAsc
	 * @Description: 根据对象名分页查询对象并按指定属性升序排序
	 * @author: zzs
	 * @date: 2019年9月3日 上午1:04:49
	 * @param name
	 * @param firstResult
	 * @param maxResults
	 * @param fields
	 * @return: List<T>
	 */
	List<T> findByNameLikeWithPageAsc(String name, Integer firstResult, Integer maxResults, String... fields);
}

在UserDao中实现该接口

/**
 * @ClassName: UserDao
 * @Description: 用户操作的接口
 * @author: zzs
 * @date: 2019年9月2日 上午11:30:11
 */
public interface UserDao extends JpaRepositoryImplementation<User, Long>, MyRespository<User, Integer> {}

编写自定义Respository接口的实现类

注意:这个实现类的命名有严格要求:必须是UserDao+Impl

/**
 * @ClassName: UserDaoimpl
 * @Description: 自定义Respository的实现类
 * @author: zzs
 * @date: 2019年9月3日 上午1:10:15
 */
public class UserDaoImpl implements MyRespository<User, Integer> {
	@PersistenceContext(name = "entityManagerFactory")
	private EntityManager entityManager;

	@SuppressWarnings("unchecked")
	@Override
	public List<User> findByNameLikeWithPageAsc(String name, Integer firstResult, Integer maxResults,
			String... fields) {
		//定义sql语句
		StringBuilder sqlBuilder = new StringBuilder("from User where name like ?1 ");
		//拼接sql语句
		int length = 0;
		if (fields != null && (length = fields.length) != 0) {
			sqlBuilder.append("order by");
			for (int i = 0; i < fields.length; i++) {
				if(i == length - 1) {
					sqlBuilder.append(" ?" + (i + 2));
					continue;
				}
				sqlBuilder.append(" ?" + (i + 2) + " ,");
			}
		}
		//获得Query对象
		System.err.println(sqlBuilder.toString());
		Query query = entityManager.createQuery(sqlBuilder.toString());
		//设置参数
		query.setParameter(1, name);
		if (fields != null && fields.length != 0) {
			for (int i = 0; i < fields.length; i++) {
				query.setParameter(i+2, fields[i]);
			}
		}
		//设置分页参数
		query.setFirstResult(firstResult);
		query.setMaxResults(maxResults);
		//执行查询,并返回结果
		return query.getResultList();
	}
}

编写测试

	/**
	 * 
	 * @Title: testMyRespository
	 * @Description: 测试自定义Respository的方法
	 * @author: zzs
	 * @date: 2019年9月3日 上午1:47:37
	 * @return: void
	 */
	@Test
	@Transactional
	@Rollback(value = false) //spring的测试默认会回滚事务,这里关闭下
	public void testMyRespository() {
		//根据对象名分页查询对象并按指定属性升序排序
		List<User> list2 = userDao.findByNameLikeWithPageAsc("zzs%", 0, 3, "age", "name");
		if (list2 != null && list2.size() != 0) {
			for (User user : list2) {
				System.out.println(user);
			}
		}
	}

项目路径:

spring-data-jpa-demo

学习使我快乐!!

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值