對於本職工作來說 JDBC 就可以很好的完成,但是當我們對持久化的需求變得更復雜時,如:
- 延遲加載
- 預先抓取
- 級聯
JDBC 就不能滿足了,我們需要使用 ORM框架 來實現這些需求。
Spring 對多個持久化框架都提供了支持,包括 Hibemate、IBATIS、JPA 等。和 Spring 對 JDBC 的支持一樣, Spring 對 ORM框架 的支持除了提供與框架的繼承點之外還包括一些附加服務:
- 支持集成 Spring 聲明式事務;
- 透明的異常處理;
- 現場安全的、輕量級的模板類;
- DAO 支持類;
- 資源管理
這里我們不會講解所有的 ORM框架,只重點講解下 JPA 的使用。
Java 持久化 API(Java Persistence API,JPA) 是基於 POJO 的持久化機制,它從 Hibemate 和 Java 對象(Java Data Object,JDO) 上借鑒了很多理念並加入了 Java5 注解的特性。
引入依賴
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<!-- druid連接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<!-- mysql連接驅動 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!-- JPA 依賴-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.2.17.Final</version>
</dependency>
</dependencies>
配置實體管理器工廠
基於 JAP 的應用程序需要使用 EntityManagerFactory 的實現類來獲取 EntityManager 實例。 JAP 定義了兩種類型的實體管理器:
- 應用程序管理類型(Application-managed): 當應用程序向實體管理器直接請求實體管理器時,工廠會創建一個實體管理器。在這種模式下,程序要負責打開或關閉實體管理器並在事務中對其進行控制。這種方式的實體管理器適合於不運行在 Java EE 容器中的獨立應用程序。
- 容器管理類型(Container-managed): 實體管理器由 Java EE 創建和管理。應用程序根本不與實體管理工廠打交道。這種類型的實體管理器最適合用於 Java EE 容器。
這兩種實體管理器工廠分別由對應的 Spring 工廠對象創建:
- LocalEntityManagerFactoryBean: 生成應用程序管理類型的 EntityManager-Factory
- LocalContainerEntityManagerFactoryBean: 生成容器管理類型的 EntityManager-Factory
配置應用程序管理類型的 JAP
對於應用管理類型的實體管理工廠,它的絕大部分配置信息來源於一個名為 persistence.xml 的配置文件,這個文件必須位於類路徑下的 META-INF 目錄下。
persistence.xml 的作用在於定義一個或多個持久化單元。持久化單元是同一個數據源下的一個或多個持久化類。
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<!-- 實體類 -->
<class>com.marklogzhu.web.entity.User</class>
<properties>
<!-- 數據庫連接的基本信息 -->
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://127.0.0.1:3306/learn"/>
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="javax.persistence.jdbc.password" value="sa000"/>
</properties>
</persistence-unit>
</persistence>
在 persistence.xml 文件中已經包含了多個配置信息,所以在 Spring 中的配置就很少了。
@Bean
public LocalEntityManagerFactoryBean entityManagerFactoryBean() {
LocalEntityManagerFactoryBean entityManagerFactoryBean = new LocalEntityManagerFactoryBean();
entityManagerFactoryBean.setPersistenceUnitName("jpa");
return entityManagerFactoryBean;
}
配置容器管理類型的 JAP
當運行在容器中時,通過容器提供的信息來生成 EntityManagerFactory。這樣做的好處就是可以不用配置 persistence.xml 文件了,只需要在 Spring 上下文中配置。
@Autowired
private DataSource dataSource;
@Autowired
private JpaVendorAdapter jpaVendorAdapter;
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(){
LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean();
emfb.setDataSource(dataSource);
emfb.setJpaVendorAdapter(jpaVendorAdapter);
// 掃描指定包下帶有 @Entity注解的實體類等同於 persistence.xml 文件 <class> 標簽
emfb.setPackagesToScan("com.marklogzhu.web.entity");
return emfb;
}
除了配置 ** dataSource ** 屬性之外,我們還配置了一個 jpaVendorAdapter 屬性,它用於指定使用哪一個廠商的 JAP 實現:
- EclipseLinkJpaVendorAdapter
- HibernateJpaVendorAdapter
- OpenJpaVendorAdapter
- TopLinkJpaVendorAdapter(Spring 3.1 版本已廢棄)
我們這邊采用 HibernateJpaVendorAdapter 實現類:
@Bean
public JpaVendorAdapter jpaVendorAdapter(){
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
// 設置數據庫類型
adapter.setDatabase(Database.MYSQL);
// 設置打印sql語句
adapter.setShowSql(Boolean.TRUE);
// 設置不生成ddl語句
adapter.setGenerateDdl(Boolean.FALSE);
// 設置hibernate方言
adapter.setDatabasePlatform("org.hibernate.dialect.MySQL5Dialect");
return adapter;
}
編寫基於 JPA 的 Repository
就像 Sprign 對其他持久方案的集成一樣, Sprign 對 JPA 集成也提供了 JpaTemplate 模板以及對象的支持類 JpaDaoSupport,但是為了更純粹的 JPA 方式,基於模板的 JPA 已經棄用了,所以我們這里只講純粹的 JPA 方式。
接口:
public interface UserRepository {
User findById(Long id);
}
實現:
@Repository("userRepository")
public class UserJpaRepository implements UserRepository {
@PersistenceContext
private EntityManager em;
@Override
public User findById(Long id) {
return em.find(User.class,id);
}
}
用戶實體類:
@Entity
@Table(name = "sys_user")
public class User {
@Id
private Long id;
private String avatar;
private String account;
private String password;
private String salt;
private String name;
private String birthday;
private Integer sex;
private String email;
private String phone;
@Column(name = "role_id")
private String roleId;
@Column(name = "dept_id")
private Long deptId;
private Integer status;
private Date createtime;
private Long version;
//get/set......
}
單元測試:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = RootConfig.class)
public class JpaDaoTest {
@Autowired
private UserRepository userRepository;
@Test
public void testFindByUserId(){
Long userId = new Long(1);
User user = userRepository.findById(userId);
Assert.assertNotNull(user);
Assert.assertEquals(userId,user.getId());
}
}
注解說明
- @Repository: 用於標注數據訪問組件,即DAO組件
- @PersistenceContext: 注入 EntityManager 對象
- @Entity: 表示這個類是一個實體類
- @Table(name = "sys_user") : 數據庫中表名是 sys_user ,而我們的實體類名稱是 User 兩者不一致所以需要設置綁定關系,設置方法有如下兩種:
- 如實例中通過 @Column 注解 做顯式轉換
- 修改默認命名策略為 PhysicalNamingStrategyStandardImpl
借助 Spring Data 實現自動化的 JPA Repository
上面的 UserJpaRepository 實現的是我們自己申明的方法,不同的 Repository 除了操作的對象不同外,其他方法都是一樣的,而借助 Spring Data 我們就可以省略這些通用的方法。
配置 Spring Data JPA
@Configuration
@EnableJpaRepositories("com.marklogzhu.web.dao")
public class JpaConfiguration {
}
使用 @EnableJpaRepositories 注解來掃描指定包下的 Repository 。我們回到 UserRepository 接口:
public interface UserRepository extends JpaRepository<User,Long> {
}
可以看到現在 UserRepository 接口繼承了 JpaRepository接口,而 JpaRepository 又擴展自 Repository 接口。所以就可以直接使用 Spring Data JPA 默認提供的18個方法:
@NoRepositoryBean
public interface 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();
}
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort var1);
Page<T> findAll(Pageable var1);
}
@NoRepositoryBean
public interface 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);
}
將 UserJpaRepository 類刪除,我們不需要自己實現的接口類了。
修改單元測試
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = RootConfig.class)
public class JpaDaoTest {
@Autowired
private UserRepository userRepository;
@Test
public void testFindByUserId(){
Long userId = new Long(1);
User user = userRepository.findById(userId).get();
Assert.assertNotNull(user);
Assert.assertEquals(userId,user.getId());
}
}
查詢方法
除了上面的接口定義方法之后,我們還能新增查詢方法來實現需求。
接口定義:
@Transactional
public interface UserRepository extends JpaRepository<User,Long> {
User findUserByAccount(String account);
}
單元測試:
@Test
public void testFindByAccount(){
String account = "admin";
User user = userRepository.findUserByAccount(account);
Assert.assertNotNull(user);
Assert.assertEquals(account,user.getAccount());
}
可以看到我們並沒有自己實現 findByAccount 方法,但是卻可以正常使用。這是因為 Spring Data JPA 會根據我們定義的方法來推斷我們需要的功能。它的原理是 Spring Data 定義了一組小型的領域特定語言(domain-speclific language,DSL)來用推斷我們方法的作用。
findUserByAccount
- find :查詢動詞
- User: 主題,大部分場景下可以省略,但是如果以 Distinct 開頭的話,它表示返回的結果不包含重復的記錄。
- Account:斷言,會有一個或多個限制結果的條件。每個條件必須引用一個屬性並且可以指定一個比較操作,如果忽略操作的話就默認是相等比較操作。
方法定義規則
符號 | 作用 |
---|---|
And | 並且 |
Or | 或 |
Is,Equals | 等於 |
Between | 兩者之間 |
LessThan | 小於 |
LessThanEqual | 小於等於 |
GreaterThan | 大於 |
GreaterThanEqual | 大於等於 |
After | 之后(時間)> |
Before | 之前(時間)< |
IsNull | 等於Null |
IsNotNull,NotNull | 不等於Null |
Like | 模糊查詢。查詢件中需要自己加% |
NotLike | 不在模糊范圍內。查詢件中需要自己加% |
StartingWith | 以某開頭 |
EndingWith | 以某結束 |
Containing | 包含某 |
OrderBy | 排序 |
Not | 不等於 |
In | 某范圍內 |
NotIn | 某范圍外 |
TRUE | 真 |
FALSE | 假 |
IgnoreCase | 忽略大小寫 |
自定義查詢方法
除了上面這種符合 DSL 規范的方法命名外,我們也可以自定義不符合命名約定的方法。在方法上面使用 @Query 注解 來為 Spring Data 提供要執行的查詢。
方法定義:
@Query("select name from User where id =:userId")
String getUserName(@Param("userId")Long userId);
單元測試:
@Test
public void testGetUserName(){
Long userId = new Long(1);
String userName = userRepository.getUserName(userId);
Assert.assertEquals("admin",userName);
}
注:語句中表名應該是 ORM 映射的類名,而不是實際的表名
除了持久化框架 SpringJPA 外,比較常用的還有半持久化框架 Mybatis,這兩種框架都能滿足我們的需求,實際的選擇還是看項目場景和個人使用習慣 。