SpringBoot系列(6)---SpringBoot-JPA

JPA 应该都熟悉了,我就不多说了什么是JPA了。目前JPA主要实现由hibernate和openJPA等。

Spring Data JPA 是Spring Data 的一个子项目,它通过提供基于JPA的Repository极大了减少了操作JPA的代码。笔者觉得这个由SpringBoot 提供的JPARepository真的是非常爽。基本上大部分的业务都可以满足了。

在Spring环境中需要配置大量了XML配置,但是SpringBoot基本上帮助我们配置好了,我们只需要简单地配置一下DataSource和几项jpa额外的配置就完成了整个配置持久化层的工作。EntityManagerFactory 那些都不用配置了。

常规贴出一下Spring-data-JPA的MAVEN


   org.springframework.boot
   spring-boot-starter-data-jpa


一、数据源和JPA配置

当然我们还是需要在application.properties配置数据源,SpringBoot默认是使用tomcat的数据源(intellij有自动提示,spring.datasource.tomcat开头的配置):

#datasource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_db?charset=utf8mb4&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=tonyyan
spring.datasource.tomcat.max-active=20
spring.datasource.tomcat.test-while-idle=true
spring.datasource.tomcat.validation-query=select 1
spring.datasource.tomcat.default-auto-commit=false
spring.datasource.tomcat.min-idle=15
spring.datasource.tomcat.initial-size=15
最后我们做一些JPA的配置,spring.jpa开头的配置

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jackson.serialization.indent-output=true
当然SpringBoot的maven也没有添加mysql driver的依赖。所以要吃自己:


   mysql
   mysql-connector-java
   6.0.6

二、Entity配置

开始之前我先提供一下我的entity配置,便于理解后面的查询操作,我们使用模仿微博的数据实体:

User类:

@Entity
@Table(name = "users")
public class User {

    public User() {
    }

    public User(String username, String userpwd) {
        this.username = username;
        this.userpwd = userpwd;
    }

    private long userId;
    private String username;
    private String userpwd;

    @JsonIgnore
    private Set weibos;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public long getUserId() {
        return userId;
    }

    public void setUserId(long userId) {
        this.userId = userId;
    }

    @Column(name="username")
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Column(name = "userpwd")
    public String getUserpwd() {
        return userpwd;
    }

    public void setUserpwd(String userpwd) {
        this.userpwd = userpwd;
    }

    @OneToMany(fetch = FetchType.LAZY,cascade = {CascadeType.REMOVE},mappedBy = "user")
    public Set getWeibos() {
        return weibos;
    }

    public void setWeibos(Set weibos) {
        this.weibos = weibos;
    }

    @Override
    public String toString() {
        return "User{" +
                "userId=" + userId +
                ", username='" + username + '\'' +
                ", userpwd='" + userpwd + '\'' +
                '}';
    }
}

Weibo类:

@Entity
@Table(name="weibo")
public class Weibo {

    public Weibo() {
    }

    public Weibo(User user, String weiboText, Date createDate) {
        this.user = user;
        this.weiboText = weiboText;
        this.createDate = createDate;
    }

    private long weiboId;
    private User user;
    private String weiboText;
    private Date createDate;
    private Set comments;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public long getWeiboId() {
        return weiboId;
    }

    public void setWeiboId(long weiboId) {
        this.weiboId = weiboId;
    }

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "user_id")
    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    @Column(name = "weibo_text")
    public String getWeiboText() {
        return weiboText;
    }

    public void setWeiboText(String weiboText) {
        this.weiboText = weiboText;
    }

    @Column(name = "create_date")
    public Date getCreateDate() {
        return createDate;
    }

    public void setCreateDate(Date createDate) {
        this.createDate = createDate;
    }

    @OneToMany(fetch = FetchType.LAZY,cascade = {CascadeType.REMOVE},mappedBy = "weibo")
    public Set getComments() {
        return comments;
    }

    public void setComments(Set comments) {
        this.comments = comments;
    }

    @Override
    public String toString() {
        return "Weibo{" +
                "weiboId=" + weiboId +
                ", user=" + user +
                ", weiboText='" + weiboText + '\'' +
                ", createDate=" + createDate +
                '}';
    }
}

