spring data jpa 实战之增删改查(干货!你想要的查询!)

熟悉了mybatis的灵活,可能你对他的动态查询很喜欢,表示各种sql都能胜任。初步接触jpa时,你会各种吐槽,不如mybatis来的方便。其实jpa也能帮你完成你的各种需求,至于编写的复杂度,那可能就仁者见仁智者见智了。习惯了,其实也一样了。

代码放github和码云了:spring-data/github spring-data/码云

save操作(含merge操作,即update也在save里)
  • save方法会预检查该entity是否持久化,isNew会判断该对象的Id类型 是否实现Persistable或EntityInformation进行重写isNew方法,如果Id是Number类型,直接判断value==0 true 执行entityManager.persist 否则执行entityManager.merge()
  /**
   * save方法会预检查该entity是否持久化,isNew会判断该对象的Id类型 是否实现Persistable或EntityInformation进行
   * 重写isNew方法,如果Id是Number类型,直接判断value==0 true 执行entityManager.persist 否则执行entityManager.merge()
   */
  @Test
  public void insert() {
    Customer customer = new Customer();
    customer.setName("lk");
    customer.setEmail("spring.jpa@163.com");
    customer.setAddress("Shanghai PuDong Area XueYe Road");
    customer.setPhone("13699999999");
    //这里保存以后customer的id会被填充为保存后entity的id
    Customer savedEntity = customerRepository.save(customer);
    //保存并立即刷新数据库,由于customer以及提供id,会执行merge方法进行保存
//    Customer savedAndFlush = customerRepository.saveAndFlush(customer);
    List<Customer> batchCustomers = Arrays.asList(new Customer(), new Customer());
    //批量保存,saveAll是循环单挑插入,并不是batch操作,数据较大使用时请注意性能
//    List<Customer> batchSaves = customerRepository.saveAll(batchCustomers);
  }
delete操作
  • delte操作会先执行查询(除了batch操作),在执行删除,若查询不到结果,抛出异常(EmptyResultDataAccessException)不执行删除
  /**
   * 删除操作,除了batch操作,其他方法均先查询后删除
   */
  @Test
  public void delete() {
    //select * from customer where id=?;delete from customer where id=?;
    //同delete(entity)
    customerRepository.deleteById(38L);
    //select * from customer;循环遍历id单个删除...delete from customer where id=?...
    customerRepository.deleteAll();
    Customer customer = new Customer();
    customer.setId(Long.valueOf(42L));
    Customer customerOther = new Customer();
    customerOther.setId(41L);
    List<Customer> deleteAll = Arrays.asList(customer,customerOther);
    //循环执行delete(entity)
    customerRepository.deleteAll(deleteAll);
    //不查询直接:delete from customer;(风险较大清空表)
    customerRepository.deleteAllInBatch();
    //不查询直接:delete from customer where id=? or id=?
    customerRepository.deleteInBatch(deleteAll);
  }

最常用的query操作

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<Age> ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection<Age> ages) … 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)
单表字段查询
    //select * from customer;
    customerRepository.findAll();
    //select * from customer where id = 1;
    customerRepository.findById(1L);
    //select * from customer where address = "address";
    customerRepository.findCustomerByAddress("address");
    //select * from customer where name = "lk" and phone = "133";
    customerRepository.findCustomersNameAndPhone("133", "lk");
    //select * from customer where name like '%k';
    customerRepository.findCustomersNameLike("k");
    //select * from customer where name like 'k'; 如果需要模糊查询需要手动拼接 % 连接符
    customerRepository.findCustomersByNameLike("k");
    //select * from customer where name like "%l";
    customerRepository.findCustomersByNameStartingWith("l");
    //select * from customer where name like "%k%";
    customerRepository.findCustomersByNameContains("k");
    / /.....还有很多,不再一一列举......
分页,排序
    //select * from customer order by name desc;
    customerRepository.findAll(Sort.by(Direction.DESC, "name"));
    //select * from customer limit 0,10;
    customerRepository.findAll(PageRequest.of(0, 10));
