ORM ( Object Relation Mapping )是对象/关系映射。它提供了概念性的、易于理解的数据模型,将数据库中的表和内存中的对象建立映射关系。它是随着面向对象的软件开发方法的发展而产生的,面向对象的开发方法依然是当前主流的开发方法。
对象和关系型数据是业务实体的两种表现形式。业务实体在内存中表现为对象,在数据库中表现为关系型数据。内存中的对象不会被永久保存,只有关系型数据库(或NoSQL数据库,或文件) 中的对象会被永久保存。
对象/关系映射(ORM)系统一般以中间件的形式存在,因为内存中的对象之间存在关联和继承关系,而在数据库中,关系型数据无法直接表达多对多的关联和继承关系。对象、数据库通过ORM 映射的关系如图所示。
Spring Data是Spring的一个子项目,旨在统一和简化各类型数据的持久化存储方式,而不拘泥于是关系型数据库还是NoSQL数据库。无论是哪种持久化存储方式,数据访问对象(Data Access Objects, DAO)都会提供对对象的增加、删除、修改和查询的方法,以及排序和分页方法等。Spring Data 提供了基于这些层面的统一接口(如:CrudRepository、PagingAndSorting- Repository),以实现持久化的存储。Spring Data包含多个子模块,主要分为主模块和社区模块。
JPA (Java Persistence API )是Java的持久化API,用于对象的持久化。它是一个非常强大的ORM持久化的解决方案,免去了使用JDBCTemplate开发的编写脚本工作。JPA通过简单约定好接口方法的规则自动生成相应的JPQL语句,然后映射成POJO对象。
JPA是一个规范化接口,封装了Hibernate的操作作为默认实现,让用户不通过任何配置即可完成数据库的操作。JPA、Spring Date和Hibernate的关系如图所示。
Hibernate 主要通过 hibernate-annotation、hibernate-entitymanager、hibernate-core 三个组件来操作数据。
可使用以下代码来创建实体类
@Data @Entity public class User{ private int id; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private String name; private int age; }
对比JPA与JDBCTemplate创建实体的方式可以看出:JPA的实现方式简单明了,不需要重写映射(支持自定义映射),只需要设置好属性即可。id的自増由数据库自动管理,也可以由程序管理,其他的工作JPA自动处理好了。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
Spring Boot项目使用MySQL等关系型数据库,需要配置连接信息,可以在 application.yml文件中进行配置。以下代码配置了与MySQL数据库的连接信息:
spring: jpa: hibernate: ddl-auto: update show-sql: true jooq: sql-dialect: org.hibernate.dialect.Mysql5InnoDBDialect datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true username: root password: 123456
代码解释如下。
在 Spring Data JPA 中,要控制 Session 的生命周期:否则会出现"could not initialize proxy
[xxxx#18]-no Session”错误。可以在配置文件中配置以下代码来控制Session的生命周期:
open-in-view: true properties: hibernate: enable_lazy_load_no_trans: true
package com.itheima.domain; import lombok.Data; import javax.persistence.*; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Size; import java.awt.*; import java.io.Serializable; import java.util.Arrays; @Entity @Data public class Article implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) /** * Description: IDENTITY代表由数据库控制,auto代表由Spring Boot应用程序统一控制(有多个表时,id 的自增值不一定从1开始) */ private long id; @Column(nullable = false,unique = true) @NotEmpty(message = "标题不能为空") private String title; /** * Description:枚举类型 */ @Column(columnDefinition = "enum('图','图文','文')") private String type;//类型 /** * Description: Boolean 类型默认false */ private Boolean available= Boolean.FALSE; @Size(min = 0,max = 20) private String keyword; @Size(max = 255) private String description; @Column(nullable = false) private String body; /** * Description:创建虚拟字段 */ @Transient private List keywordlists; public List getKeywordlists(){ return (List) Arrays.asList(this.keyword.trim().split("|")); } public void setKeywordlists(List keywordlists) { this.keywordlists = keywordlists; } }
JPA提供了操作数据库的接口。在开发过程中继承和使用这些接口,可简化现有的持久化开发 工作。可以使Spring找到自定义接口,并生成代理类,后续可以把自定义接口注入Spring容器中进行管理。在自定义接口过程中,可以不写相关的SQL操作,由代理类自动生成。
JpaRepository 继承自 PagingAndSortingRepository, 该接口提供了 JPA 的相关实用功能, 以及通过Example进行查询的功能。Example对象是JPA提供用来构造查询条件的对象。该接口的关键代码如下:
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, Query By ExampleExecutor<T> {}
在上述代码中,T表示实体对象,ID表示主键。ID必须实现序列化。
JpaRepository提供的方法见下表
PagingAndSortingRepository继承自CrudRepository提供的分页和排序方法。其关键代码如下:
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
lterable<T> findAll(Sort var1);
Page<T> findAII(Pageable var1);
}
其方法有如下两种。
CrudRepository接口继承自Repository接口,并新增了增加、删除、修改和查询方法。
CrudRepository提供的方法见下表
@RequestMapping("/article") public ModelAndView articleList(@RequestParam(value = "start",defaultValue = "0") Integer start, @RequestParam(value = "limit",defaultValue = "10") Integer limit){ start = start < 0?0:start; Sort sort = new Sort(Sort.Direction.DESC,"id"); Pageable pageable = PageRequest.of(start, limit, sort); Page<Article> page = articleRepository.findAll(pageable); ModelAndView mav = new ModelAndView("admin/article/list"); mav.addObject("page",page); return mav; }
Sort类专门用来处理排序。最简单的排序就是先传入一个属性列,然后根据属性列的值进行排序。默认情况下是升序排列。它还可以根据提供的多个字段属性值进行排序。例如以下代码是通过 Sort.Order对象的List集合来创建Sort对象的:
List<Sort.Order> orders = new ArrayList<Sort.Order>(); orders.add(new Sort.Order(Sort.Direction.DESC,"id")); orders.add(new Sort.Order(Sort.Direction.ASC,"view")); Pageable pageable = PageRequest.of(start,limit,sort); Pageable pageable = PageRequest.of(start,limit,Sort.by(orders));
Sort排序的方法还有下面几种:
约定方法名一定要根据命名规范来写,Spring Data会根据前缀、中间连接词(Or、And、Like、NotNull等类似SQL中的关键词)、内部拼接SQL代理生成方法的实现。约定方法名的方法见下表
接口方法的命名规则也很简单,只要明白And、Or、Is、Equal、Greater、StartingWith等英文单词的含义,就可以写接口方法。具体用法如下:
package com.itheima.domain; import org.springframework.data.repository.Repository; import java.util.List; public interface UserRepository extends Repository<User,Long> { List<User> findByEmailOrName(String email,String name); }
上述代码表示,通过email或name来查找User对象。
约定方法名还可以支持以下几种语法:
JPQL语言(Java Persistence Query Language)是一种和SQL非常类似的中间性和对象化查询语言,它最终会被编译成针对不同底层数据库的SQL语言,从而屏蔽不同数据库的差异。
JPQL语言通过Query接口封装执行,Query接口封装了执行数据库查询的相关方法。调用 EntityManager的Query、NamedQuery及NativeQuery方法可以获得查询对象,进而可调用 Query接口的相关方法来执行查询操作。
JPQL是面向对象进行查询的语言,可以通过自定义的JPQL完成UPDATE和DELETE操作。JPQL不支持使用INSERT,对于UPDATE或DELETE操作,必须使用注解@Modifying 进行修饰。
public interface UserRepository extends JpaRepository<User,Long> { @Query("select u from User u where u.name = ?1") User findByName(String name); }
public interface UserRepository extends JpaRepository<User,Long> { @Query("select u from User u where u.name like %?1") List<User> findByName(String name); }
@Override @Query(value = "select * from user u where u.id = :id", nativeQuery = true) Optional<User> findById(@Param("id") Long id);
@Query(value = "select * from user", nativeQuery = true) List<User> findAllNative();
@Query(value = "select * from user where email = ?1", nativeQuery = true) User findByEmail(String email);
@Query(value = "select * from user where name = ?1", countQuery = "select count(*) from user where name = ?1", nativeQuery = true) Page<User> findByName(String name, Pageable pageable);
@Modifying @Query("update user set email = :email where name = :name") Void updateUserEmaliByName(@Param("name") String name,@Param("email") String email);
如果要使 Repository 支持 Specification 查询,则需要在 Repository 中继承 JpaSpecification- Executor接口,具体使用见如下代码:
package com.itheima.executor; import com.itheima.dao.ArticleRepository; import com.itheima.domain.Article; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.domain.Specification; import org.springframework.test.context.junit4.SpringRunner; import javax.persistence.criteria.*; @SpringBootTest @RunWith(SpringRunner.class) public class testJpaSpecificationExecutor { @Autowired private ArticleRepository articleRepository; @Test public void testJpaSpecificationExecutor(){ int pageNo = 0; int pageSize = 5; PageRequest pageable = PageRequest.of(pageNo,pageSize); Specification<Article> specification = new Specification<Article>(){ @Override public Predicate toPredicate(Root<Article> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) { Path path = root.get("id"); Predicate predicate1 = criteriaBuilder.gt(path,2); Predicate predicate2 = criteriaBuilder.equal(root.get("num"),42283); Predicate predicate = criteriaBuilder.and(predicate1,predicate2); return predicate; } }; Page<Article> page = articleRepository.findAll(specification,pageable); System.out.println("总记录数:"+page.getTotalElements()); System.out.println("当前第:"+(page.getNumber()+1)+"页"); System.out.println("总页数:"+page.getTotalPages()); System.out.println("当前页面的List:"+page.getContent()); System.out.println("当前页面的记录数:"+page.getNumberOfElements()); } }
代码解释如下。
运行上面的测试代码,在控制台会输出如下结果(确保数据库己经存在数据):
Hibernate: select card0_.id as id1_0_, card0_.num as num2_0_ from card card0_ where card0_.id>2 and card0_.num=422803 limit ? Hibernate: select count(cardO_.id) as col_0_0_ from card card0_ where cardO_Jd>2 and card0_.num=422803 总记录数:6 当前第:1页 总页数:2 当前页面的 List: [Card(id=4, num二422803), Card(id=8, num=422803), Card(id=10, num=422803), Card(id=20t num=422803), Card(id=23, num=422803)] 当前页面的记录数:5
Spring Data可以通过Example对象来构造JPQL查询,具体用法见以下代码:
User user = new User(); user.setName("test"); ExampleMatcher matcher = ExampleMatcher.matching() .withIgnorePaths("name") .withIncludeNullValues() .withStringMatcher(ExampleMatcher.StringMatcher.ENDING); Example<User> example = Example.of(user,matcher); List<User> list = userRepository.findAll(example);
QueryDSL也是基于各种ORM之上的一个通用查询框架,它与Spring Data JPA是同级别的。 使用QueryDSL的API可以写岀SQL语句(Java代码,非真正标准SQL),不需要懂SQL语句。 它能够构建类型安全的查询。这与JPA使用原生查询时有很大的不同,可以不必再对“Object[]' 进行操作。它还可以和JPA联合使用。
@Entity @Data public class Article extends BaseEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(nullable = false,unique = true) @NotEmpty(message = "标题不能为空") private String title; @Column(nullable = false) private String body; }
代码解释如下:
@Service public interface ArticleRepository extends JpaRepository<Article,Long>, JpaSpecificationExecutor<Article>{ Article findById(long id); }
public interface ArticleService { public List<Article> getArticleList(); public Article findArticleById(long id); }
在impl包下,新建article的impl实现service,并标注这个类为service服务类。
通过implements声明使用ArticleService接口,并重写其方法,见以下代码:
@Service public class ArticleServiceImpl implements ArticleService{ @Autowired private ArticleRepository articleRepository; @Override public List<Article> getArticleList() { return articleRepository.findAll(); } @Override public Article findArticleById(long id) { return articleRepository.findById(id); } }
package com.itheima.controller; import com.itheima.dao.ArticleRepository; import com.itheima.domain.Article; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; @Controller @RequestMapping("article") public class ArticleController { @Autowired private ArticleRepository articleRepository; @RequestMapping("") public ModelAndView articlelist(@RequestParam(value = "start",defaultValue = "0")Integer start, @RequestParam(value = "limit",defaultValue = "5")Integer limit) { start = start < 0 ? 0 : start; Sort sort = Sort.by(Sort.Direction.DESC, "id"); Pageable pageable = PageRequest.of(start, limit, sort); Page<Article> page = articleRepository.findAll(pageable); ModelAndView mav = new ModelAndView("article/list"); mav.addObject("page", page); return mav; } @GetMapping("/{id}") public ModelAndView getArticle(@PathVariable("id") long id) { Article article = articleRepository.findById(id); ModelAndView mav = new ModelAndView("article/show"); mav.addObject("article", article); return mav; } @GetMapping("/add") public String addArticle(){ return "article/add"; } @PostMapping("") public String saveArticle(Article model){ articleRepository.save(model); return "redirect:/article/"; } @DeleteMapping("/{id}") public String deleteArticle(@PathVariable("id") long id){ articleRepository.deleteById(id); return "redirect:"; } @GetMapping("/edit/{id}") public ModelAndView editArticle(@PathVariable("id") long id) { Article model = articleRepository.findById(id); ModelAndView mav = new ModelAndView("article/edit"); mav.addObject("article", model); return mav; } @PutMapping("/{id}") public String editArticleSave(Article model,long id){ model.setId(id); articleRepository.save(model); return "redirect:"; } }
在操作实体类时,通常需要记录创建时间和更新时间。如果每个对象的新增或修改都用手工来操作,则会显得比较烦琐。这时可以使用Spring Data JPA的注解@EnableJpaAuditing来实现自动填充字段功能。具体步骤如下。
通过在入口类中加上注解@EnableJpaAuditing,来开启JPA的Auditing功能
@Data @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class BaseEntity { @CreatedDate private Long createTime; //createTime @LastModifiedDate private Long updateTime; //updateTime @Column(name = "create_by") @CreatedBy private Long createBy; //createBy @Column(name = "lastmodified_by") @LastModifiedBy private Long lastModifiedBy; //lastModifiedBy }
上述代码已经自动实现了创建和更新时间赋值,但是创建人和最后修改人并没有赋值,所以需要实现"AuditorAware"接口来返回需要插入的值
public class InjectAuditor implements AuditorAware<String> { //给 Bean 中的 @CreatedBy @LastModifiedBy 注入操作人 @Override public Optional<String> getCurrentAuditor() { SecurityContext securityContext = SecurityContextHolder.getContext(); if (securityContext==null) { return null; } if (securityContext.getAuthentication()==null) { return null; }else { String loginUserName = securityContext.getAuthentication().getName(); Optional<String> name = Optional.ofNullable(loginUserName); return name; } } }
代码解释如下。
@Configuration:表示此类是配置类。让Spring来加我该类配置。
SecurityContextHolder:用于获取 SecurityContext,其存放了 Authentication 和特定于请求的安全信息。这里是判断用户是否登录。如果用户登录成功,则荻取用户名,然后把用户名返回给操作人。
要在其他类中使用基类,通过在其他类中继承即可
对象关系映射(object relational mapping )是指通过将对象状态映射到数据库列'来开发和 维护对象和关系数据库之间的关系。它能够轻松处理(执行)各种数据库操作,如插入、更新、 删除等
ORM的映射方向是表与表的关联(join ),可分为两种。