- 使用JDBC雖然簡單, 但代碼比較繁瑣. Spring簡化了數據庫訪問:
- 提供了簡化的JDBC的模板類, 不必動手釋放資源;
- 提供了一個統一的DAO類以實現Data Access Object模式;
- 把
SQLException
封裝爲DataAccessException
, 這個異常是一個RuntimeException
, 並且能夠讓我們能區分SQL異常的原因; - 能方便地集成Hibernate, JPA和MyBatis這些數據庫訪問框架
使用JDBC
-
java使用JDBC訪問數據庫步驟:
- 創建全局
DataSource
實例, 表示數據庫連接池 - 通過
Connection
實例創建PreparedStatement
實例 - 執行SQL語句, 如果是查詢, 則通過
ResultSet
讀取結果集, 如果是修改, 獲取int
結果
- 創建全局
-
關鍵使用
try...finally...
釋放資源, 涉及到事務的代碼需要正確提交或回滾事物 -
在Spring使用JDBC
- 首先通過IoC容器創建並管理一個
DataSource
實例 - 然後Spring提供了一個
JdbcTemplate
, 可以方便地讓我們操作JDBC - 通常情況下, 我們會實例化一個JdbcTemplate. 主要使用了
Template
模式
- 首先通過IoC容器創建並管理一個
@Component
public class UserService {
@Autowired
JdbcTemplate jdbcTemplate;
// 提供了jdbc的`Connection`使用
public User getUserById(long id) {
// 傳入ConnectionCallback
return jdbcTemplate.execute((Connection conn) -> {
// 可以直接使用Connection實例, 不要釋放, 回調結束後JdbcTemplate自動釋放:
// 內部手動創建的PreparedStatement, ResultSet必須用try(...)釋放:
try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
ps.setObject(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return new User(rs.getLong("id"), rs.getString("email"), rs.getString("password"), rs.getString("name"));
}
throw new RuntimeException("user not found by id");
}
}
});
}
public User getUserByName(String name) {
// 需要傳入SQL語句, 以及PreparedStatementCallback
return jdbcTemplate.execute("SELECT * FROM users WHERE name = ?", (PreparedStatement ps) -> {
// PreparedStatement實例已經由JdbcTemplate創建, 並在回調後自動釋放:
ps.setObject(1, name);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return new User(rs.getLong("id"), rs.getString("email"), rs.getString("password"), rs.getString("name"));
}
throw new RuntimeException("user not found by id");
}
});
}
public User getUserByEmail(String email) {
// 傳入SQL, 參數, 和RowMapper實例
// RowMapper可以返回任何Java對象
return jdbcTemplate.queryForObject("SELECT * FROM users WHERE email = ?", new Object[] { email },
(ResultSet rs, int rowNum) -> {
return new User(rs.getLong("id"), rs.getString("email"), rs.getString("password"), rs.getString("name"));
});
}
// 返回多行記錄
public List<User> getUsers(int pageIndex) {
int limit = 100;
int offset = limit * (pageIndex - 1);
return jdbcTemplate.query("SELECT * FROM users LIMIT ? OFFSET ?", new Object[] { limit, offset },
new BeanPropertyRowMapper<>(User.class) // 數據庫結構恰好類似, 可以把一行記錄按照列名轉換爲JavaBean
);
}
// 插入, 更新, 刪除, 需要使用`update()`方法
public void updateUser(User user) {
// 傳入SQL, SQL參數, 返回更新的行數
if (1 != jdbcTemplate.update("UPDATE user SET name = ? WHERE id = ?", user.getName(), user.getId())) {
throw new RuntimeException("User not found by id");
}
}
// `INSERT`操作比較特殊
// 如果某一列是自增列, 通常, 需要獲取插入後的自增值.
// 提供了一個`KeyHolder`簡化操作
public User register(String email, String password, String name) {
// 創建一個KeyHolder
KeyHolder holder = new GeneratedKeyHolder();
if (1 != jdbcTemplate.update(
// 參數1: PrepareStatementCreator
(conn) -> {
// 創建PreparedStatement時, 必須指定RETURN_GENERATED_KEYS:
PreparedStatement ps = conn.prepareStatement("INSERT INFO users(email, password, name) VALUES()",
Statement.RETURN_GENERATED_KEYS);
ps.setObject(1, email);
ps.setObject(2, password);
ps.setObject(3, name);
return ps;
},
// 參數2: KeyHolder
holder)) {
throw new RuntimeException("Insert failed.");
}
return new User(holder.getKey().longValue(), email, password, name);
}
}
-
JdbcTemplate還有許多重載方法.
-
本質是對JDBC操作的一個簡單封裝.
-
目的:
- 減少手動編寫
try(resource) {...}
- 通過
RowMapper
實現了JDBC結果集到Java對象的轉換
- 減少手動編寫
-
用法:
- 針對簡單查詢, 優選
query()
和queryForObject()
, 因爲只需要提供SQL語句, 參數和RowMapper
- 針對更新操作, 優選使用
update()
, 因爲只需要提供SQL語句和參數; - 任何複雜的操作, 最終可以通過
execute(ConnectionCallback)
實現, 因爲拿到Connection
就可以做任何JDBC操作
- 針對簡單查詢, 優選
-
在設計表結構時, 能夠和JavaBean的屬性一一對應, 直接使用
BeanPropertyRowMapper
會很方便. -
操作時候遇到了一個最大的問題, 就是數據庫有兩條數據, 因爲設置了不唯一主鍵, 插入的時候, 一直衝突, 需要添加一條刪除表的語句
@Component
public class DatabaseInitializer {
@Autowired
JdbcTemplate jdbcTemplate;
@PostConstruct
public void init() {
jdbcTemplate.update(" DROP TABLE IF EXISTS users;"
+ "CREATE TABLE IF NOT EXISTS users ( "
+ "id BIGINT IDENTITY NOT NULL PRIMARY KEY, "
+ "email VARCHAR(100) NOT NULL, "
+ "password VARCHAR(100) NOT NULL, "
+ "name VARCHAR(100) NOT NULL, "
+ "UNIQUE (email))"
);
}
}
使用聲明式事務
- Spring提供了一個
PlatformTransactionManager
表示事務管理器. TransactionStatus
表示事務.
TransactionStatus tx = null;
try {
// 開啓事務
tx = txManager.getTransaction(new DefaultTransactionDefinition());
// 相關jdbc操作
jdbcTemplate.update("...");
jdbcTemplate.update("...");
// 提交事務
txManager.commit(tx);
} catch (Exception e) {
// 回滾事務
txManager.rollback(tx);
throw e;
}
-
抽象
PlatformTransactionManager
和TransactionStatus
是爲了支持分佈式事務 -
分佈式事務指多個數據源(多個數據庫, 多個消息系統)要在分佈式環境下實現事務的時候.
-
通過一個分佈式事務管理器實現兩階段提交, 但本身數據庫事務就不快, 基於數據庫事務實現的分佈式事務就非常慢, 使用率不高.
-
Spring爲了同時支持JDBC和JTA兩種事務模型, 就抽象出
PlatformTransactionManager
. -
Spring使用AOP代理, 即通過自動創建Bean的Proxy實現: 對一個聲明式事務方法的事務支持
-
聲明瞭
@EnableTransactionManager
後, 不必額外添加@EnableAspectJAutoProxy
事務回滾
- 發生了
RuntimeException
, Spring的聲明式事務將自動回滾. - 在一個事務中, 如果程序判斷需要回滾事務, 只需要拋出
RuntimeException
@Transactional(rollbackFor = {RuntimeException.class, IoException.class})
public buyProducts(long productId, int num) throws IOException{
...
if (store < num) {
// 庫存不夠, 購買失效
throw new IllegalArgumentException("No enough products");
}
...
}
- 強烈建議業務異常體系從
RuntimeException
中派生, 這樣就不必聲明任何特殊異常即可讓Spring的聲明式事務正常工作
事務邊界
- 在使用事務的時候, 明確事務邊界非常重要.
- 如果一個事務內部, 又調用其他的事務方法, 在回滾的時候, 可能會造成一起回滾的現象.
事務傳播
-
解決事務邊界問題, 定義事務的傳播類型.
-
Spring的聲明式事務爲事務傳播定義了幾個級別, 默認的傳播級別是
REQUIRED
. -
如果當前沒有事務, 就創建一個新事務, 如果當前有事務, 就加入到當前事務中執行.
-
這樣整個事務邊界就清晰了: 只有一個事務, 就是
UserService.register()
. -
這樣每個事務就都是單獨且清晰的.
-
事務傳播級別:
REQUIRED
: 默認, 沒有事務, 就創建一個, 有, 就加入SUPPORTS
: 如果有事務, 就加入, 沒有, 自己也不開啓事務執行. 一般用在查詢方法MANDATORY
REQUIRES_NEW
: 不管當前有沒有, 都必須開啓一個新的事務執行. 如果當前有事務, 那麼當前事務會掛起, 等新事物完成後, 再恢復執行;NOT_SUPPORTED
NEVER
NOT_SUPPORTED
NESTED
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Product createProduct() {
}
Spring如何傳播事務
// jdbc中事務寫法
Connection con = openConnection();
try {
// 關閉自動提交
con.setAutoCommit(false)
// 執行多條SQL語句
insert();
update();
delete();
// 提交事務
con.commit();
} catch (SQLException e) {
// 回滾事務
con.rollback();
} finally {
con.setAutoCommit(true)
con.close();
}
-
使用
ThreadLocal
-
Spring總把JDBC相關的
Connection
和TransactionStatus
實例綁定到ThreadLocal
-
如果一個事務方法從
ThreadLocal
中未取到事務, 那麼它會打開一個新的JDBC鏈接, 同時開啓一個事務. -
否則, 就直接從
ThreadLocal
獲取JDBC鏈接以及TransactionStatus
-
因此事務支取之前的前提是, 方法調用是在一個線程內執行.
@Transactional
public User register(String email, String password, String name) { // BEGIN TX-A
User user = jdbcTemplate.insert("...");
new Thread(() -> {
// BEGIN TX-B
bonusService.addBuns(user.id, 100)
// END TX-B
}).start();
} // END TX-A
- 事務只能在當前線程傳播, 無法跨躍線程傳播
使用DAO
-
傳統的多層應用程序中, 通常是Web層調用業務層, 業務層調用數據訪問層.
-
業務層負責處理各種業務邏輯, 數據訪問層只負責對數據進行增刪改查.
-
實現數據訪問層就是用
JdbcTemplate
實現對數據庫的操作. -
DAO: Data Access Object
public class AbstractDao<T> extends JdbcDaoSupport{
private String table;
private Class<T> entityClass;
private RowMapper<T> rowMapper;
@Autowired
private JdbcTemplate jdbcTemplate;
@PostConstruct
public void init() {
super.setJdbcTemplate(jdbcTemplate);
}
public AbstractDao() {
// 獲取當前類型的泛型類型
this.entityClass = getParameterizedType();
this.table = this.entityClass.getSimpleName().toLowerCase() + "s";
this.rowMapper = new BeanPropertyRowMapper<>(entityClass);
}
public T getById(long id) {
return getJdbcTemplate().queryForObject(
"SELECT * FROM " + table + " WHERE id = ?",
this.rowMapper,
id
);
}
public List<T> getAll(int pageIndex) {
int limit = 100;
int offset = limit * (pageIndex - 1);
return getJdbcTemplate().query(
"SELECT * FROM " + table + " LIMIT ? OFFSET ?",
new Object[] {limit, offset},
this.rowMapper
);
}
public void deleteById(long id) {
getJdbcTemplate().update("DELETE FROM " + table + " WHERE id = ? ", id);
}
public RowMapper<T> getRowMapper() {
return this.rowMapper;
}
private Class<T> getParameterizedType() {
...
}
}
- 這樣每個子類都會有了這些通用方法
@Component
@Transactional
public class UserDao extends AbstractDao<User> {
// 已經有了:
// User getUserById(long)
// List<User> getAll(int)
// void deleteById(long)
}
@Component
@Transactional
public class BookDao extends AbstractDao<Book> {
// 已經有了:
// Book getById(long)
// List<Book> getAll(int)
// void deleteById(long)
}
- DAO模式是一種簡單的數據訪問模式, 根據實際情況, 是否使用DAO.
- 直接在Service層操作數據庫也是完全沒有問題的.
集成Hibernate
-
使用
JdbcTemplate
的時候, 我們用的最多的方法就是List<T> query(String sql, Object[] args, RowMapper rowMapper)
-
RowMapper
的作用: 把ResultSet
的一行記錄映射爲Java Bean. -
這種關係數據庫的表記錄映射爲Java對象的過程就是ORM: Object-Relational Mapping.
-
ORM可以把記錄轉換爲Java對象, 也可以把Java對下個轉換爲行記錄.
-
Hibernate作爲ORM框架, 可以替代
JdbcTemplate
, 但仍然需要JDBC驅動. -
所以我們需要引入JDBC驅動, 連接池, 已經Hibernate本身.
-
使用Hibernate時, 不要使用基本類型的屬性, 總是使用包裝類型, 如Long或Integer
-
使用Spring集成Hibernate, 配合JPA註解, 無需任何額外的XML配置
-
抽象一層, 可以直接注入通用屬性
@MappedSuperclass // 表示用於繼承
public abstract class AbstractEntity {
private Long id;
private Long createdAt;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, updatable = false)
public Long getId() {
return id;
}
@Column(nullable = false, updatable = false)
public Long getCreatedAt() {
return createdAt;
}
@Transient // 表示虛擬屬性, 不從數據庫讀取
public ZonedDateTime getCreatedDateTime() {
return Instant.ofEpochMilli(this.createdAt).atZone(ZoneId.systemDefault());
}
@PrePersist // 表示JavaBean持久化到數據庫之前(INSERT), 會先執行這個方法.
public void preInsert() {
setCreatedAt(System.currentTimeMillis());
}
public void setCreatedAt(Long createdAt) {
this.createdAt = createdAt;
}
public void setId(Long id) {
this.id = id;
}
}
@Entity
public class Book extends AbstractEntity {
private String title;
@Column(nullable = false, updatable = false)
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
插入
public User register(String email, String password, String name) {
// 創建一個對象
User user = new User();
// 設置好屬性
user.setEmail(email);
user.setPassword(password);
user.setName(name);
// 不用設置id, 因爲設置了自增主鍵, 保存到數據庫
System.out.print(hibernateTemplate);
hibernateTemplate.save(user);
// 現在已經自動獲得了id;
System.out.println(user.getId());
return user;
}
刪除
public boolean deleteUser(Long id) {
// 先根據主鍵加載記錄
// get: 返回null
// load: 拋出異常
User user = hibernateTemplate.get(User.class, id);
if (user != null) {
hibernateTemplate.delete(user);
return true;
}
return false;
}
更新
public void updateUser(Long id, String name) {
User user = hibernateTemplate.load(User.class, id);
user.setName(name);
hibernateTemplate.update(user);
}
查詢
- findByExample
- criteria: 可以實現任意複雜的查詢
- HQL:
public User login(String email, String password) {
User example = new User();
example.setEmail(email);
example.setPassword(password);
List<User> list = hibernateTemplate.findByExample(example);
// 在使用findByExample時, 基本類型字段總會加入到WHERE條件.
return list.isEmpty() ? null : list.get(0);
}
public User login(String email, String password) {
DetachedCriteria criteria = DetachedCriteria.forClass(User.class);
criteria.add(Restrictions.eq("email", email));
criteria.add(Restrictions.eq("password", password));
List<User> list = (List<User>) hibernateTemplate.findByCriteria(criteria);
return list.isEmpty() ? null : list.get(0);
}
@NamedQueries(
@NamedQuery(
name = "login",
query = "SELECT u FROM User u WHERE u.email=?0 AND u.password=?1"
)
)
public class User extends AbstractEntity{
...
}
public User login(String email, String password) {
List<User> list = (List<User>) hibernateTemplate.findByNamedQuery("login", email, password);
return list.isEmpty() ? null : list.get(0);
}
使用Hibernate原生接口
- 原生接口總是從
SessionFactory
出發, 通常用全局變量存儲. - 在
HibernateTemplate
中以成員變量注入.
void operation() {
Session session = null;
boolean isNew = false;
// 獲取當前Session或者打開新的Session
try {
session = this.sessionFactory.getCurrentSession();
} catch (HibernateException e) {
session = this.sessionFactory.openSession();
isNew = true;
}
// 操作Session
try {
User user = session.load(User.class, 123L);
}
finally {
// 關閉新打開的Session
if (isNew) {
session.close();
}
}
}
集成JPA
- JPA: Java Persistence API, 是ORM標準
- 如果使用JPA, 引用:
javax.persistence
, 不再是org.hibernate
第三方包 - JPA只是一個接口, 需要一個實現產品, 例如
Hibernate
@Bean
LocalContainerEntityManagerFactoryBean createEntityManagerFactory(@Autowired DataSource dataSource) {
LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
// 設置DataSource
entityManagerFactoryBean.setDataSource(dataSource);
// 掃面package
entityManagerFactoryBean.setPackagesToScan("com.zhangrh.spring.entity");
// 指定JPA的提供商是Hibernate:
JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter);
// 設定特定提供商自己的配置
Properties props = new Properties();
props.setProperty("hibernate.hbm2ddl.auto", "update");
props.setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
props.setProperty("hibernate.show_sql", "true");
entityManagerFactoryBean.setJpaProperties(props);
return entityManagerFactoryBean;
}
@Bean
PlatformTransactionManager createTxManager(@Autowired EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
-
使用Spring + Hibernate作爲API的實現, 無需任何配置文件.
-
Entity Bean的配置和上一節完全相同, 全部採用Annotation標註.
-
JDBC, Hibernate, JPA關係
- DataSource SessionFactory EntityManagerFactory
- Connection Session EntityManager
-
@PersistenceContext // Spring會自動注入
EntityManager
代理, 該代理類會在必要的時候自動打開EnetityManager
-
多線程引用的
EntityManager
雖然是一個代理類, 但該代理類內部針對不同線程會創建不同的EntityManager
實例 -
@Persistence的
EntityManager
可以多線程安全的共享
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> q = cb.createQuery(User.class);
Root<User> r = q.from(User.class);
q.where(cb.equal(r.get("email"), cb.parameter(String.class, "e")));
TypedQuery<User> query = em.createQuery(q);
// 綁定參數
query.setParameter("e", email);
// 執行查詢
List<User> list = query.getResultList();
return list.isEmpty() ? null : list.get(0);
// JPQL查詢
TypedQuery<User> query = em.createQuery("SELECT u FROM User u WHERE u.email = :e", User.class);
query.setParameter("e", email);
List<User> list = query.getResultList();
if (list.isEmpty()) {
throw new RuntimeException("User not found by email");
}
return list.get(0);
public User login(String email, String password) {
TypedQuery<User> query = em.createNamedQuery("login", User.class);
query.setParameter("e", email);
query.setParameter("p", password);
List<User> list = query.getResultList();
return list.isEmpty() ? null : list.get(0);
}
public User register(String email, String password, String name) {
User user = new User();
user.setEmail(email);
user.setPassword(password);
user.setName(name);
em.persist(user);
return user;
}
public void updateUser(Long id, String name) {
User user = getUserById(id);
user.setName(name);
em.refresh(user);
}
public void deleteUser(Long id) {
User user = getUserById(id);
em.remove(user);
}
集成MyBatis
- ORM框架的主要工作就是把ResultSet的每一行編程Java Bean.
- 或者把Java Bean自動轉換到INSERT或UPDATE語句的參數中去, 從而實現ORM
- 因爲我們在Java Bean的屬性上給了足夠的註解作爲元數據
- ORM獲取Java Bean的註解之後, 知道如何進行映射
- 通過
Proxy
模式, 對每個setter方法進行覆寫, 達到update()
目的
public class UserProxy extends User{
Session _session;
boolean _isNamedChanged;
public void setName(String name) {
super.setName(name);
_isNamedChanged = true;
}
// 獲取User對象關聯的Address對象
public Address getAddress() {
Query q = _session.createQuery("from Address where userId = :userId");
q.setParameter("userId", this.getId());
List<Address> list = query.list();
return list.isEmpty() ? null : list(0);
}
}
-
Proxy必須保持當前的Session, 事務提交後, Session自動關閉, 要麼無法訪問, 要麼數據不一致.
-
ORM總是引入Attached/Detached, 表示此Java Bean到底是在Session的範圍內, 還是脫離了Session編程了一個"遊離對象".
-
ORM提供了緩存
- 一級緩存: 指在一個Session範圍內的緩存, 例如根據主鍵查詢時候, 兩次查詢返回同一個實例
- 二級緩存: 跨Session緩存, 默認關閉. 二級緩存極大的增加了數據的不一致性
-
JdbcTemplate和ORM相比:
- 查詢後需要手動提供Mapper實例, 以便把ResultSet的每一行變爲Java對象
- 增刪改操作所需參數列表, 需要手動傳入, 即把User實例變爲[user.id, user.name, user.email]這樣的列表, 比較麻煩
-
jdbcTemplate
- 優勢: 確定性, 每次讀取數據庫一定是數據庫操作, 而不是緩存, 所執行的SQL是完全確定的.
- 缺點: 代碼比較繁瑣, 構造
INSERT INTO users VALUES(?,?,?)
更加複雜
-
半自動ORM框架:
MyBatis
:- 只負責ResultSet自動映射到Java Bean
- 自動填充Java Bean參數
- 需要自己寫出SQL
-
JDBC | Hibernate | JPA | MyBatis
-
DataSource | SessionFactory | EntityManagerFactory | SqlSessionFactory
-
Connection | Session | EntityManager | SqlSession
-
MyBatis使用Mapper來實現映射.
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User getById(@Param("id") long id);
@Select("SELECT * FROM users LIMIT #{offset}, #{maxResults}")
List<User> getAll(@Param("offset") int offset, @Param("maxResults") int maxResults);
}
- MyBatis執行查詢後, 將根據方法的返回類型自動把ResultSet的每一行轉換爲User實例
- 轉換規則按照列名和屬性名對應
- 如果對應不成, 改寫sql語句:
-- 列名: created_time; 屬性名: createdAt
SELECT id, name, email, created_time AS createdAt FROM users
@MapperScan("com.zhangrh.spring.mapper") //自動創建所有mapper的實現類
public class AppConfig {
// ...
}
public class UserService {
@Autowired
UserMapper userMapper;
public User getUserById(long id) {
User user = userMapper.getById(id);
if (user == null) {
throw new RuntimeException("User not found by id");
}
return user;
}
}
XML配置方式
-
xml可以動態組裝輸出sql, 但是配置繁瑣, 不推薦使用
-
使用MyBatis最大的問題: 所有的sql全部需要手寫
-
優點: sql是我們自己寫的, 優化簡單, 可以編寫任意負責sql
-
切換數據庫不太方便, 但是大部分項目沒有切換數據庫的需求
設計ORM
- ORM: 建立在JDBC的基礎上, 通過ResultSet到JavaBean的映射, 實現各種查詢.
設計ORM接口
// todo: 不再看了, 暫時達成能用就成. 後面補上.