example查询(场景较少)
    Customer customer = new Customer();
    customer.setAddress("address");
    //select * from customer where address ="address";
    customerRepository.findAll(Example.of(customer));
    customer.setName("lk");
    customer.setPhone("133");
    ExampleMatcher matcher = ExampleMatcher.matching()
        .withMatcher("name", match -> match.contains())
        .withMatcher("phone", match -> match.startsWith());
    //select * from customer where name like '%lk%" and phone like '133%' and address = "address";
    customerRepository.findOne(Example.of(customer, matcher));
namedQuery 也是自定义的@Query的一种
//entity
@Entity(name = "Customer")
@NamedQuery(name = "Customer.findByNameNQ", query = "select c from Customer c where name =?1")
public class Customer {}

//repository定义方法findByNameNQ
List<Customer> findByNameNQ(String name);

//test
//namedQuery:select * from customer where name = "lk";
customerRepository.findByNameNQ("lk");
@Query自定义JQL语句查询,语法跟sql类似,但注意基于entity的命名,如果属性nativeQuery为ture则,必须用原生sql语句
  @Query("select c from Customer c where name like %?1")
  List<Customer> findCustomersNameLike(String name);

  @Query("select c from Customer c where name = :name and phone = :phone")
  List<Customer> findCustomersNameAndPhone(@Param("phone") String phone,
      @Param("name") String name);

  @Query(value = "select * from customer where name =?1",nativeQuery = true)
  List<Customer> findByNameSql(String name);
@Modify 配合 @Query实现 修改部分字段
  @Modifying
  @Query("update Customer c set c.name = :name where c.id = :id")
  int modifyByPhone(@Param("name") String name,@Param("id") Long id);

  @Modifying
  @Query("delete from Customer c  where c.id = ?1")
  int deleteCustomer(Long id);

自定义返回值,基于JQL语法,在拼装返回结果集时,是根据构造函数进行组装的,可以基于接口或者类,要保证属性是entity内的属性。也可以借助@Query,使用 new map()返回map,或者new Class返回想要的结果。

  //repository

  List<NameOnlyI> findCustomersByName(String name);

  List<NameOnly> findByName(String name);

  @Query("select new com.spring.jpa.beans.NameOnly(name,address) from Customer where name = ?1")
  List<NameOnly> findByName4Obj(String name);

  @Query("select new map(name as myname,address as myaddress) from Customer where name = :name")
  List<Map<String, Object>> findByName4Map(@Param("name") String name);

//test

    //基于接口的返回值
    List<NameOnlyI> interfaces = customerRepository.findCustomersByName("lk1");
    //基于类的返回值,如果有两个构造函数会报错,无法解析转换
    List<NameOnly> nameOnlies = customerRepository.findByName("lk1");
    //基于类的返回值,@Query显式声明返回bean
    List<NameOnly> objs = customerRepository.findByName4Obj("lk1");
    //@Query返回map 用as做key,不用as默认key是0,1,2...
    List<Map<String, Object>> maps = customerRepository.findByName4Map("lk1");

复杂关联关系查询 @OneToOne @ManyToMany @ManyToOne @OneToMany

customer 顾客表,和customer_group 多对一
customer_group 顾客分组表
book 书籍表,customer是多对多。
book_detail 书籍详细表,和book是一对一

  • 四张表没任何业务,假象出来的,单纯为了验证jpa查询方式
表关系注解参数(@OneToOne @ManyToMany @ManyToOne @OneToMany )
Cascade 级联操作
CascadeType. PERSIST 级联持久化 ( 保存 ) 操作
CascadeType. MERGE 级联更新 ( 合并 ) 操作
CascadeType. REFRESH 级联刷新操作,只会查询获取操作
CascadeType. REMOVE 级联删除操作
CascadeType. ALL 级联以上全部操作
  • Fetch
    抓取是否延迟加载,默认情况一的方为立即加载,多的一方为延迟加载,可以手动指定Fetch.EAGER/Fetch.LAZY
  • mappedBy
    关联关系由此方属性维护,可以理解成一对注解使用mappedBy的一方由另一方维护,且必须是注解作用下的属性名。
可以根据关联表的属性作为条件查询,结果同样是根据两次sql查询出来的。通过关联表的属性进行查询时,使用关联 entityName_columnName方式,或者直接使用 _columnName进行查询。如果不想级联查询时,在一方不适用注解即可。