comment评论类:

@Entity
@Table(name = "comments")
public class Comment {

    public Comment() {
    }

    public Comment(User user, Weibo weibo, String commentText, Date commentDate) {
        this.user = user;
        this.weibo = weibo;
        this.commentText = commentText;
        this.commentDate = commentDate;
    }

    private long commentId;
    private User user;
    @JsonIgnore
    private Weibo weibo;
    private String commentText;
    private Date commentDate;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public long getCommentId() {
        return commentId;
    }

    public void setCommentId(long commentId) {
        this.commentId = commentId;
    }

    @ManyToOne
    @JoinColumn(name = "user_id")
    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "weibo_id")
    public Weibo getWeibo() {
        return weibo;
    }

    public void setWeibo(Weibo weibo) {
        this.weibo = weibo;
    }

    @Column(name = "comment_text")
    public String getCommentText() {
        return commentText;
    }

    public void setCommentText(String commentText) {
        this.commentText = commentText;
    }

    @Column(name = "comment_date")
    public Date getCommentDate() {
        return commentDate;
    }

    public void setCommentDate(Date commentDate) {
        this.commentDate = commentDate;
    }

    @Override
    public String toString() {
        return "Comment{" +
                "commentId=" + commentId +
                ", user=" + user +
                ", weibo=" + weibo +
                ", commentText='" + commentText + '\'' +
                ", commentDate=" + commentDate +
                '}';
    }
}

三、强大的Spring JpaRepository

SpringBoot创建DAO层很多种方法其中japrepository是最强大的而且最有特色的一种,我们可以针对不同的实体创建repository接口。Spring会根据方法名称的规则进行自动生成实现,强大的不要不要的。在SpringBoot中默认已经提供了非常多的常规CRUD操作的repository,以下就是Spring为我们提供的repository接口:

@NoRepositoryBean
public interface JpaRepository, ID extends Serializable> extends PagingAndSortingRepository, ID>, QueryByExampleExecutor {
    List findAll();

    List findAll(Sort var1);

    List findAll(Iterable var1);

    extends T> List save(Iterable var1);

    void flush();

    extends T> S saveAndFlush(S var1);

    void deleteInBatch(Iterable var1);

    void deleteAllInBatch();

    T getOne(ID var1);

    extends T> List findAll(Example var1);

    extends T> List findAll(Example var1, Sort var2);
}

@NoRepositoryBean
public interface PagingAndSortingRepository, ID extends Serializable> extends CrudRepository, ID> {
    Iterable findAll(Sort var1);

    Page findAll(Pageable var1);
}

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

    extends T> Iterable save(Iterable var1);

    T findOne(ID var1);

    boolean exists(ID var1);

    Iterable findAll();

    Iterable findAll(Iterable var1);

    long count();

    void delete(ID var1);

    void delete(T var1);

    void delete(Iterableextends T> var1);

    void deleteAll();
}

这些操作完全不用我们去实现,这些不是我们以往在普通的Spring项目中自己所定义的BaseDao吗?SpringBoot真的是非常体贴,大大减低了我们的工作量。但是更为强大的还在后面。我们通过继承JpaRepository接口,除了可以获得上面的基础CRUD操作方法之外,还可以通过Spring规定的接口命名方法自动创建复杂的CRUD操作,以下是我在Spring Data JPA 文档中找到的命名规则表:

Keyword Sample JPQL snippet

And

findByLastnameAndFirstname

… where x.lastname = ?1 and x.firstname = ?2

Or

findByLastnameOrFirstname

… where x.lastname = ?1 or x.firstname = ?2

Is,Equals

findByFirstname,findByFirstnameIs,findByFirstnameEquals

… where x.firstname = ?1

Between

findByStartDateBetween

