在常规的WEB应用中,数据库的crud是用的最多的功能,基于spring boot框架,我们最常用的就两个框架,一个是hibernate、另外一个则是mybatis。两者在使用方法、以及使用的效果效率等方面有什么区别。
需要注意的hibernate本来是一个完全的ORM数据持久化框架,他完全遵守了JPA的规范,因此springboot的Jpa默认引用了hibernate来做为持久层框架。当然使用者也可以主动排除hibernate的依赖,自行添加其他的ORM框架。
而国内互联网公司常用的mybatis并非完全的ORM框架,未遵守jpa规范,他的使用则是另成体系,我们再下一篇文章中进行相关介绍。
首先笔者先根据自己的理解,介绍下使用JPA规范(依赖hibernate做持久层)进行CRUD操作的一些优缺点比较。
一、使用的一些注意事项:
1、如果DAO层不是按规范来写的接口,而是需要按照自己的约束条件进行SQL的操作,则需要DAO的接口类中按照JPA规则的约定写对应的方法名,且只需要写规范的方法名,框架会自动拦截方法名后去实现。如下类代码所示:
public interface RoleRepository extends JpaRepository<TestRole, Long> {
//如果不是按规范来写的接口,自己约束了SQL的,需要在下面写对应的方法
//具体的JPA规范自行查询手册,下面做简单解释
//delete TestRole(这个实体对象的类名)
//RoleName查询的字段
//And 查询条件是 and
//连接的另外一个查询条件UserId
//特别注意的,这里面的所有名称都是实体类的对象名、属性名,而不是数据库的表名、字段名等
int deleteTestRoleByRoleNameAndUserId(String roleName, long userId);
Set<TestRole> findTestRoleByUserId(long userId);
}
2 JPA进行多表查询的方式分两种。1类是通过oneToMany,ManyToOne,ManyToMany,oneToOne的注解进行级联查询;第2类方式是写自定义多表联合查询的sql,inner join \ left inner join \ left out join ...
3、级联查询,如果不需要被查询的从表反向更新主表,则只需要在主表进行注解即可。级联的两个DEMO实体类如下所示:
//User类,1个User可能存在多个角色
@Data
@Entity
@Table(name = "user")
public class TestUser {
//括号里面的参数表示这个ID是自增长
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id;
//如果类的属性和数据库字段名字是一致的,就不用加注解了
@Column(name = "username")
String username;
@Column(name = "password")
String password;
@OneToMany(mappedBy = "userId", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@OrderBy(value = "id desc ")//查询的这个数据按照user_id字段降序排序
Set<TestRole> roles;
}
再次说明需要注意的:
mapper里是SET泛型里面的类的属性,而不是要去映射的数据库表名,
CascadeType.ALL表示这个属性针对刷新数据,也就是查数据
fetch代表是无论是否调用getRoles,都要先去查询数据,(饿汉模式)
当这里设置了映射到外表的字段,框架会给外表自动把这个创建为外键,并且级联的选项和注释的cascade向匹配
这里如果要用hibernate级联查询的功能,被级联的字段坚决不能注释@Transient,这样框架才会主动根据这个映射的mapper和Set的类型去Get数据。
当然可能你要问@Transient有什么用,注解为@Transient后,表示这个属性只是类属性,不会对应相应的数据库字段。
从表mysql数据库不需要单独去设置外键,如果使用onetomany的注解后,Hibernate会自动添加外键
4、如果需要采用自定义SQL进行联表操作,需要先自定义联表后返回结果集的接收的类型或接口,建议写类型接受数据。这是Dao层接口代码:
public interface CustomRepository extends JpaRepository<TestUser, Long> {
//下面采用自定义SQL进行连表操作,需要先自定义联表后返回结果集的接收的类型或接口,建议写类型,写接口来接受数据并没有跑通
//如果涉及到删除修改,需要加上@Modifying注解
/*另外顺便说一下连表时候on与where的区别
on是先对表进行筛选再生成关联表,where是先生成关联表再对关联表进行筛选,on执行的优先级高于left join,而where的优先级低于left join
当我们使用on关键字时,会先根据on后面的条件进行筛选,条件为真时返回该行,由于on的优先级高于left join,所以left join关键字会把左表中没有匹配的所有行也都返回,然后生成临时表返回
where对与行的筛选是在left join之后的,也就是生成临时表之后才会对临时表进行筛选
*/
//如果采用自定义SQL。需要接收SQL返回的什么字段的数据,就在类里面定义相应的属性,并且//一定要在构造函数中接收这些参数。
@Query(value = "SELECT new com.ywcai.demo.jpa.TestUserByCustom(u.id ,u.username , u.password , r) FROM TestUser u INNER JOIN TestRole r on u.id=r.userId ")
Page<TestUserByCustom> getAllUserInfo(Pageable pageable);
//代码里面顺便使用了hibernate自带的分页组件。
@Query(value = "SELECT new com.ywcai.demo.jpa.TestUserByCustom(u.id ,u.username , u.password , r) FROM TestUser u INNER JOIN TestRole r on u.id=r.userId ")
List<TestUserByCustom> getAllUserInfo();
}
这是对应多表集合所对应的实体类代码:
@Data
public class TestUserByCustom {
long id;
String username;
String password;
TestRole testRole;
//主要是这个构造函数及起形参,必须要写一个接收所有属性的构造函数,否则报错。
//并且再Dao要见结构隐射到这个对象时,需要全限名的类名以及保证参数顺序也一致
public TestUserByCustom(long id, String username,
String password,
TestRole testRole)
{
this.id = id;
this.username = username;
this.password = password;
this.testRole = testRole;
}
}
另外,下面是针对springboot2.2.5版本的分页组最新的写法:
Sort sort = Sort.by(Sort.Direction.DESC, "id");
Pageable pageable = PageRequest.of(currentPage, pageSize, sort);
不再采用 new 的方式。
5、Springboot-jpa所自带的实现框架为Hibernate , 不需要单独再引入Hibernate了,这个开篇已经说过了。
6、如果实体类的属性(既加了@Entity注解的类)和数据库字段名字是一致的,就不用加注解了。
7、@EnableJpaRepositories的作用是开启Jpa的支持,如果我们有多个需要自定义的数据源,就需要单独实现在数据源配置类的@EnableJpaRepositories注解中引用相关的Dao接口、工厂bean、事务bean。其中,entityManagerFactoryRef是实体关联管理工厂Bean和transactionManagerRef事务管理Bean。如果不使用系统默认配置的数据源,自定数据源时,这两个Bean都需要我们自行实现,然后注解中进行关联。具体代码如下:
//创建一个事务管理bean
@Bean(name = "manTransactionManager")
@Qualifier(value = "manTransactionManager")
public PlatformTransactionManager manTransactionManager(
@Qualifier("manEntityManagerFactory")
LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean
) {
EntityManagerFactory factory = localContainerEntityManagerFactoryBean.getObject();
return new JpaTransactionManager(factory);
}
//创建实体管理工厂bean
@Bean(name = "manEntityManagerFactory")
@Qualifier(value = "manEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean manEntityManagerFactory
(@Qualifier(value = "jpaDataSource")
DataSource jpaDataSource) {//5
LocalContainerEntityManagerFactoryBean factory =
new LocalContainerEntityManagerFactoryBean();
//这里实体管理工厂需要注入新的数据源
factory.setDataSource(jpaDataSource);
factory.setPackagesToScan("com.ywcai.demo.jpa");
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
// //这里可以配置原来的hibernate的配置
hibernateJpaVendorAdapter.setShowSql(jpaProperties.isShowSql());
hibernateJpaVendorAdapter.setDatabase(jpaProperties.getDatabase());
hibernateJpaVendorAdapter.setDatabasePlatform(jpaProperties.getDatabasePlatform());
hibernateJpaVendorAdapter.setGenerateDdl(jpaProperties.isGenerateDdl());
factory.setJpaVendorAdapter(hibernateJpaVendorAdapter);
//原来的jpa的属性会失效,如果需要,自行重新配置
return factory;
}
8、 在自定义的工厂类里面,需要把jpa的配置另行注入manEntityManagerFactory,原来的配置将不在生效。同样见上面的代码。
二、jpa和mybatis优缺点的对比 优点: 1、简单的单表查询,JPA(hibernate)规范已经约定,基本不用写任何方法即可进行数据库操作; 2、类与数据库的映射关系可以再实体类上直接进行注解,并且实体类内部还可以包含其他实体类,方便多表查询后多维度数据集的映射 3、同时可通过注解的方式进行多表的级联查询,在表字段和查询条件复杂度较低时,使用时比较方便的。 4、内置了比较好用的分页组件,分页也是物理分页,效率满足基本应用的需求 5、当然还有一个最大的优点,JPA是一套数据库规范,其实现与底层使用的哪家数据库没有关系。所以你后期可以随意的切换关系型数据库而不影响业务代码,mybatis则与底层数据库强关联。 缺点: 1、首先使用JPA规范,要进行稍微复杂的查询时,方法名非常长,一不留神就有可能写错; 2、如果使用注解的方式进行级联查询,框架会给数据库表的从表设置外键,有可能给数据库带来未知的隐患; 3、当需要执行交复杂的查询时,级联查询使用起来较为复杂; 4、可以使用注解,采用自定义SQL语句的方式进行复杂多表查询和操作,但如果使用完全原生的SQL语句方式,无法进行实体类与结果集的映射,需要自己写代码去转换。如果不使用完全原生的SQL语句,在SQL中仍然引用类对象,虽大部分功能可实现,但因使用的不规范有可能在书写等发生错误时不易被发现。 5、太长太复杂的语句注解中不易拼接。 6、通过JPA规范进行的相关数据操作,有些语法效率不高,冗余的SQL语句较多。 7、总体来说,无论按照规范还是自定义的方式来,灵活度+易用性只能占其中1一样,更多的时候用JPA还是选择起易用性(采用规范)。
三、jpa的主要配置和使用代码
1、POM文件的引用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
这里注意的是需要单独添加个数据库连接的驱动
2、yml文件中数据源的配置,这里还是先介绍使用默认配置的方式:
Spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver //这里注意驱动名称,新版本的驱动名字
username: root
password: youword
url: jdbc:mysql://localhost:3306/database?characterEncoding=utf-8&serverTimezone=UTC
jpa:
hibernate:
ddl-auto: update # 如果初始化,则根据实体类可以自动生成表结构
database-platform: org.hibernate.dialect.MySQL5Dialect
show-sql: true
generate-ddl: true
database: mysql
open-in-view: true
当然jpa默认还试用了hirika的数据库连接池,这个连接池配置在后面的mybatis教程章节中介绍。
在使用默认配置的情况下,数据源的配置文件就不需要些了,框架会自动生成相应的数据源、工厂和事务管理器。同样下一章,我们会介绍如何完全使用注解的方式自定义mybatis的数据源以及如何进行双数据源的切换。
UserRepository.class文件
//如果是按照JPA规范来的,就不用写接口方法
public interface UserRepository extends JpaRepository<TestUser, Long> {
}
RoleRepository.class类
package com.ywcai.demo.jpa;
public interface RoleRepository extends JpaRepository<TestRole, Long> {
//如果不是按规范来写的接口,自己约束了SQL的,需要在下面写对应的方法
int deleteTestRoleByRoleNameAndUserId(String roleName, long userId);
Set<TestRole> findTestRoleByUserId(long userId);
}
TEST类中测试方法调用
package com.ywcai.demo.jpa;
@RunWith(SpringRunner.class)
@SpringBootTest
@EnableTransactionManagement
@Slf4j
class JpaTests {
/**
* @描述 1、JPA进行多表查询,可以采用级联表查询和自定义SQL的关联查询两种方法
* 2、级联表会自动为查询的从表生成外键,并且分别生成对应的每个单表的实体进行@entiry注解标准
* 3、每个实体类需要写好相应的set\get方法,当然也可以通过lombok的@DATA进行注解后自动生成
* 4、要关联的从表,需要引用从表的对象类,声明本地类的属性,并且注解中用mapperBy映射到从表的外键字段进行关联。
* 5、若要通过定义SQL进行多表查询,则需要根据关联查询返回的结果集字段定义相应的接口来接收结果集。
* 6、自定义SQL进行多表查询,只需要在mapper接口中定义查询的方法和注解SQL,并创建结果集类的接口即可,不需要实例化
* 7、框架会根据接口的属性名字进行反射和自动实例化,当然加上Service注解,这里可以直接用autowired方式通过type来识别
* @创建人 jimi
* @参数
* @返回值
* @创建时间 2020/3/13
*/
@Autowired
@Qualifier("userRepository")
UserRepository userRepository;
@Autowired
@Qualifier("roleRepository")
RoleRepository roleRepository;
@Autowired
CustomRepository customRepository;
@Test
void findAll() {
/**
*@描述 这个方法和getOne()方法的区别,getOne返回一个引用,而findById.get则是返回了对象原型。
*@创建人 jimi
*@参数 []
*@返回值 void
*@创建时间 2020/3/13
*/
TestUser user = userRepository.findById(9l).get();
log.info("user is {}", user);
}
@Test
void delete() {
/**
*@描述 级联删除,删除user的时候会把级联把所有ID所对应的roles的删除
*@创建人 jimi
*@参数 []
*@返回值 void
*@创建时间 2020/3/13
*/
userRepository.deleteById(9l);
}
@Test
void deleteByXXX() {
/**
*@描述 级联删除, 删除从表中的数据
*@创建人 jimi
*@参数 []
*@返回值 void
*@创建时间 2020/3/13
*/
int rows = roleRepository.deleteTestRoleByRoleNameAndUserId("MyTEST2222", 9l);
log.info("删除了 {} 条数据", rows);
}
@Test
void updateXXX() {
/**
*@描述 1、级联更新,这里是新增加一个用户,如果是更新一个用户,则先查出来,然后更新对象,然后saveandflush即可
*@创建人 jimi
*@参数 []
*@返回值 void
*@创建时间 2020/3/13
*/
TestUser testUser = userRepository.findById(9l).get();
TestRole testRole = new TestRole();
//注意,设置这个外键非常关键,如果这个外键的ID在外表中不存在,则会添加数据失败。
testRole.setUserId(testUser.getId());
testRole.setRoleName("MyTEST");
TestRole testRole2 = new TestRole();
testRole2.setUserId(testUser.getId());
testRole2.setRoleName("MyTEST2222");
Set<TestRole> set = new HashSet<>();
set.add(testRole);
set.add(testRole2);
userRepository.saveAndFlush(testUser);
}
@Test
void findByPage() {
/**
*@描述 1、利用JPA自带的分页组件
* 2、注意分页的组件用法,好好的用new对象的方式不行,非要改成这种奇怪的写法,也不是建造者模式、也不是工厂模式。
*@创建人 jimi
*@参数 []
*@返回值 void
*@创建时间 2020/3/13
*/
int currentPage = 1;
int pageSize = 2;
Sort sort = Sort.by(Sort.Direction.DESC, "id");
Pageable pageable = PageRequest.of(currentPage, pageSize, sort);
Page<TestRole> page = roleRepository.findAll(pageable);
for (TestRole role : page.getContent()
) {
log.info("role : {}", role);
}
}
@Test
void joinAndCustomMethod() {
int currentPage = 1;
int pageSize = 2;
Sort sort = Sort.by(Sort.Direction.DESC, "id");
Pageable pageable = PageRequest.of(currentPage, pageSize, sort);
List<TestUserByCustom> users = customRepository.getAllUserInfo();
for (TestUserByCustom user : users
) {
log.info("role : {}", user);
}
log.info("==============下面是分页");
Page<TestUserByCustom> page = customRepository.getAllUserInfo(pageable);
for (TestUserByCustom user : page.getContent()
) {
log.info("role : {}", user);
}
}
}