@OneToOne

一对一关联关系,有三种形式存在:

  • 两张表共享主键pk,使用@PrimaryKeyJoinColumn来建立关联关系
@Entity
public class A {
  @Id
  private Long id;
  @OneToOne(cascade = CascadeType.ALL)
  @PrimaryKeyJoinColumn
  private B b;
}

@Entity
public class B {
  @Id
  private Long id;
}
  • 通过中间表建立关联关系,使用@JoinTable注解,joinColumns指定本表和关联表的外键,inverseJoinColumns指定关联关系另一方和关联表的外键
@Entity
public class A {
  @Id
  private Long id;
  @OneToOne(cascade = CascadeType.ALL)
  joinColumns = @JoinColumn(name="a_fk"),
  inverseJoinColumns = @JoinColumn(name="b_fk")
  private B b;
}

@Entity
public class B {
  @Id
  private Long id;
  @OneToOne(mappedBy="b")
  private A a;
}
  • 通过外键,唯一约束指定关联关系,使用@JoinColumn注解,如果不写该注解,默认会在此表中自动创建连接列:主表属性_关联表主键名称
@Entity(name = "book")
public class Book {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;
  private Integer count;

  @OneToOne(cascade = CascadeType.ALL)
  @JoinColumn(name = "detail_id",referencedColumnName = "id")
//referencedColumnName 不写默认是主键,当不是主键是可以用此声明,但必须保证连接键在连接表是唯一约束的
  private BookDetail bookDetail;

//setter getter
}

@Entity(name = "book_detail")
public class BookDetail {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String size;
//不需要依赖book可以不写关联属性

//setter getter
}

public interface BookRepository extends JpaRepository<Book, Long> {

  //可以查出关联表的实体,结果是根据两次sql查询出来的,即select * from book where name = ?;->查出detail_id->select * from book_detail where id = ?;
  List<Book> findByName(String name);
  //可以根据关联表的属性作为条件查询,结果同样是根据两次sql查询出来的。通过关联表的属性进行查询时,使用关联 entityName_columnName方式,或者直接使用 _columnName进行查询。
  List<Book> findByNameAndBookDetail_Id(String name, Long id);

  //自定义返回值,一次查询返回结果
  @Query("select new com.spring.jpa.beans.BookResult(b.id as id,b.name as name ,d.size as size) from Book as b left join BookDetail as d on b.bookDetail = d.id")
  List<BookResult> findResults();

}
@ManyToMany
  • 通过@ManyToMany 注解定义多对多关系,同时通过 @JoinTable 注解描述关联表和关联条件。
// 维护端注解
@Entity
public class A {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @ManyToMany (cascade = CascadeType.REFRESH)
  @JoinTable (
           name =  "a_b" , //关联表名
           inverseJoinColumns =  @JoinColumn (name =  "a_id" ),//被维护端外键
           joinColumns =  @JoinColumn (name =  "b_id" ))//维护端外键被维护端注解
   private B b;

//setter getter
}

@Entity
public class B {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @ManyToMany(cascade = CascadeType.REFRESH,
            mappedBy = "b",//通过维护端的属性关联
            fetch = FetchType.LAZY)
// 关系维护端删除时,如果中间表存在些纪录的关联信息,则会删除该关联信息;
// 关系被维护端删除时,如果中间表存在些纪录的关联信息,则会删除失败 .
  private A a;

//setter getter
}
  • 默认joinColumn值:关联表名:主表表名 + 下划线 + 从表表名;关联表到主表的外键:主表表名 + 下划线 + 主表中主键列名;关联表到从表的外键名:主表中用于关联的属性名+ 下划线 + 从表的主键列名
@ManyToOne @OneToMany
  • 注解和上面都差不多,无非就是谁关联谁
@Entity
@NamedQuery(name = "Customer.findByNameNQ", query = "select c from Customer c where name =?1")
public class Customer {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String name;
  private String email;
  private String address;
  private String phone;

//  //targetEntity 默认是关联实体,若使用接口作为关联实体时,应指明targetEntity的实现类,且接口应继承Serializable,否则无法被解析
//  @ManyToOne(fetch = FetchType.EAGER,targetEntity = CustomerGroup.class)
//  @JoinColumn(name = "group_type",referencedColumnName = "type")
//  private CustomerGroupInterface customerGroup;

  //manyToOne 单向关联或 oneToMany双向关联
//  //这里使用非主键作为外键关联,type在customer_group表中唯一约束,也可以使用@JoinTable 处理关联表做连接这里不再演示
  @ManyToOne(fetch = FetchType.EAGER,cascade = CascadeType.ALL)
  @JoinColumn(name = "group_type",referencedColumnName = "type")
  private CustomerGroup customerGroup;

  //customerGroup为一的一方,单向关联customer(多方)
//  @Column(name = "group_type")
//  private String type;

//setter getter
}

@Entity
@Table(name = "customer_group")
public class CustomerGroup implements CustomerGroupInterface {

  private static final long serialVersionUID = -6956725658881048590L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @Column(unique = true)
  private String type;
  private String name;
  private Integer level;

  //oneToMany 单向关联,customer中无需注解,注意joinColumn是table中的name
//  @OneToMany(fetch = FetchType.EAGER)
//  @JoinColumn(name = "group_type")

  //oneToMany 双向关联,customer中无需注解,mappedBy必须指向entity中的属性名,即标有@ManyToOne的属性名,且此处不可再使用@JoinColumn。
  @OneToMany(fetch = FetchType.EAGER, mappedBy = "customerGroup")
  private List<Customer> customers;

//setter getter
}
其他注解
注解 解释
@Entity 声明一个类为实体Bean。
@Table 说明此实体类映射的表名,目录,schema的名字。
@Id 声明此表的主键。
@GeneratedValue 定义主键的增长策略。我这里一般交给底层数据库处理,所以调用了名叫generator的增长方式,由下边的@GenericGenerator实现。
@GenericGenerator hibernate内部的主键增长方式。
@Version 注解用于支持乐观锁版本控制。一般可以用 数字 或者 timestamp 类型来支持 version.
@Column name 可选,列名(默认值是属性名); unique 可选,是否在该列上设置唯一约束(默认值false);nullable 可选,是否设置该列的值可以为空(默认值true); insertable 可选,该列是否作为生成的insert语句中的一个列(默认值true);updatable 可选,该列是否作为生成的update语句中的一个列(默认值true); columnDefinition 可选,为这个特定列覆盖SQL DDL片段 (这可能导致无法在不同数据库间移植); table 可选,定义对应的表(默认为主表);length 可选,列长度(默认值255);precision 可选,列十进制精度(decimal precision)(默认值0);scale 可选,如果列十进制数值范围(decimal scale)可用,在此设置(默认值0)
@Index 某一字段加索引 @Table(name = "customer", indexes = {@Index(columnList = "name")}),给name字段加上索引
@Transient 被注解成 @Transient 的 getter 方法或属性,将不会被持久化(自己测试,只有放在getter方法内才起作用)
@Basic 所有没有定义注解的属性,等价于在其上面添加了 @Basic注解可以声明属性的获取策略 ( fetch strategy ),fetch:抓取策略,延时加载与立即加载,optional:指定在生成数据库结构时字段是否允许为 null.
@Temporal 在核心的 Java API 中并没有定义时间精度 ( temporal precision )。因此处理时间类型数据时,你还需要定义将其存储在数据库中所预期的精度。
@Enumerated 枚举类型成员属性映射,EnumType.STRING指定属性映射为字符串,EnumType.ORDINAL指定属性映射为数据序
@Lob 用于标注字段类型为Clob和Blob类型,Clob(Character Large Ojects)类型是长字符串类型,实体的类型可为char[]、Character[]、或者String类型,Blob(Binary Large Objects)类型是字节类型,实体的类型可为byte[]、Byte[]、或者实现了Serializable接口的类。通常使用惰性加载的方式,@Basic(fetch=FetchType.LAZY)
@SecondaryTable (@javax.persistence.SecondaryTable)将一个实体映射到多个数据库表中

最后写的比较急躁,代码放github和码云了:spring-data/github spring-data/码云

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容