… where x.startDate between ?1 and ?2

LessThan

findByAgeLessThan

… where x.age < ?1

LessThanEqual

findByAgeLessThanEqual

… where x.age <= ?1

GreaterThan

findByAgeGreaterThan

… where x.age > ?1

GreaterThanEqual

findByAgeGreaterThanEqual

… where x.age >= ?1

After

findByStartDateAfter

… where x.startDate > ?1

Before

findByStartDateBefore

… where x.startDate < ?1

IsNull

findByAgeIsNull

… where x.age is null

IsNotNull,NotNull

findByAge(Is)NotNull

… where x.age not null

Like

findByFirstnameLike

… where x.firstname like ?1

NotLike

findByFirstnameNotLike

… where x.firstname not like ?1

StartingWith

findByFirstnameStartingWith

… where x.firstname like ?1 (parameter bound with appended %)

EndingWith

findByFirstnameEndingWith

… where x.firstname like ?1 (parameter bound with prepended %)

Containing

findByFirstnameContaining

… where x.firstname like ?1 (parameter bound wrapped in %)

OrderBy

findByAgeOrderByLastnameDesc

… where x.age = ?1 order by x.lastname desc

Not

findByLastnameNot

… where x.lastname <> ?1

In

findByAgeIn(Collection ages)

… where x.age in ?1

NotIn

findByAgeNotIn(Collection age)

… where x.age not in ?1

True

findByActiveTrue()

… where x.active = true

False

findByActiveFalse()

… where x.active = false

IgnoreCase

findByFirstnameIgnoreCase

… where UPPER(x.firstame) = UPPER(?1)

以下就是我写的UserRepository接口,后面的WeiboRepository还有更加复杂的操作:

public interface UserRepository extends JpaRepository,Long> {

    //查询用户名称包含username字符串的用户对象
    List findByUsernameContaining(String username);

    //获得单个用户对象,根据username和pwd的字段匹配
    User getByUsernameIsAndUserpwdIs(String username,String pwd);

    //精确匹配username的用户对象
    User getByUsernameIs(String username);
} 

什么annotation都不用打,也不需要写实现。直接使用即可:

@Controller
public class TestController {

    @Autowired
    private UserRepository userRepository;

    @RequestMapping("/searchUser/{username}")
    public @ResponseBody
    List searchUser(@PathVariable("username") String username) {
        List result = this.userRepository.findByUsernameContaining(username);
        return result;
    }

}

方便到不可相信,如果在intellij还可以有提示,根本不需要记住命名规则,只要你懂点英文基本上intellij给你的提示就足够了。

Spring提供了非常大的自由度给开发者,我们可以在接口方法中通过定义@Query annotation自定义接口方法的JPQL语句,其实笔者对JPQL并不熟悉,但是笔者对HQL非常熟悉,其实笔者感觉两者基本上并没有什么差别,笔者在JPA开发过程当中直接都是在写JPQH的语句当中使用HQL也正常无误。以下是WeiboRepository的接口:

public interface WeiboRepository extends JpaRepository,Long>,JpaSpecificationExecutor {

    @Query("select w from Weibo w where w.user.username = :username")
    List searchUserWeibo(@Param("username") String username);

    @Query("select w from Weibo w where w.user.username = :username")
    List searchUserWeibo(@Param("username") String username, Sort sort);

    @Modifying
    @Transactional(readOnly = false)
    @Query("update Weibo w set w.weiboText = :text where w.user = :user")
    int setUserWeiboContent(@Param("text")String weiboText,@Param("user")User user);

    Page findByUserIsAndWeiboTextContaining(User user, String weiboText, Pageable pageable);
@Transactional( readOnly = false) int deleteByUser(User user) ;}

除了我们刚刚说过的@Query annotation之外,我这里还是用了Spring Data JPA提供的Sort和Pageable。暂时忽略JpaSpecificationExecutor接口的继承

