创建索引
【针对的是mysql8.0版本】
jpa默认建表引擎为: MyISAM,字符集为utf8mb4。如果需要改成innodb,可以添加配置
spring.jpa.database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
MyISAM对单个字段索引长度是小于1000bytes(字符集为utf8mb4时varchar长度小于250),否则创建索引失败
innodb 对单个字段索引长度是小于3072bytes()字符集为utf8mb4时varchar长度小于250),否则创建索引失败。
必看的参考文章
【spring-data-jpa 介绍 复杂查询,包括多表关联,分页,排序等】
https://blog.csdn.net/xusheng__zhang/article/details/78485136
【Spring data jpa 复杂动态查询方式总结】https://blog.csdn.net/qq_30054997/article/details/79420141?utm_source=app
jpa注解踩坑
如果给该列加了索引,那么改列的字节数不能超过1000字节,utf而言:一个汉字占3字节,utfmb4,一个汉字占4字节,所以该列的varchar长度不能超过250,varchar(200)表示可以存200个汉字
@Index(name = "original_file_name_index", columnList = "original_file_name"),
类似这样的注解:该注解只在首次创建时生效(original_file_name不存在,首次传键时长度100会生效,创建好后,你在该长度为200时没有用的,这是只能通过写sql去该)
@Column(name = "original_file_name", nullable = false, length = 100)
private String originalFileName;
原理
底层就是动态拼接sql来实现的
public interface UserRepository extends JpaRepository<User, Integer>{}
@Autowired
UserRepository userRepository;
这里UserRepository是一个接口,为什么可以直接调用,原理就是在运行的时候,通过cglib,会自动用SimpleJpaRepository这个类去作为UserRepository的实现类,如果你还自定义了UserRepositoryImpl,那么在运行的时候,UserRepositoryImpl就是SimpleJpaRepository的子类,这样我们就不用一个个的重新实现JpaRepository里面的接口了,SimpleJpaRepository的代码大致如下:[这个类包装了entityManager,底层的调用关系还是entityManager在进行CRUD]
public class SimpleJpaRepository<T, ID extends Serializable> implements JpaRepository<T, ID>,
JpaSpecificationExecutor<T>
查询之两大杀手锏
- findByXXX【它会自动翻译成sql】
- 动态查询
参数是一个接口/方法---->这种方式称之为回调/策略模式。
【一个加法函数】:
如果参数是非接口类,那么你传入整数a,b,返回的结果一定是整数a+b的值—>这个方法是完整的,你可以直接用
如果参数是接口类,那么你传入整数a,b,返回的结果可能是整数a+b的值,也可能是a*b,a-b的值,这样看你在传入这个接口的时候你是如何重写这个方法的。—>这个方法是不完整的,你在调用它之前会强制你去完善这个方法,然后就和一般方法调用没什么两样了
分页查询
/**
* 根据appID分页查询文件
*/
@PostMapping("/base/file/pageByAppcode")
public Page<FileEntity> pageByAppcode(@RequestBody Request.FilePageQuery filePageQuery){
Pageable pageable = PageRequest.of(filePageQuery.getPageNo(), filePageQuery.getPageSize(),
Sort.by(Sort.Direction.valueOf(filePageQuery.getOrder()), filePageQuery.getSortBy()));
return fileRepository.findByAppCode(filePageQuery.getAppCode(),pageable);
//return fileRepository.findByAppCodeAndFileNameContaining(filePageQuery.getAppCode(),filePageQuery.getFileName(),pageable);
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class FilePageQuery extends PageVo implements Serializable {
@NotEmpty(message = "appCode不能为空")
private String appCode;
private int pageNo;
private int pageSize = 10;
private String order;
private String sortBy;
public int getPageNo() {
return pageNo < 0 ? pageNo = 0 : pageNo;
}
public int getPageSize() {
return pageSize <= 0 ? pageSize = 10 : pageSize;
}
public String getOrder() {
return StringUtils.isEmpty(order) ? "DESC" : order;
}
public String getSortBy() {
return StringUtils.isEmpty(sortBy) ? "update_time" : sortBy;
}
}
@Component
public interface FileRepository extends JpaRepository<FileEntity,Long>, PagingAndSortingRepository<FileEntity, Long> {
Page<FileEntity> findByAppCode(String appCode, Pageable pageable);
}
结果:
{
"content": [
{
"id": 788002134103687168,
"fileName": "action_20201214111909.jpg",
"originalFileName": "action.jpg",
"fileSize": 23428,
"fileType": "image/jpeg",
}
],
"pageable": {
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"offset": 10,
"pageSize": 10,
"pageNumber": 1,
"unpaged": false,
"paged": true
},
"totalPages": 2,
"totalElements": 18,
"last": true,
"number": 1,
"size": 10,
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"numberOfElements": 8,
"first": false,
"empty": false
}
模型如下:
- 动态分页查询
/**
* 根据appID分页查询文件
* pageNo从0开始的
*/
@PostMapping("/base/file/pageByAppcode")
public Page<FileEntity> pageByAppcode(@RequestBody Request.FilePageQuery filePageQuery){
Pageable pageable = PageRequest.of(filePageQuery.getPageNo(), filePageQuery.getPageSize(),
Sort.by(Sort.Direction.valueOf(filePageQuery.getOrder()), filePageQuery.getSortBy()));
Specification userQuery =new Specification<FileEntity>() {
@Override
public Predicate toPredicate(Root<FileEntity> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicates = new ArrayList<>();
//使用最左前缀匹配,避免索引失效
if (!StringUtils.isEmpty(filePageQuery.getFileName())) {
predicates.add(criteriaBuilder.like(root.get("fileName"), filePageQuery.getFileName()+"%"));
}
if (!StringUtils.isEmpty(filePageQuery.getAppCode())){
predicates.add(criteriaBuilder.equal(root.get("appCode"), filePageQuery.getAppCode()));
}
return criteriaQuery.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
}
};
return fileRepository.findAll(userQuery,pageable);
}
- 建立索引
@Table(name = "bs_file",indexes = {@Index(name = "app_code_index", columnList = "app_code,b_code", unique = false),
@Index(name = "original_file_name_index", columnList = "original_file_name"),
@Index(name = "update_time_index", columnList = "update_time")})
public class FileEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private Long id;
@Column(name = "file_name", nullable = false, length = 300)
private String fileName;
//这个长度设为300,创建索引就会报错:长度大于1000byte,不能创建索引
//utf8mb4字符集里:一个vachar占4字节,300vachar就是1200字节超了
//utf是3字节
@Column(name = "original_file_name", nullable = false, length = 100)
private String originalFileName;
其实现原理:
基于EntityManager(可动态拼接sql)自己封装实现分页查询
public class StudentRepositoryImpl {
@PersistenceContext
private EntityManager em;
@SuppressWarnings("unchecked")
public Page<Student> search(User user) {
String dataSql = "select t from User t where 1 = 1";
String countSql = "select count(t) from User t where 1 = 1";
if(null != user && !StringUtils.isEmpty(user.getName())) {
dataSql += " and t.name = ?1";
countSql += " and t.name = ?1";
}
Query dataQuery = em.createQuery(dataSql);
Query countQuery = em.createQuery(countSql);
if(null != user && !StringUtils.isEmpty(user.getName())) {
dataQuery.setParameter(1, user.getName());
countQuery.setParameter(1, user.getName());
}long totalSize = (long) countQuery.getSingleResult();
Page<User> page = new Page();
page.setTotalSize(totalSize);
List<User> data = dataQuery.getResultList();
page.setData(data);
return page;
}
}
in操作的查询:
在日常手动写sql的时候有in这种查询是比较多的,比如select * from user t where t.id in (1, 2, 3);有人说in的效率不高,要少用,但是其实只要in是主键,或者说是带有索引的,效率是很高的,mysql中如果in是子查询貌似不会走索引,不过我个人经验,在我遇到的实际应用中,in(ids)这种是比较多的,所以一般来说是没有性能问题的。
那么,sql里面比较好写,但是如果使用spring-data-jpa的动态查询方式呢,就和前面的稍微有点区别。大致上是这么一个思路:
if(!CollectionUtils.isEmpty(ids)) {
In<Long> in = cb.in(root.<Long> get("id"));
for (Long id : parentIds) {
in.value(id);
}
query.where(in);
}
cb创建一个in的Predicate,然后给这个in赋值,最后把in加到where条件中。
jpa答疑解惑
需求》性能》开发效率
jpa是完全可以满足需求的,开发效率也是很高的,至于性能问题,需要通过压测才能判断,瓶颈点应该是多表关联的查询优化,jpa有三种方式多表查询,1. 配置pojo的多对多关系,2. 动态sql,3.手写jpl
spring data jpa 通过创建方法名来做查询,只能做简单的查询,那如果我们要做复杂一些的查询呢,多条件分页怎么办,这里,spring data jpa为我们提供了JpaSpecificationExecutor接口,只要简单实现toPredicate方法就可以实现复杂的查询
@Entity
@Table(name = "tb_task")
public class Task {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "project_id")
Project project;
}
//2.多表查询
Join<Task,Project> join = root.join("project", JoinType.INNER);
返回值为对象的意义
是jpa查询表内容返回值基本上都是对象,但是仅仅需要一个字段返回整体对象不是会有很多数据冗余吗,其实大多数情况对一个数据表的查询不可能只有一次或者说这个表不仅仅是这一次会用到,如果我写好一个返回对象的方法,之后都可以直接调用,一般情况下多出一点数据对网络的压力可以忽略不计,而这样对开发效率的提升还是很大的.如果仅仅想得到一部分字段也可以新建一个只有想要字段的Entity.
特别的:在一对多中或者多对一中,即便是fetch为eager,也会先查询主对象,再查询关联对象,但是在eager的情况下虽然是有多次查询问题,但是没有n+1问题,关联对象不会像n+1那样多查询n次,而仅仅是把关联对象一次性查询出来,因此,在企业级应用当中,访问量不大的情况下一般来说没什么问题。
参数是一个function,其实在框架内部就调用了这个参数,于是这个这个方法执行了。这种模式还有一个JDK的排序集合上面也有体现,我们的netty框架也采用这种方式来实现异步IO的能力。
- 建造者模式
使用多个简单的对象一步一步构建成一个复杂的对象
一条sql语句的结构:
-- 第一步执行语句,数据来源和查询条件
from 表名
where 查询条件
group by 分组字段
having 分组指定条件
-- 第二步执行语句
select 查询结果
-- 第三步执行语句,对查询结果显示要求
order by 查询结果排序(升序asc 降序desc)
limit 查询结果取出指定行
这个结构是不变的,里面的条件是变化的,一条sql其实是一个复杂的大对象,它由很多简单的小对象(select,where,from等)构造起来的
EntityManager.getCriteriaBuilder()
动态sql的过程: 创建builder => 创建Query => 构造条件 => 查询
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Student> query = builder.createQuery(Student.class);
Root<Student> root = query.from(Student.class);
Predicate p1 = builder.like(root.<String> get("name"), "%" + student.getName() + "%");
Predicate p2 = builder.equal(root.<String> get("password"), student.getPassword());
query.where(p1, p2);
public class OrgController {
@Autowired
EntityManager entityManager;
public void joinSql(User student){
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
// select *
CriteriaQuery<User> query = builder.createQuery(User.class);
// from user
Root<User> root = query.from(User.class);
// user是主表,left join role on name='zhangsna'
Join<User, Role> roleJoin = root.join("roleList", JoinType.LEFT);
builder.equal(roleJoin.get("name"),"zhangsan");
// 多表关联
Join<Role, Menu> menuJoin = root.join("menuList", JoinType.LEFT);
builder.equal(menuJoin.get("name"),"home");
// where name like '%xx%'
Predicate p1 = builder.like(root.<String> get("name"), "%" + student.getName() + "%");
// and password='xxx'
Predicate p2 = builder.equal(root.<String> get("password"), student.getPassword());
query.where(p1, p2);
//CriteriaQuery 等价于: select * from user where name like '%xx%' and password='xxx'
List<User> userList = entityManager.createQuery(query).getResultList();
}
}
说明:虽然说JPA中这种方式查询会存在着多次级联查询的问题,对性能有所影响,但是在一般的企业级应用当中,为了开发的便捷,这种性能牺牲一般来说是可以接受的。
特别的:在一对多中或者多对一中,即便是fetch为eager,也会先查询主对象,再查询关联对象,但是在eager的情况下虽然是有多次查询问题,但是没有n+1问题,关联对象不会像n+1那样多查询n次,而仅仅是把关联对象一次性查询出来,因此,在企业级应用当中,访问量不大的情况下一般来说没什么问题。
关联查询(实体类实现)
me_article 文章的作者,评论数,标题
me_article_body 文章内容----markdown文本:html文本
me_article_tag 中间表:文章<------------>个人标签/类别
me_category 文章属于应用的哪个种类
me_comment 文章评论内容
me_tag 个人给文章打的标签
sys_user 系统用户
@Entity
@Table(name = "me_article")
public class Article extends BaseEntity<Integer> {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private User author;
@OneToOne(cascade = {CascadeType.ALL}, fetch = FetchType.LAZY, orphanRemoval = true)
@JoinColumn(name = "body_id")
private ArticleBody body;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "me_article_tag",
joinColumns = {@JoinColumn(name = "article_id")},
inverseJoinColumns = {@JoinColumn(name = "tag_id")})
private List<Tag> tags;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "article", orphanRemoval = true)
private List<Comment> comments;
@Column和@GeneratedValue注解
这两个当表字段已经存在时,配置将失效(字段已经生成后你再在字段上添加就会无效,要生效只能删表再重新运行)
@Data
@Entity
@Table(name = "bs_file_config")
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FileConfigEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "anon_down",columnDefinition = "tinyint(4) not null default 1 COMMENT '匿名下载'",nullable = false, length = 4)
private Integer anonDown;
@Column(name = "anon_upload",columnDefinition = "tinyint(4) not null default 1 COMMENT '匿名上传'",nullable = false, length = 4)
private Integer anonUpload;
@Column(name = "app_code",columnDefinition = "varchar(50) not null default 'default' unique", nullable = false, length = 50)
private String appCode="default";
}
整合querydsl
引入依赖
<!-- querydsl -->
<!-- QueryDSL仅仅是一个通用的查询框架,专注于通过Java API构建类型安全的SQL查询-->
<!-- 目前QueryDSL支持的平台包括JPA,JDO,SQL,Mongodb 等等。。。-->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<scope>provided</scope>
</dependency>
@Data
@Entity
@Table(name = "menu")
public class Menu implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String url;
}
点击编译就好了
使用方法
QMenuRole menuRole = QMenuRole.menuRole;
QUserRole userRole = QUserRole.userRole;
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
JPAQuery<Tuple> jpaQuery = queryFactory.select(menuRole,userRole)
.from(menuRole)
.leftJoin(userRole)
.on(userRole.rid.eq(menuRole.rid));
//添加查询条件
jpaQuery.where(userRole.uid.eq(1));
//拿到结果
List<Tuple> userRoleMenuList = jpaQuery.fetch();
userRoleMenuList.stream().map(Tuple::get)
log.info(userRoleMenuList+"--<");
QMenu menu1 =new QMenu("menu1");
QMenu menu2 = new QMenu("menu2");
List<Tuple> menuList = queryFactory.select(menu1, menu2)
.from(menu1)
.innerJoin(menu2)
.on(menu1.id.eq(menu2.parentId))
.where(menu2.enabled.eq(1),menu2.id.in())
.fetch();