遇到的问题
1,springboot集成jpa返回Json报错 com.fasterxml.jackson.databind.exc.InvalidDefinitionException
https://blog.csdn.net/liu_yulong/article/details/84594771
2,org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist: xxx; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: xxx
有可能是级联保存时,关联的实体类对象数据有问题,比如主键id没值,或者id重复,总是数据不能合法持久化
3,Not a managed type
一定有一个实体类没有@Table @Entity 或者主键属性没有@Id
待学习
https://www.bilibili.com/video/BV1Y4411W7Rx?p=32&spm_id_from=pageDriver
https://www.bilibili.com/video/BV12T4y1u71m?p=1
https://www.bilibili.com/video/BV1Vb411e7of
api总结
https://www.cnblogs.com/rulian/p/6434631.html
SpringDataJPA、JPA、hibernate关系
JPA只是一种规范,内部都是由接口和抽象类构建的;hibernate它是我们最初使用的一套由ORM思想构建的成熟框架,但是这个框架内部又实现了一套JPA的规范(实现了JPA规范定义的接口),所有也可以称hibernate为JPA的一种实现方式我们使用JPA的API编程,意味着站在更高的角度上看待问题(面向接口编程);Spring Data JPA它是Spring家族提供的,对JPA规范有一套更高级的封装,是在JPA规范下专门用来进行数据持久化的解决方案。
主要接口
Repository: 是 Spring Data的一个核心顶层接口,它不提供任何方法,开发者需要在自己定义的接口中声明需要的方法。
CrudRepository: 继承Repository,提供增删改查方法,可以直接调用。
PagingAndSortingRepository: 继承CrudRepository,具有分页查询和排序功能
JpaRepository: 继承PagingAndSortingRepository,针对JPA技术提供的接口
JpaSpecificationExecutor: 可以执行原生SQL查询
CrudRepository
CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S var1);
<S extends T> Iterable<S> saveAll(Iterable<S> var1);
Optional<T> findById(ID var1);
boolean existsById(ID var1);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> var1);
long count();
void deleteById(ID var1);
void delete(T var1);
void deleteAll(Iterable<? extends T> var1);
void deleteAll();
PagingAndSortingRepository
PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort var1);
Page<T> findAll(Pageable var1);
JpaRepository
JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
List<T> findAll();
List<T> findAll(Sort var1);
List<T> findAllById(Iterable<ID> var1);
<S extends T> List<S> saveAll(Iterable<S> var1);
void flush();
<S extends T> S saveAndFlush(S var1);
void deleteInBatch(Iterable<T> var1);
void deleteAllInBatch();
T getOne(ID var1);
<S extends T> List<S> findAll(Example<S> var1);
<S extends T> List<S> findAll(Example<S> var1, Sort var2);
pom
spring-boot-starter-data-jpa 依赖 spring-data-jpa
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.9.0.RELEASE</version>
</dependency>
HelloWorld
提前说一声,这并不是jpa的helloworld,也不是springdatajpa的helloworld,而是springboot整合springdatajpa,因为这个是主流趋势啊。数据库是mysql8
yml
spring:
# 数据源
datasource:
# mysql8驱动
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
jpa:
hibernate:
# 可选参数 ddl-auto 详细文章:https://www.cnblogs.com/qingmuchuanqi48/p/11616145.html
# create 启动时删数据库中的表,然后创建,退出时不删除数据表
# create-drop 启动时删数据库中的表,然后创建,退出时删除数据表 如果表不存在报错
# update 如果启动时表格式不一致则更新表,原有数据保留
# validate 项目启动表结构进行校验 如果不一致则报错
ddl-auto: update
show-sql: true
# ddl-auto: update 时,第一次运行程序需要自己手动建表,也建议这么做,因为create和create-drop可怕的要死,会在启动程序时先“删”
CREATE TABLE `jpa_user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`email` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `jpa_hobby` (
`id` bigint NOT NULL AUTO_INCREMENT,
`level` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`remark` varchar(255) DEFAULT NULL,
`userid` bigint DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
entity
@Entity
@Table(name = "jpa_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String username;
@Column(name = "email")
private String email;
Repository
泛型为操作的bean类型 主键类型
public interface UserRepository extends JpaRepository<User, Long> { }
controller
@Controller
@RequestMapping(path = "/user/crud")
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping(path = "/save")
public User saveUser( @RequestParam String name, @RequestParam String email ) {
User n = new User();
n.setUsername(name);
n.setEmail(email);
System.out.println(n);
return userRepository.save(n);
}
@GetMapping(path = "/deleteById")
@ResponseBody
public void deleteById(Long id) {
userRepository.deleteById(id);
}
@GetMapping(path = "/deleteByUser")
@ResponseBody
public void deleteByUser(User user) {
userRepository.delete(user);
}
@GetMapping(path = "/update")
@ResponseBody
private User updateUser(Long id, String name){
Optional<User> optionalUser = userRepository.findById(id);
optionalUser.get().setUsername(name);
User u = userRepository.save(optionalUser.get());
return u;
}
@GetMapping(path = "/findOne")
@ResponseBody
public Optional<User> findById(Long id) {
return userRepository.findById(id);
}
@GetMapping(path = "/getOne")
@ResponseBody
public User getOne(Long id) {
return userRepository.getOne(id);
}
@GetMapping(path = "/findAll")
@ResponseBody
public List<User> getAllUser() {
return userRepository.findAll();
}
}
测试
http://localhost:8080/user/crud/save?name=zhangsan&email=zhangsan123@qq.com
http://localhost:8080/user/crud/getOne?id=3
此时已经可以实现,但有时候更规范写法是增加一个自定义的接口,然后增加一个实现类,单实现类还是注入的UserRepository接口,因为这个继承了JpaRepository,所以UserRepository才是真正起交互作用的。
public interface UserService {
User saveUser(User user);
void deleteUser(User user);
void deleteUser(Long id);
User updateUser(User user);
User findUser(Long id);
Iterable<User> findAll();
}
注意,接口不加@Service注解,而是实现类加
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
public User saveUser(User user) {
return userRepository.save(user);
}
}
此时controller当要要注入的是实现类。而不是UserRepository了
测试:http://localhost:8080/user/crud/saveUserByImpl?name=姚明&email=姚明@qq.com
@Controller
@RequestMapping(path = "/user/crud")
public class UserController {
@Autowired
private UserServiceImpl userServiceImpl;
@GetMapping(path = "/saveUserByImpl")
public User saveUser( @RequestParam String name, @RequestParam String email ) {
User n = new User();
n.setUsername(name);
n.setEmail(email);
System.out.println(n);
return userServiceImpl.saveUser(n);
}
}
常见注解
@Column
@Column(name = “name”, nullable = false) // @Column: 对应数据库列名,可选, nullable 是否可以为空, 默认true
@query
@Transactional
@NoRepositoryBean
@Temporal
数据库的字段类型有date、time、datetime
而Temporal注解的作用就是帮Java的Date类型进行格式化,一共有三种注解值:
第一种:@Temporal(TemporalType.DATE)——>实体类会封装成日期“yyyy-MM-dd”的 Date类型。
第二种:@Temporal(TemporalType.TIME)——>实体类会封装成时间“hh-MM-ss”的 Date类型。
第三种:@Temporal(TemporalType.TIMESTAMP)——>实体类会封装成完整的时间“yyyy-MM-dd hh:MM:ss”的 Date类型。
注解方式有两种:
写在字段上:
@Temporal(TemporalType.TIMESTAMP)
private Date birthday;
写在 getXxx方法上:
@Temporal(TemporalType.DATE)
@Column(name = "birthday", length = 10)
public Date getBirthday() {
return this.birthday;
}
@Entity
应用于实体类,表明该实体类被JPA管理,将映射到指定的数据库表
@Id
应用于实体类的属性或者属性对应的getter方法,表示该属性映射为数据库表的主键
@GerneratedValue
于@Id一同使用,表示主键的生成策略,通过strategy属性指定。
JPA提供的生成策略有:
- AUTO — JPA自动选择合适的策略,是默认选项
- IDENTITY — 采用数据库ID自增长的方式来生成主键值,Oracle不支持这种方式;
- SEQUENCE — 通过序列产生主键,通过@SequenceGenerator注解指定序列名,MySql不支持这种方式;
- TABLE — 采用表生成方式来生成主键值,这种方式比较通用,但是效率低
Repository常用方法使用规范
关键字 | 方法命名 | sql where字句 |
---|---|---|
And | findByNameAndPwd | where name= ? and pwd =? |
Or | findByNameOrSex | where name= ? or sex=? |
Is,Equals | findById,findByIdEquals | where id= ? |
Between | findByIdBetween | where id between ? and ? |
LessThan | findByIdLessThan | where id < ? |
LessThanEquals | findByIdLessThanEquals | where id <= ? |
GreaterThan | findByIdGreaterThan | where id > ? |
GreaterThanEquals | findByIdGreaterThanEquals | where id > = ? |
After | findByIdAfter | where id > ? |
Before | findByIdBefore | where id < ? |
IsNull | findByNameIsNull | where name is null |
isNotNull,NotNull | findByNameNotNull | where name is not null |
Like | findByNameLike | where name like ? |
NotLike | findByNameNotLike | where name not like ? |
StartingWith | findByNameStartingWith | where name like ‘?%’ |
EndingWith | findByNameEndingWith | where name like ‘%?’ |
Containing | findByNameContaining | where name like ‘%?%’ |
OrderBy | findByIdOrderByXDesc | where id=? order by x desc |
Not | findByNameNot | where name <> ? |
In | findByIdIn(Collection<?> c) | where id in (?) |
NotIn | findByIdNotIn(Collection<?> c) | where id not in (?) |
True | findByAaaTue | where aaa = true |
False | findByAaaFalse | where aaa = false |
IgnoreCase | findByNameIgnoreCase | where UPPER(name)=UPPER(?) |
自定义实体类对应表名
场景:
在entity类中@Table(name = “cst_customer_#{appConfig.tableName}”)支持SpEl表达式替换
实现思路如下:
- 自定义命名策略
- 配置自定义策略
- JavaConfig定义
- @Table使用配置
原理总结:
配置命名策略后,写一个config类,类对象的属性值从yml中获取(但是此技术其实是yml固定写死的,并没有动态,想怎么更新值,就怎么更新值)
自定义命名策略
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
import org.springframework.beans.BeansException;
import org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.BeanFactoryAccessor;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
/**
* 自定义命名策略(实现支持解析#{javaConfig.property}获取表名功能 )
*
* @author xxx
* @since 2019-03-26
*/
@Component
public class MySpringPhysicalNamingStrategy extends SpringPhysicalNamingStrategy
implements ApplicationContextAware {
private final StandardEvaluationContext context = new StandardEvaluationContext();
private final SpelExpressionParser parser = new SpelExpressionParser();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context.addPropertyAccessor(new BeanFactoryAccessor());
this.context.setBeanResolver(new BeanFactoryResolver(applicationContext));
this.context.setRootObject(applicationContext);
}
@Override
public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment jdbcEnvironment) {
String nameStr = name.getText();
if(nameStr.contains(ParserContext.TEMPLATE_EXPRESSION.getExpressionPrefix())){
Expression expression = this.parser.parseExpression(nameStr, ParserContext.TEMPLATE_EXPRESSION);
return Identifier.toIdentifier((String)expression.getValue(this.context, String.class));
}else {
//默认方式不变
return super.toPhysicalTableName(name, jdbcEnvironment);
}
}
}
如果不配置命名策略就会报这样的错误
org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL “create table cst_customer_#{app_config_table_name} (cust_id bigint not null auto_increment, cust_address varchar(255), cust_industry varchar(255), cust_level varchar(255), cust_name varchar(255), cust_phone varchar(255), cust_source varchar(255), primary key (cust_id)) engine=InnoDB” via JDBC Statement
配置自定义策略
spring:
jpa:
show-sql: true
hibernate:
naming:
physical-strategy: 包名.MySpringPhysicalNamingStrategy
myTableName: tb_user
JavaConfig定义
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class AppConfig {
@Value("${myTableName}")
private String tableName;
public String getTableName() {
return tableName;
}
public void setTableName(String tableName) {
this.tableName = tableName;
}
}
@Table使用配置
@Table(name = "cst_customer_#{appConfig.tableName}")
一对多级联保存
User实体对应多个Hobby实体,每个人都有多个爱好
在User增加Set属性,保存对应的多个hobby,以及两个注解 @OneToMany @JoinColumn 这种方式是在多的一方(Hobby)的表中增加一个外键列来保存关系
注意:这里说的是一对多的单向关联,不是一对多的双向关联。
@Entity
@Table(name = "jpa_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String username;
@Column(name = "email")
private String email;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) // 级联保存、更新、删除、刷新;延迟加载
@JoinColumn(name="userid") // 在hobby表指定一个外键列来实现一对多的单向关联,这个列会动态创建
private Set<Hobby> hobby;
@Entity
@Table(name = "jpa_hobby")
public class Hobby {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "level")
private String level;
@Column(name = "remark")
private String remark;
用springboot自带的测试类DemoApplicationTests进行测试
测试前由于yml中ddl-auto: update每次都会删除旧表,新建表的,所以测试时最好删除两张表,从新开始测试,以免旧数据干扰
import com.example.demo.entity.Hobby;
import com.example.demo.entity.User;
import com.example.demo.repository.user.UserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.HashSet;
import java.util.Set;
@SpringBootTest
class DemoApplicationTests {
@Autowired
private UserRepository userRepository;
@Test
void contextLoads() {
Set<Hobby> setHooby = new HashSet<>();
for (int i = 0; i < 10; i++) {
Hobby hobby = new Hobby();
hobby.setName("篮球");
hobby.setLevel("级别为"+i);
hobby.setRemark("但是没有体育精神,垃圾指数"+i);
setHooby.add(hobby);
}
User user = new User();
user.setUsername("王五");
user.setEmail("wangwu27@qq.com");
user.setHobby(setHooby);
userRepository.save(user);
}
}
另一种方式:只在User类的hobby属性上加一个注解
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) // 级联保存、更新、删除、刷新;延迟加载
Hobby类不做任何变化,这样会多生成一张表,会以User的表名和下划线和Hobby的表名生成一张表user_hobby,来保存映射关系。注意,会多一张表,也许因为如此,会使数据库变得繁杂混乱,用的人比较少吧。