Spring學習之旅(十二)--持久化框架


對於本職工作來說 JDBC 就可以很好的完成,但是當我們對持久化的需求變得更復雜時,如:

  • 延遲加載
  • 預先抓取
  • 級聯

JDBC 就不能滿足了,我們需要使用 ORM框架 來實現這些需求。

Spring 對多個持久化框架都提供了支持,包括 HibemateIBATISJPA 等。和 SpringJDBC 的支持一樣, SpringORM框架 的支持除了提供與框架的繼承點之外還包括一些附加服務:

  • 支持集成 Spring 聲明式事務;
  • 透明的異常處理;
  • 現場安全的、輕量級的模板類;
  • DAO 支持類;
  • 資源管理

這里我們不會講解所有的 ORM框架,只重點講解下 JPA 的使用。


Java 持久化 API(Java Persistence API,JPA) 是基於 POJO 的持久化機制,它從 HibemateJava 對象(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 對其他持久方案的集成一樣, SprignJPA 集成也提供了 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,這兩種框架都能滿足我們的需求,實際的選擇還是看項目場景和個人使用習慣 。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM