package luck.spring.jpa.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import luck.spring.jpa.enums.Sex;
import org.hibernate.annotations.*;
import javax.persistence.AccessType;
import javax.persistence.Entity;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OrderBy;
import javax.persistence.Table;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
// JPA规定,@Entity实体映射到数据库表
@Entity
// 给实体添加监听器
@EntityListeners({User.UserListener.class})
// 定义命名查询,类似于JDBC的预编译,在加载阶段就编译,提高查询效率
@NamedQueries({
@NamedQuery(
// 名称
name = "findTop10User",
// JPQL
query = "SELECT u FROM User u WHERE u.id < 10", hints = {
// 额外的查询属性
@QueryHint(name = "org.hibernate.timeout", value = "1000")
}),
@NamedQuery(name = "findUserByName", query = "SELECT u.name FROM User u WHERE u.name=:name"),
// 查询多个字段默认返回一个数组
@NamedQuery(name = "findNameAndAgeById", query = "SELECT u.name,u.age FROM User u WHERE u.id=1"),
// 查询多个字段,返回自定义对象
@NamedQuery(name = "findVOById", query = "SELECT NEW luck.spring.jpa.vo.UserVO(u.name,u.age) FROM User u WHERE u.id=1")
})
// 定义命名查询,类似于JDBC的预编译,在加载阶段就编译,提高查询效率
@NamedQuery(name = "User.findUserByName", query = "select u FROM User u where u.name ='luck'")
// NamedEntityGraph是使用到Entity中,EntityGraph写到Repository
// 用于解决N+1查询的方案@EntityGraph,什么是N+1?就是一对多的时候,一条主sql查询出来,同时会发送关联表的N条查询
@NamedEntityGraphs({
// 单独处理idCard
@NamedEntityGraph(name = "User.idCard", attributeNodes = {
@NamedAttributeNode("idCard")
}),
// 单独处理roles
@NamedEntityGraph(name = "User.roles", attributeNodes = {
@NamedAttributeNode("roles"),
}),
// 单独处理addresses
@NamedEntityGraph(name = "User.addresses", attributeNodes = {
@NamedAttributeNode("addresses"),
}),
// 处理当前关联的所有对象
@NamedEntityGraph(name = "User.detail",
attributeNodes = {
@NamedAttributeNode("addresses"),
@NamedAttributeNode("idCard"),
@NamedAttributeNode("roles")
}
)
})
// 配置@Column注解生效的位置,在字段上
@Access(AccessType.FIELD)
// 标识表名
@Table(name = "user")
// 移除父类的共用监听器,例如在父类开启的审计的监听器AuditingEntityListener
// 这里只是测试,实际上需要AuditingEntityListener这个监听器
@ExcludeSuperclassListeners
// 移除默认的,JPA内部的一些默认的监听器
// @ExcludeDefaultListeners
// 生成Insert的动态SQL
// 当为字段为null的情况不插入值
@DynamicInsert
// 生成Update的动态SQL
// 当为字段为null的情况不更新值
@DynamicUpdate
// 定义批量加载集合或惰性实体的大小,加载在类上,表示其他类引用该类会被一次性加载的数据量
// 只能作用于ManyToMany注解上
@BatchSize(size = 10)
@Builder
@Data
@AllArgsConstructor
@Accessors(chain = true)
@NoArgsConstructor
public class User extends BaseEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
/**
* @Column 可以标注在字段上, 也可以标注在get方法上
*/
@Column(name = "name")
private String name;
/**
* columnDefinition: 定义生成字段的结构
*/
@Column(name = "age", columnDefinition = "INT DEFAULT 0")
private Integer age;
@Column(name = "sex")
/**
* 映射枚举,使用枚举的值作为数据库值,也可以使用枚举的索引
*/
@Enumerated(EnumType.STRING)
private Sex sex;
/**
* 该字段需要懒加载
*/
@Basic(fetch = FetchType.LAZY)
@Column(name = "lazy")
private String lazy;
/**
* 格式化时间
*/
@Temporal(TemporalType.DATE)
@Column(name = "birthday", columnDefinition = "datetime(3) null comment '生日'")
private Date birthday;
@OneToMany(mappedBy = "user")
// 一般使用和OneToMany一起使用
@OrderBy("address desc")
// FetchMode.SELECT: 默认策略,新增一个sql查询数据,也是N+1问题
// FetchMode.JOIN: 通过关联关系join一条sql查询出来(只能通过联合唯一键或者Id关联查询才有效果),对懒加载不起作用,因为是一条sql查询出来
// FetchMode.SUBSELECT: 通过一条子查询完成(只能通过*ToMany的关联关系),然后主表中通过"in"来完成
@Fetch(FetchMode.JOIN)
private List<Address> addresses;
/**
* 双向关联
*/
@OneToOne(mappedBy = "user", fetch = FetchType.LAZY)
// 解决JSON序列化循环依赖
// @JsonBackReference
// @JsonManagedReference
private IdCard idCard;
/**
* 双向关联
* ManyToMany一般用的比较少,一般使用成对的OneToMany和ManyToOne,并且自定义创建中间表,系统自动创建的中间表可能不太符合要求
*/
@ManyToMany(mappedBy = "users")
// 关联查询一次性批量需要拉去的数据
// 只能作用于ManyToMany注解上,他是通过"in"来完成的
@BatchSize(size = 20)
private List<Role> roles;
/**
* 标记该字段新增,不更新,唯一
*/
@Column(insertable = false, columnDefinition = "varchar(200)", updatable = false, unique = true)
private String demo;
// 不提供SpEL对JPA实体的支持
// @Value("#{systemProperties.user.name}")
// private String systemName;
/**
* 标记不添加进数据库字段
*/
@Transient
private String luck;
// 实体监听器,监听实体的变化,从而执行对应的回调
@PrePersist
public void prePersist() {
System.out.println("prePersist");
}
@PostPersist
public void postPersist() {
System.out.println("postPersist");
}
static class UserListener {
@PreUpdate
public void preUpdate(User res) {
if (!res.name.equals("luck")) {
throw new RuntimeException("name must luck");
}
System.out.println(res);
}
@PostUpdate
public void postUpdate(User res) {
System.out.println(res);
}
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", sex=" + sex +
", lazy='" + lazy + '\'' +
", birthday=" + birthday +
", demo='" + demo + '\'' +
", luck='" + luck + '\'' +
'}';
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Accessors(chain = true)
public class RoleNameKey implements Serializable {
private String name;
private String roleKey;
}
package luck.spring.jpa.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import javax.persistence.*;
import java.util.List;
@Entity
@Table(name = "role")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Accessors(chain = true)
// 表示联合主键,RoleNameKey中的字段为当前类字段需要联合主键字段名
// 必须实现序列化接口
// 在当前类中,对应的字段添加上ID
@IdClass(value = RoleNameKey.class)
public class Role extends BaseEntity {
@Id
@Column(name = "name", columnDefinition = "varchar(64) comment '角色名称'")
private String name;
@Id
@Column(name = "role_key", columnDefinition = "varchar(64) comment '角色KEY'")
private String roleKey;
@ManyToMany
@JoinTable(name = "user_role_rel", joinColumns = {@JoinColumn(name = "name"), @JoinColumn(name = "role_key")}, inverseJoinColumns = @JoinColumn(name = "user_id"))
private List<User> users;
public Role(String name, String roleKey) {
this.name = name;
this.roleKey = roleKey;
}
}
package luck.spring.jpa.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import javax.persistence.*;
@Entity
@Table(name = "id_card")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Accessors(chain = true)
public class IdCard extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "card_no", columnDefinition = "varchar(18) comment '身份证'")
private String cardNo;
@OneToOne
@JoinColumn(name = "user_id", nullable = false, columnDefinition = "bigint(20) comment '用户Id'")
private User user;
}
package luck.spring.jpa.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import javax.persistence.*;
@Entity
@Table(name = "address")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Accessors(chain = true)
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "address", columnDefinition = "varchar(200) comment '地址'")
private String address;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
public Address(String address) {
this.address = address;
}
}
package luck.spring.jpa.entity;
import lombok.Data;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
import java.time.LocalDateTime;
/**
* 继承该类的所有类都是一张独立的表,当前类的字段都会被继承
*/
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Data
public class BaseEntity {
@CreatedBy
@Column(name = "create_by")
private String createBy;
@LastModifiedBy
@Column(name = "update_by")
private String updateBy;
@CreatedDate
@Column(name = "create_time")
private LocalDateTime createTime;
@LastModifiedDate
@Column(name = "update_time")
private LocalDateTime updateTime;
@Column(name = "is_deleted", columnDefinition = "tinyint(1) default false")
private Boolean deleted;
/**
* 开启乐观锁
* <pre>
*
* // 判断实体是不是新的
* public boolean isNew(T entity) {
* // 是否存在@Version注解
* if (!versionAttribute.isPresent()|| versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) {
* // 如果不存在@Version,则判断Id是否存在
* return super.isNew(entity);
* }
* // 获取@Version注解的字段
* BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity);
* // 字段存在值,返回false,否则返回true
* // 可以看出当更新的时候一定要带上version,如果没有version,只有ID,系统认为是新增
* return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true);
* }
* </pre>
*/
@Version
@Column(name = "version", columnDefinition = "int default 0")
private Integer version;
}
@Getter
@AllArgsConstructor
public enum Sex {
MAN("男性"), WOMAN("女性");
private String sex;
}
package luck.spring.jpa.repository;
import lombok.Data;
import luck.spring.jpa.entity.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface UserRepository extends JpaRepository<User, Long>, CustomUserRepository, JpaSpecificationExecutor<User> {
/**
* META-INF/jpa-named-queries.properties的命名查询
*/
@Query(name = "User.findAllAdultUsers")
List<User> findAllAdultUsers();
/**
* 按照命名规则查询
*/
List<User> findUserByName(String name);
List<User> findUserByAgeOrIdOrderByAgeDesc(Integer age, Long id);
/**
* 返回值为接口,会通过返回值处理器进行处理,将最终的map转换为接口,生成接口的代理对象返回
*/
@Query(value = "select u.name as name,u.age as age from User u")
List<UserDTO> findUserNameAndAge();
/**
* 用于解决N+1查询的方案@EntityGraph,什么是N+1?就是一对多的时候,一条主sql查询出来,同时会发送关联表的N条查询
*/
@EntityGraph(value = "User.roles", type = EntityGraph.EntityGraphType.LOAD)
List<User> findAll();
// 覆盖父类接口,使用悲观锁,不建议使用
// @Lock(LockModeType.PESSIMISTIC_WRITE)
// @Override
// Optional<User> findById(Long aLong);
// SpEL对Jpa的支持
// @Query("select u from User u where u.name=?1")
// entityName: 实体名称
@Query("select u from #{#entityName} u where u.name=?1")
List<User> findByName(String name);
@Data
class UserVO {
String userName;
String userLazy;
}
interface UserDTO {
String getName();
String getAge();
// SpringDataJpa支持SPEL表达式
@Value("#{target.name + '' +target.age}")
String getNameAge();
}
}
public interface CustomUserRepository {
List<User> findUsersWithIdGreaterThan(Long id);
}
public class CustomUserRepositoryImpl implements CustomUserRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<User> findUsersWithIdGreaterThan(Long id) {
return entityManager.createQuery("SELECT u FROM User u WHERE u.id > :id", User.class)
.setParameter("id", id)
.getResultList();
}
}
@Component
public class AuditAware implements AuditorAware {
private List<String> names = List.of("luck", "anti", "bao");
@Override
public Optional getCurrentAuditor() {
return Optional.of(names.get(RandomUtil.randomInt(0, names.size())));
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserVO {
private String name;
private Integer age;
}
package luck.spring.jpa;
import com.google.common.collect.Lists;
import java.util.Date;
import java.time.LocalDateTime;
import luck.spring.jpa.entity.IdCard;
import luck.spring.jpa.enums.Sex;
import cn.hutool.extra.spring.EnableSpringUtil;
import cn.hutool.extra.spring.SpringUtil;
import lombok.SneakyThrows;
import luck.spring.jpa.entity.User;
import luck.spring.jpa.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.web.JsonPath;
import org.springframework.data.web.PageableDefault;
import org.springframework.data.web.ProjectedPayload;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import javax.persistence.EntityManager;
import java.io.IOException;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.concurrent.CompletableFuture;
@EnableTransactionManagement
@Configuration
@EnableWebMvc
@ComponentScan("luck.spring.jpa")
@EnableJpaRepositories(value = "luck.spring.jpa", considerNestedRepositories = true)
// 开启JPA审计功能,就是自动添加当前实体的操作人,操作时间
@EnableJpaAuditing
@EnableSpringUtil
// 开启JPA对web环境的支持
// DomainClassConverter:让SpringMVC用来处理实体来自请求参数或路径变量
// HandlerMethodArgumentResolver: 实现让SpringMVC解决可,分页和排序实例来自请求参数
@EnableSpringDataWebSupport
// 开启重试机制
@EnableRetry
@SpringBootApplication
public class JpaDemo {
private static UserRepository userRepository;
private static EntityManager entityManager;
public JpaDemo() {
JpaDemo.userRepository = SpringUtil.getBean(UserRepository.class);
JpaDemo.entityManager = SpringUtil.getBean(EntityManager.class);
}
@SneakyThrows
public static void main(String[] args) throws IOException {
SpringApplication.run(JpaDemo.class, args);
User user = new User();
user.setId(10L);
user.setName("");
user.setAge(20);
user.setSex(Sex.MAN);
user.setLazy("");
user.setBirthday(new Date());
user.setDemo("");
user.setLuck("");
user.setDeleted(false);
System.out.println(userRepository.save(user));
}
@RestController
static class Controller {
@Autowired
private UserRepository userRepository;
// curl http://127.0.0.1:8080?sort=age&sort=age,asc
@GetMapping("/")
public Page<User> userPage(@PageableDefault(size = 10, page = 1, sort = {"age,desc"}) Pageable page, Sort sort, User user) {
return userRepository.findAll(PageRequest.of(page.getPageNumber(), page.getPageSize(), sort));
}
@GetMapping("/user/{id}")
public User user(@PathVariable("id") User user) {
return userRepository.findById(user.getId()).orElse(user);
}
@PostMapping("/")
public UserInterface userInterface(@RequestBody UserInterface userInterface) {
return userInterface;
}
}
@ProjectedPayload
interface UserInterface extends Serializable {
@JsonPath("$.age")
Integer getAge();
@JsonPath("$.name")
String getName();
}
@Component
static class UserService {
@Autowired
UserRepository userRepository;
// 重试机制
@Retryable(value = SQLException.class, maxAttemptsExpression = "retry.maxAttempts")
public void retry() {
System.out.println("luck");
}
@Transactional
public void future() {
// 当我们使用JPA的时候,CompletableFuture可能出现意想不到的事务结果,或者异常
// 我们应该使用CompletableFuture机制来处理异常,这样就比较方便的找出问题
CompletableFuture.runAsync(() -> {
System.out.println("luck");
}).exceptionally((e) -> {
e.printStackTrace();
return null;
});
}
}
}