Spring Boot JPA - 基本使用 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
lufficc
V2EX    程序员

Spring Boot JPA - 基本使用

  •  1
     
  •   lufficc
    lufficc 2017-03-27 21:56:27 +08:00 6934 次点击
    这是一个创建于 3121 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文地址:Spring Boot JPA - 基本使用

    什么是 JPA ?

    JPA ( The Java Persistence API )是用于访问,持久化和管理 Java 对象 /类与关系型数据库之间的数据交互的 Java 规范。 JPA 被定义为 EJB ( Enterprise JavaBeans ) 3.0 规范的一部分,作为 EJB 2 CMP 实体 Bean 规范的替代。

    注意, JPA 只是一个标准,只定义了一系列接口,而没有具体的实现。很多企业级框架提供了对 JPA 的实现,如 Spring 。因此 Spring 本身与 JPA 无关,只是提供了对 JPA 的支持,因此在 Spring 中你也会看到很多注解都是属于 javax.persistence 包的。

    JPA 允许 POJO( Plain Old Java Objects )轻松地持久化,而不需要类来实现 EJB 2 CM P 规范所需的任何接口或方法。 JPA 还允许通过注解或 XML 定义对象的关系映射,定义 Java 类如何映射到关系数据库表。 JPA 还定义了一个运行时 EntityManager API ,用于处理对象的查询和管理事务。 同时, JPA 定义了对象级查询语言 JPQL,以允许从数据库中查询对象,实现了对数据库的解耦合,提高了程序的可移植性,而不具体依赖某一底层数据库。

    JPA 是 Java 持久化规范中的一个最新版本。第一个版本是 OMG 持久性服务 Java 绑定,但这个一个失败的产品,甚至没有任何商业产品支持它。接下来的版本是 EJB 1.0 CMP Entity Beans ,它已经非常成功地被大型 Java EE 提供程序( BEA , IBM )采用,但是它复杂性太高而且性能比较差。 EJB 2.0 CMP 试图通过引入本地接口来减少 Entity Bean 的一些复杂性,但是大多数复杂性仍然存在,而且缺乏可移植性。

    历史总是要向前发展的,种种的这些使得 EJB 3.0 规范将降低复杂性作为主要目标,这导致规范委员会沿着 JPA 的路径前进。 JPA 旨在统一 EJB 2 CMP , JDO , Hibernate ,从目前来看, JPA 的确取得了成功。

    目前大多数持久化供应商已经发布了 JPA 的实现,并被行业和用户采用。这些包括 Hibernate (由 JBoss 和 Red Hat 收购), TopLink (由 Oracle 收购)和 Kodo JDO (由 BEA 和 Oracle 收购)。其他支持 JPA 的产品包括 Cocobase (由 Thought Inc. 收购)和 JPOX 。

    定义实体类

    依赖

    在 Spring Boot 中引入 Spring Data JPA 很简单,在 pom.xml 中加入依赖即可:

    <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> 

    Model

    如果具体的 SQL 语句来创建数据库表,就会和具体的底层数据库产生耦合。在 Springl Boot 中,我们可以定义一系列 Entity ,来实现 Java 对象到 数据表的映射。

    加入我们的数据表有一些公用的属性,我们可以定义一个超类,来声明这些公用属性。@MappedSuperclass 注解表明实体是一个超类,保证不会被 Spring 实例化 (保险起见加上abstract):

    @MappedSuperclass public abstract class BaseModel implements Serializable { /** * */ private static final long serialVersiOnUID= 1782474744437162148L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ID") private Long id; public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; BaseModel baseModel = (BaseModel) o; return id != null ? id.equals(baseModel.id) : baseModel.id == null; } @Override public int hashCode() { return id != null ? id.hashCode() : 0; } } 

    @Id 表明实体主键为整数类型,@GeneratedValue 表明主键值生成策略,strategy 可选值为 TABLESEQUENCEIDENTITYAUTO 。如果我们想对列加上更多的限制,可以使用 @Column 注解,@Column 可以制定列的属性有:name,列名;unique ,是否为一;nullable ,是否为空;length ,长度;columnDefinition,列定义,如 TEXT 类型;等等。

    注意,列名和表名到数据库的映射策略,定义在 org.hibernate.boot.model.naming 包下,你也可以定义自己的映射策略,然后修改 application.yml spring.jpa.hibernate.naming.physical-strategy 的属性即可。

    关系

    以一个简单博客系统来说明吧,包括用户,文章,分类,标签四个表。用户与文章是一对多,一个用户可以有多篇文章;分类与文章也是一对多,文章与标签是多对多: User 表:

    @Entity @Table public class User extends BaseModel { private String name; private String email; private String address; @OneToMany(mappedBy = "user") private List<Post> posts; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public List<Post> getPosts() { return posts; } public void setPosts(List<Post> posts) { this.posts = posts; } } 

    @OneToMany 声明了 UserPost 的一对多关系,mappedBy 指明了 Post 类中维持该关系的字段名称是 user ,即 Post 表中的 user 字段: Post 表:

    @Entity @Table public class Post extends BaseModel { private String title; @Column(columnDefinition = "TEXT") private String content; @ManyToOne @JoinColumn(name = "category_id") private Category category; @ManyToMany @JoinTable(name = "post_tag", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id")) private Set<Tag> tags; private Date createdAt; @ManyToOne @JoinColumn(name = "user_id") private User user; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.cOntent= content; } public Category getCategory() { return category; } public void setCategory(Category category) { this.category = category; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public Date getCreatedAt() { return createdAt; } public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; } public Set<Tag> getTags() { return tags; } public void setTags(Set<Tag> tags) { this.tags = tags; } } 

    Post 类中,因为 UserPost 的一对多关系,所以 user 字段加上了 @JoinColumn(name = "user_id"),表明会在 Post 表中新增加一列 user_id ,因为一对多关系大多数会在“多”的一方加入“一”那一方的主键来做外键(如果不加 @JoinColumn(name = "user_id") , JPA 会默认新建一个表来维持这种关系)。 Tag 表:

    @Entity @Table public class Tag extends BaseModel { private String name; @ManyToMany @JoinTable(name = "post_tag", joinColumns = @JoinColumn(name = "tag_id"), inverseJoinColumns = @JoinColumn(name = "post_id")) private Set<Post> users = new HashSet<>(); public String getName() { return name; } public void setName(String name) { this.name = name; } public Set<Post> getUsers() { return users; } public void setUsers(Set<Post> users) { this.users = users; } } 

    来说一下多对多,多对多需要一个额外的表来保持关系,所以 PostTag 表都使用了 @JoinTable 注解。其中 name 指的是额外表的名称,当前是 post_tagjoinColumns 指的是当前当前表的主键在额外的表中的名称,如 tag_id 表明 Tag 的 ID 在 post_tag 中名称为 tag_idinverseJoinColumnsjoinColumns 正好相反,PostTag 中他们的属性正好相反。

    Repository

    核心概念

    Spring Data 中最核心的一个接口是 Repository 接口,这是一个空接口,就像 Serializable 接口一样只提供一个标识,供 Spring 动态代理其方法(或者路由到接口的实现上)。实质上,Repository 只是为了获取 Domain Class 和 Domain Class 的 ID 类型等信息。

    但是只提供给开发者一个空接口太不负责了!所以 Spring 内置提供了 CrudRepository (继承自 RepositoryCrud 的意思是增删改查等基本操作),这样我们可以对数据库进行基本的操作。每个方法的意思不用注释,应该能根据它的方法名,参数,返回类型来判断吧:

    public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> { <S extends T> S sve(S entity); T findOne(ID primaryKey); Iterable<T> findAll(); Long count(); void delete(T entity); boolean exists(ID primaryKey); // … more functionality omitted. } 

    当然继承自 CrudRepository 的有一个 PagingAndSortingRepository ,提供了基本的分页操作。

    public interface PagingAndSortingRepository<T, ID extends Serializable> extends CrudRepository<T, ID> { Iterable<T> findAll(Sort sort); Page<T> findAll(Pageable pageable); } 

    因此在项目中我们可以根据需要继承 RepositoryCrudRepositoryPagingAndSortingRepository 或者 JpaRepository (停供了一些批量操作,如批量删除、增加)。如果你想为项目中所有 Repository 创建一个自定义的基 Repository 来让所有继承自该接口的接口共享方法,可以使用 @NoRepositoryBean 注解,内置的 PagingAndSortingRepository 或者 JpaRepository 也都加了 @NoRepositoryBean @NoRepositoryBean 注解,这表明 Spring 不会在运行时动态生成该接口的实例:

    @NoRepositoryBean interface MyBaseRepository<T, ID extends Serializable> extends Repository<T, ID> { T findOne(ID id); T save(T entity); T customSharedMethod(String arg); } interface UserRepository extends MyBaseRepository<User, Long> { User findByEmailAddress(EmailAddress emailAddress); } 

    注意到 findOne(…)save(…) 方法 Spring 在 SimpleJpaRepository 类中已经有了实现,所以调用这些方法时会路由到 SimpleJpaRepository 中对用的方法,而自定义的 SimpleJpaRepository 中不存在方法如 T customSharedMethod(String arg) 会由 Spring 动态代理来执行。

    因为 Spring 内置的方法 Repositorys 不可能满足我们所有的需求,所以在 Repository 中自定义自己的方法是必不可少的。

    那么问题来了,我们自定义的方法, Spring 怎么知道如何去执行呢? Spring 有一套自己的查找策略。

    如果你的方法签名比较简单,像 findOne(…) 或者 save(…) ,这些方法的执行会被路由到 SimpleJpaRepository 中对应的方法去执行。而其他的 Spring 则利用自己的机制来解析。

    机制首先找到方法名的 find … By, read … By, query … By, count … By, 或者 get … By 前缀,接着解析剩下的部分。... 的位置亦可以插入一些高级的表达式,如 Distinct 。注意,By 是前缀和真正实体属性的分隔符,By 后面可以添加多个属性,用 And 或者 Or 连接。 例如:

    public interface PersonRepository extends Repository<User, Long> { // 可以直接根据另一个关联实体进行查找 List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname); // 查找非重复行 List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname); List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname); // 忽略大小写 List<Person> findByLastnameIgnoreCase(String lastname); List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname); // 按 Firstname 排序 List<Person> findByLastnameOrderByFirstnameAsc(String lastname); List<Person> findByLastnameOrderByFirstnameDesc(String lastname); } 

    属性表达式

    属性表达式( Property Expressions )只能指向被 Repository 管理的实体的属性,但是也可以指向实体的属性的嵌套属性:假设 Person 有一个 Address 属性,Address 有一个 ZipCode 属性,那么我们可以根据 ZipCode 来查找 Person

    List<Person> findByAddressZipCode(ZipCode zipCode); 

    这会判断 x.address.zipCode 是否和 zipCode 相等(根据 ZipCode 的主键来判断 )。具体的解析策略是: Spring 检测到整个属性表达式 AddressZipCode ( By 后面) ,然后判断实体类是否有 addressZipCode 这个属性,如果有,则算法停止,使用该属性;否则,算法根据驼峰命名规则,从右边将该属性分为“头”和“尾”两部分,例如 AddressZipCode。如果实体类拥有“头”所指示的属性,然后 Spring 会从此处开始,按照刚才的方法将“尾”拆分,来递归判断是否属于“头”的属性。如果第一次拆分实体类没有“头”所指示的属性,则算法左移一步,拆分为 AddressZipCode 然后继续。

    大所述情况下该算法都会正常工作,但也有例外。比如 PersonaddressZip 属性,但是 AddressZip 没有 code 属性,因此就会在第一次分割后失败。这种情况下,我们可以使用 _ 显示分割:

    List<Person> findByAddress_ZipCode(ZipCode zipCode); 

    其他

    我们还可以传 PageableSort 参数来实现分页和排序:

    Page<User> findByLastname(String lastname, Pageable pageable); Slice<User> findByLastname(String lastname, Pageable pageable); List<User> findByLastname(String lastname, Sort sort); List<User> findByLastname(String lastname, Pageable pageable); 

    还可以使用 first 或者 top 限制返回结果的个数:

    User findFirstByOrderByLastnameAsc(); User findTopByOrderByAgeDesc(); Page<User> queryFirst10ByLastname(String lastname, Pageable pageable); Slice<User> findTop3ByLastname(String lastname, Pageable pageable); List<User> findFirst10ByLastname(String lastname, Sort sort); List<User> findTop10ByLastname(String lastname, Pageable pageable); 

    可以返回 Java 8 的 Stream 对象:

    @Query("select u from User u") Stream<User> findAllByCustomQueryAndStream(); Stream<User> readAllByFirstnameNotNull(); @Query("select u from User u") Stream<User> streamAllPaged(Pageable pageable); 

    注意,使用 Stream 之后必须手动关闭,或者这样写:

    try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) { stream.forEach(…); } 

    还可以返回异步加载的对象:

    //使用 java.util.concurrent.Future 作为返回对象. @Async Future<User> findByFirstname(String firstname); //使用 Java 8 java.util.concurrent.CompletableFuture 作为返回对象. @Async CompletableFuture<User> findOneByFirstname(String firstname); //使用 org.springframework.util.concurrent.ListenableFuture 作为返回对象. @Async ListenableFuture<User> findOneByLastname(String lastname); 

    附录

    方法名支持的关键字

    看原文吧, V2EX 不支持表格?

    方法支持的返回值

    看原文吧, V2EX 不支持表格?

    参考

    1. Java Persistence/What is JPA?
    2. Spring Data JPA - Reference Documentation
    3. Querydsl Reference Guide
    9 条回复    2017-10-01 13:02:45 +08:00
    kimown
        1
    kimown  
       2017-03-27 22:01:17 +08:00
    可以用 Lombok 简化 bean 的 getter setter 的写法
    lufficc
        2
    lufficc  
    OP
       2017-03-27 22:13:30 +08:00
    @kimown 嗯。。。 DEMO 演示,没用
    crytis
        3
    crytis  
       2017-03-28 00:08:32 +08:00
    mybatis 跟这个有啥区别
    Cbdy
        4
    Cbdy  
       2017-03-28 09:37:47 +08:00
    @kimown 之前和朋友交流过 Lombok ,他表示这东西用了非公开的 api ,有风险,你怎么看?
    hantsy
        5
    hantsy  
       2017-03-28 10:19:19 +08:00
    Repository , CrudRepository , PagingAndSortingRepository 来自 Spring Data Commons 包, Spring Data JPA 有另外一个 JPARepository 接口。。。

    Spring Data JPA 支持 Type Safe 查询,支持 QueryDSL ,和自定义的 Specifcation 接口(基于 JPA 2.0 Criteria API )。

    https://github.com/hantsy/spring4-sandbox/tree/master/data-jpa/src/main/java/com/hantsylabs/example/spring/jpa/spec

    以前写过一些 Spring Data 操作支持, https://github.com/hantsy/spring-sandbox/wiki
    lufficc
        7
    lufficc  
    OP
       2017-03-28 12:30:49 +08:00
    @hantsy Specifcation 是有一套标准,但是投影,表连接这些操作可以吗?
    nightYe
        8
    nightYe  
       2017-03-29 09:38:36 +08:00
    对于复杂的 sql 怎么处理,比如说业务需要关联 3 张表
    jack80342
        9
    jack80342  
       2017-10-01 13:02:45 +08:00
    最近翻译了 Spring Boot2.0 的英文文档: https://www.gitbook.com/book/jack80342/spring-boot/details
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     4912 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 30ms UTC 09:51 PVG 17:51 LAX 02:51 JFK 05:51
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86