Sort提供字段排序的功能,而Pageable则提供分页的功能。需要注意的是,如果使用了Pageable返回值是Page的泛型对象,这个对象里面提供的分页信息相当丰富。足够使用。只要方法有Pageable参数就会自动实现分页,同时将总页面数和总对象数等信息都通过page对象一一返回。

以下是searchUserWeibo方法、findByUserIsAndWeiboTextLike方法的使用:

@RequestMapping("/username/{username}")
public List getUserWeibo(@PathVariable("username") String username) {
    return this.weiboRepository.searchUserWeibo(username,new Sort(new Sort.Order(Sort.Direction.DESC,"weiboId")));
}
Sort的使用非常简单,可以看到上述代码当中可以使用多个order进行排序。

@RequestMapping("/simpleSearch")
    public Page simpleSearch(String username,String weiboText,int pageNo,int pageSize){
        User user = this.userRepository.getByUsernameIs(username);
        return this.weiboRepository.findByUserIsAndWeiboTextContaining(user,weiboText,new PageRequest(pageNo,pageSize));
    }
Pageable是一个接口,而上述使用到的PageRequest是一个实现类。这里的pageNo是从0开始。Page的数据结构如下,以下是返回的Json字符串:

{
    "content": [
        {
            "weiboId": 4,
            "user": {
                "userId": 1,
                "username": "TONY",
                "userpwd": "TONYYAN"
            },
            "weiboText": "TONY's WEIBO_D!!!",
            "createDate": 1498751667000,
            "comments": []
        },
        {
            "weiboId": 5,
            "user": {
                "userId": 1,
                "username": "TONY",
                "userpwd": "TONYYAN"
            },
            "weiboText": "TONY's WEIBO_E!!!",
            "createDate": 1498751667000,
            "comments": []
        }
    ],
    "last": true,
    "totalElements": 5,
    "totalPages": 2,
    "number": 1,
    "size": 3,
    "sort": null,
    "first": false,
    "numberOfElements": 2
}
重点!!!当笔者尝试在命名式方法传入为空的对象时,Spring并没有自动排除为空的字段条件。

或者你可能会发现了如下的接口方法:

@Transactional(readOnly = false)
int deleteByUser(User user);
这个也是Spring提供的命名实现,我是根据intellij的提示打出来的,但是这个会发出很多条SQL:

Hibernate: select user0_.user_id as user_id1_2_, user0_.username as username2_2_, user0_.userpwd as userpwd3_2_ from users user0_ where user0_.username=?
Hibernate: select weibo0_.weibo_id as weibo_id1_3_, weibo0_.create_date as create_d2_3_, weibo0_.user_id as user_id4_3_, weibo0_.weibo_text as weibo_te3_3_ from weibo weibo0_ left outer join users user1_ on weibo0_.user_id=user1_.user_id where user1_.user_id=?
Hibernate: select comments0_.weibo_id as weibo_id5_0_0_, comments0_.comment_id as comment_1_0_0_, comments0_.comment_id as comment_1_0_1_, comments0_.comment_date as comment_2_0_1_, comments0_.comment_text as comment_3_0_1_, comments0_.user_id as user_id4_0_1_, comments0_.weibo_id as weibo_id5_0_1_, user1_.user_id as user_id1_2_2_, user1_.username as username2_2_2_, user1_.userpwd as userpwd3_2_2_ from comments comments0_ left outer join users user1_ on comments0_.user_id=user1_.user_id where comments0_.weibo_id=?
Hibernate: select comments0_.weibo_id as weibo_id5_0_0_, comments0_.comment_id as comment_1_0_0_, comments0_.comment_id as comment_1_0_1_, comments0_.comment_date as comment_2_0_1_, comments0_.comment_text as comment_3_0_1_, comments0_.user_id as user_id4_0_1_, comments0_.weibo_id as weibo_id5_0_1_, user1_.user_id as user_id1_2_2_, user1_.username as username2_2_2_, user1_.userpwd as userpwd3_2_2_ from comments comments0_ left outer join users user1_ on comments0_.user_id=user1_.user_id where comments0_.weibo_id=?
Hibernate: select comments0_.weibo_id as weibo_id5_0_0_, comments0_.comment_id as comment_1_0_0_, comments0_.comment_id as comment_1_0_1_, comments0_.comment_date as comment_2_0_1_, comments0_.comment_text as comment_3_0_1_, comments0_.user_id as user_id4_0_1_, comments0_.weibo_id as weibo_id5_0_1_, user1_.user_id as user_id1_2_2_, user1_.username as username2_2_2_, user1_.userpwd as userpwd3_2_2_ from comments comments0_ left outer join users user1_ on comments0_.user_id=user1_.user_id where comments0_.weibo_id=?
Hibernate: select comments0_.weibo_id as weibo_id5_0_0_, comments0_.comment_id as comment_1_0_0_, comments0_.comment_id as comment_1_0_1_, comments0_.comment_date as comment_2_0_1_, comments0_.comment_text as comment_3_0_1_, comments0_.user_id as user_id4_0_1_, comments0_.weibo_id as weibo_id5_0_1_, user1_.user_id as user_id1_2_2_, user1_.username as username2_2_2_, user1_.userpwd as userpwd3_2_2_ from comments comments0_ left outer join users user1_ on comments0_.user_id=user1_.user_id where comments0_.weibo_id=?
Hibernate: select comments0_.weibo_id as weibo_id5_0_0_, comments0_.comment_id as comment_1_0_0_, comments0_.comment_id as comment_1_0_1_, comments0_.comment_date as comment_2_0_1_, comments0_.comment_text as comment_3_0_1_, comments0_.user_id as user_id4_0_1_, comments0_.weibo_id as weibo_id5_0_1_, user1_.user_id as user_id1_2_2_, user1_.username as username2_2_2_, user1_.userpwd as userpwd3_2_2_ from comments comments0_ left outer join users user1_ on comments0_.user_id=user1_.user_id where comments0_.weibo_id=?
Hibernate: delete from comments where comment_id=?
Hibernate: delete from comments where comment_id=?
Hibernate: delete from comments where comment_id=?
Hibernate: delete from comments where comment_id=?
Hibernate: delete from comments where comment_id=?
Hibernate: delete from comments where comment_id=?
Hibernate: delete from comments where comment_id=?
Hibernate: delete from comments where comment_id=?
Hibernate: delete from comments where comment_id=?
Hibernate: delete from comments where comment_id=?
Hibernate: delete from weibo where weibo_id=?
Hibernate: delete from weibo where weibo_id=?
Hibernate: delete from weibo where weibo_id=?
Hibernate: delete from weibo where weibo_id=?
Hibernate: delete from weibo where weibo_id=?

它会一条条的发SQL去删。


在当然@Query不止是查询语言也可以用于更新和删除语句,不过需要额外添加@Modifying annotation,由于实验环境我就直接打上@Transactional声明式事务,在正式的开发环境请在你的业务层上写@Transactional,当然你也可以都写上,这样会按照你配置的或者是默认的事务传播机制进行事务传播:

@Modifying
@Transactional(readOnly = false)
@Query("update Weibo w set w.weiboText = :text where w.user = :user")
int setUserWeiboContent(@Param("text")String weiboText,@Param("user")User user);

除了@Query annotation 还可以在Entity类中使用@NamedQuery,定义查询语句。但是笔者觉得还是不及@Query来得直接方便。

@Entity
@Table(name = "users")
@NamedQuery(name = "User.searchUserName",query = "select u from User u where u.username like :username")
public class User {
UserRepository接口定义searchUserName方法即可:

public interface UserRepository extends JpaRepository,Long> {

