V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lufficc
V2EX  ›  程序员

Spring Boot JPA - 基本使用

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

    原文地址: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 save(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   ·   实用小工具   ·   5670 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 01:32 · PVG 09:32 · LAX 17:32 · JFK 20:32
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.