    List searchUserName(@Param("username") String username);


四、JpaSpecificationExecutor接口

还记得刚刚暂时忽略的JpaSpecificationExecutor接口继承吗?在WeiboRepository接口中继承JpaSpecificationExecutor接口就可以在外部使用Spring提供的Criteria查询,但是这个查询也存在一些问题~  以下就是一段范例:

@RequestMapping("/searchWeibo")
    public Page searchWeibo(final String username, final String weiboText, final Date startDate, final Date endDate,int pageNo,int pageSize) {
        Page page = this.weiboRepository.findAll(new Specification() {
            @Override
            public Predicate toPredicate(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) {
                List predicates = new LinkedList<>();

                if (!StringUtils.isEmpty(username)) {
                    //Join有两种方式                   
//                    Join userJoin = root.join("user",JoinType.INNER);
//                    predicates.add(criteriaBuilder.equal(userJoin.get("username"), username));
                    predicates.add(criteriaBuilder.equal(root.get("user").get("username"),username));
                }
                if (!StringUtils.isEmpty(weiboText)) {
                    predicates.add(criteriaBuilder.like(root.get("weiboText"), "%" + weiboText + "%"));
                }
                if(startDate!=null){
                    predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("createDate").as(Date.class),startDate));
                }
                if(endDate != null){
                    predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get("createDate").as(Date.class),endDate));
                }
                return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
            }
        },new PageRequest(pageNo,pageSize));
        return page;
    }
可以直接使用findAll方法并创建匿名内部类继承Specification即可,实现toPredicate方法返回最终的条件,但是笔者想在这里使用JoinFetch,通过很多方法也未能在toPredicate实现,希望其他大牛走过路过,留下在这里使用joinFetch的方法。


五、普通的DAO

当然我们可以使用最为基础的方法去写DAO。直接@Repository 然后@PersistenceContext。最原始最基本~

@Repository
public class WeiboDao {

    @PersistenceContext
    private EntityManager entityManager;


    @Transactional(readOnly = true)
    public List searchWeiboByEm(String username, String weiboText, Date startDate, Date endDate, int pageNo, int pageSize) {
        StringBuffer jpql = new StringBuffer("select w from Weibo w join fetch w.user u left join fetch w.comments c where 1=1 ");
        Map,Object> paramMap = new HashMap<>();
        if(!StringUtils.isEmpty(username)){
            jpql.append(" and u.username = :username");
            paramMap.put("username",username);
        }
        if(!StringUtils.isEmpty(weiboText)){
            jpql.append(" and w.weiboText like :weiboText");
            paramMap.put("weiboText","%"+weiboText+"%");
        }
        if(startDate!=null){
            jpql.append(" and w.createDate >= :startDate");
            paramMap.put("startDate",startDate);
        }
        if(endDate != null){
            jpql.append(" and w.createDate <= :endDate");
            paramMap.put("endDate",endDate);
        }

        Query query = entityManager.createQuery(jpql.toString());
        Set keys = paramMap.keySet();
        for (String keyItem : keys) {
            query.setParameter(keyItem,paramMap.get(keyItem));
        }
        return query.setFirstResult(pageNo*pageSize).setMaxResults(pageSize).getResultList();
    }

}

六、声明式事务

SpringBoot默认已经帮我们自动配置好了JPATransaction,我们直接使用即可。

直接在Service层测试@Transactional,当然正常的事务隔离级别还有事务传递方式还是和以前Spring一样配置。注意这个@Transactional是Spring提供的,不要错误的使用JPA的@Transactional

@Service
public class WeiboService {

    @Autowired
    private WeiboRepository weiboRepository;

    @Transactional(readOnly = false,isolation = Isolation.READ_COMMITTED)
    public List importWeiboList(List weibos, User user){
        int index = 0;
        Date nowDateTime = new Date(System.currentTimeMillis());
        for (Weibo weiboItem: weibos) {
            weiboItem.setUser(user);
            weiboItem.setCreateDate(nowDateTime);
            if(5<=index++){
                throw new RuntimeException("Weibo out of limit!!!");
            }
            this.weiboRepository.save(weiboItem);
        }
        return weibos;
    }

}

你可能感兴趣的:(SpringBoot系列(6)---SpringBoot-JPA)