概述:
Hibernate 是一個開放源代碼的對象/關係映射框架和查詢服務。它對 JDBC 進行了輕量級的對象封裝,負責從 Java 類映射到數據庫表,並從 Java 數據類型映射到 SQL 數據類型。在 4.0 版本 Hibenate 開始支持多租戶架構——對不同租戶使用獨立數據庫或獨立 Sechma,並計劃在 5.0 中支持共享數據表模式。
在 Hibernate 4.0 中的多租戶模式有三種,通過 hibernate.multiTenancy 屬性有下面幾種配置:
1. NONE:非多租戶,爲默認值。
2. SCHEMA:一個租戶一個 Schema。
3. DATABASE:一個租戶一個 database。
4. DISCRIMINATOR:租戶共享數據表。計劃在 Hibernate5 中實現。
本篇文章我們主要介紹“一個租戶一個Schema”這種模式。
一個租戶一個Schema
一:設置 hibernate.multiTenancy 等相關屬性。
配置文件 Hibernate.cfg.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="connection.url">jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8</property>
<property name="connection.username">root</property>
<property name="connection.password">wyj</property>
<property name="connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="dialect">org.hibernate.dialect.MySQLInnoDBDialect</property>
<property name="hibernate.connection.autocommit">false</property>
<property name="hibernate.cache.use_second_level_cache">false</property>
<property name="show_sql">false</property>
<!-- <property name="hibernate.hbm2ddl.auto" >create</property> -->
<property name="hibernate.multiTenancy">SCHEMA</property>
<!-- 屬性規定了一個合約,以使 Hibernate 能夠解析出應用當前的 tenantId,-->
<!-- 該類必須實現 CurrentTenantIdentifierResolver 接口,通常我們可以從登錄信息中獲得 tenatId。 -->
<property name="hibernate.tenant_identifier_resolver">hotel.dao.hibernate.TenantIdResolver</property>
<!-- 指定了 ConnectionProvider,即 Hibernate 需要知道如何以租戶特有的方式獲取數據連接 -->
<property name="hibernate.multi_tenant_connection_provider">hotel.dao.hibernate.SchemaBasedMultiTenantConnectionProvider</property>
<mapping class="hotel.model.Guest" />
<!-- <mapping resource="hotel/model/Guest.hbm.xml" /> -->
</session-factory>
</hibernate-configuration>
二:獲取當前 tenantId(用戶標示)
package hotel.dao.hibernate;
import hotel.Login;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
/**
* 獲取專屬用戶的標記。
* @author wyj
* 說明:必須實現 CurrentTenantIdentifierResolver 接口,通常我們可以從登錄信息中獲得 用戶標示信息。
*時間:2015年6月17日 19:40
*/
public class TenantIdResolver implements CurrentTenantIdentifierResolver {
//獲取當前 tenantId
@Override
public String resolveCurrentTenantIdentifier() {
return Login.getTenantId();
}
@Override
public boolean validateExistingCurrentSessions() {
return true;
}
}
三:以租戶特有的方式獲取數據庫連接
package hotel.dao.hibernate;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;
import org.hibernate.HibernateException;
import org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl;
import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider;
import org.hibernate.service.spi.Configurable;
import org.hibernate.service.spi.ServiceRegistryAwareService;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import org.hibernate.service.spi.Stoppable;
/**
* 以租戶特有的方式獲取數據庫連接
* @author wyj
*
* 說明:實現了MultiTenantConnectionProvider 接口,
* 根據 tenantIdentifier 獲得相應的連接。
* 在實際應用中,可結合使用 JNDI DataSource 技術獲取連接以提高性能。
*時間:2015年6月17日 19:40
*/
public class SchemaBasedMultiTenantConnectionProvider implements MultiTenantConnectionProvider, Stoppable,
Configurable, ServiceRegistryAwareService {
private final DriverManagerConnectionProviderImpl connectionProvider = new DriverManagerConnectionProviderImpl();
//得到數據庫連接
@Override
public Connection getAnyConnection() throws SQLException {
return connectionProvider.getConnection();
}
//關閉數據庫連接
@Override
public void releaseAnyConnection(Connection connection) throws SQLException {
connectionProvider.closeConnection(connection);
}
//根據不同用戶,Use對應用戶的庫的鏈接
@Override
public Connection getConnection(String tenantIdentifier) throws SQLException {
final Connection connection = getAnyConnection();
try {
connection.createStatement().execute("USE " + tenantIdentifier);
} catch (SQLException e) {
throw new HibernateException("Could not alter JDBC connection to specified schema [" + tenantIdentifier
+ "]", e);
}
return connection;
}
@Override
public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
try {
connection.createStatement().execute("USE test");
} catch (SQLException e) {
throw new HibernateException("Could not alter JDBC connection to specified schema [" + tenantIdentifier
+ "]", e);
}
connectionProvider.closeConnection(connection);
}
@Override
public boolean isUnwrappableAs(Class unwrapType) {
return this.connectionProvider.isUnwrappableAs(unwrapType);
}
@Override
public <T> T unwrap(Class<T> unwrapType) {
return this.connectionProvider.unwrap(unwrapType);
}
@Override
public void stop() {
this.connectionProvider.stop();
}
@Override
public boolean supportsAggressiveRelease() {
return this.connectionProvider.supportsAggressiveRelease();
}
@Override
public void configure(Map configurationValues) {
this.connectionProvider.configure(configurationValues);
}
//注入服務
@Override
public void injectServices(ServiceRegistryImplementor serviceRegistry) {
this.connectionProvider.injectServices(serviceRegistry);
}
}
四:POJO 類 Guest
package hotel.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* 實體類 Guest
*
* @author wyj
* 說明:表 guest 對應的 POJO 類 Guest,其中主要是一些 getter 和 setter方法
*時間:2015年6月17日 19:40
*/
@Entity
@Table(name = "guest")
public class Guest {
private Integer id;
private String name;
private String telephone;
private String address;
private String email;
@Id
@GeneratedValue
@Column(name = "id", unique = true, nullable = false)
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Column(name = "name", nullable = false, length = 30)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Column(name = "telephone", nullable = false, length = 30)
public String getTelephone() {
return telephone;
}
public void setTelephone(String telephone) {
this.telephone = telephone;
}
@Column(name = "address", nullable = false, length = 255)
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Column(name = "email", unique = true, nullable = false, length = 50)
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Guest.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping default-lazy="true" package="hotel.model">
<class name="Guest" table="guest">
<id name="id" column="id" type="int" unsaved-value="0">
<generator class="native" />
</id>
<property name="name" column="name"/>
<property name="telephone" column="telephone"/>
<property name="address" column="address"/>
<property name="email" column="email"/>
</class>
</hibernate-mapping>
五:以添加用戶爲例測試。
(註冊時已將dataBaseName存入session)
/**
* 添加用戶,根據登錄時的用戶名,判斷該用戶是哪個Schema的。存入session中,在這裏取出。並傳遞下去。
*/
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Session session = null;
Guest guest =null;
List<Guest> list = null;
Transaction tx = null;
//獲取數據庫名稱和頁面傳遞的值
String databaseName = String.valueOf(request.getSession().getAttribute(
"databaseName"));
String name=request.getParameter("name");
String telephone=request.getParameter("telephone");
String address=request.getParameter("address");
String email=request.getParameter("email");
// 加載用戶的庫名稱
Login.setTenantId(databaseName);
// 開啓session和事務
session = sessionFactory.openSession();
tx = session.beginTransaction();
//給實體賦值
guest = new Guest();
guest.setName(name);
guest.setTelephone(telephone);
guest.setAddress(address);
guest.setEmail(email);
//執行保存或更新方法
session.saveOrUpdate(guest);
list = session.createCriteria(Guest.class).list();
StringBuffer sb= new StringBuffer();
for (Guest gue : list) {
sb.append(gue.toString());
sb.append("<br>");
}
//提交事務
tx.commit();
//關閉session
session.close();
request.getSession().setAttribute("userinfo", sb.toString());
System.out.println(sb.toString());
response.sendRedirect("/Hotel1/adduser.jsp");
}
共享數據庫、共享 Schema、共享數據表模式
hibernate4可以利用Hibernate Filter來實現該模式,不同租戶通過的數據通過 tenant_id字段或者稱爲鑑別器來區分。在上述例子中只需要進行下面的修改就可以實現:
一:添加字段 tenant_id
在每個數據表需要添加一個字段 tenant_id 以判定數據是屬於哪個租戶的。
二:對象關係映射文件 Guest.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping default-lazy="true" package="hotel.model">
<class name="HotelGuest" table="hotel_guest">
<id name="id" column="id" type="int" unsaved-value="0">
<generator class="native" />
</id>
<property name="name" column="name"/>
<property name="telephone" column="telephone"/>
<property name="address" column="address"/>
<many-to-one name="tenant" class="Tenant" column="tenant_id" access="field" not-null="true"/>
<filter name="tenantFilter" condition="tenant_id = :tenantFilterParam" />
</class>
<filter-def name="tenantFilter">
<filter-param name="tenantFilterParam" type="string" />
</filter-def>
</hibernate-mapping>
三:獲取 Hibernate Session 的工具類 HibernateUtil
package hotel.dao.hibernate;
import hotel.LoginContext;
import org.hibernate.HibernateException;
import org.hibernate.Session;
public class HibernateUtil {
public static final ThreadLocal<Session> session = new ThreadLocal<Session>();
public static Session currentSession() throws HibernateException {
Session s = session.get();
if (s == null) {
s = sessionFactory.openSession();
String tenantId = LoginContext.getTenantId();
s.enableFilter("tenantFilter").setParameter("tenantFilterParam", tenantId);
session.set(s);
}
return s;
}
public static void closeSession() throws HibernateException {
Session s = session.get();
if (s != null) {
s.close();
}
session.set(null);
}
}
注意:Filter 只是有助於我們讀取數據時顯示地忽略掉 tenantId,但在進行數據插入的時候,我們還是不得不顯式設置相應 tenantId 才能進行持久化。這種狀況只能在 Hibernate5 版本中得到根本改變。
hibernate緩存
- 基於獨立 Schema 模式的多租戶實現,其數據表無需額外的 tenant_id。通過 ConnectionProvider 來取得所需的 JDBC 連接,對其來說一級緩存(Session 級別的緩存)是安全的可用的,一級緩存對事物級別的數據進行緩存,一旦事物結束,緩存也即失效。但是該模式下的二級緩存是不安全的,因爲多個 Schema 的數據庫的主鍵可能會是同一個值,這樣就使得 Hibernate 無法正常使用二級緩存來存放對象。例如:在 hotel_1 的 guest 表中有個 id 爲 1 的數據,同時在 hotel_2 的 guest 表中也有一個 id 爲 1 的數據。通常我會根據 id 來覆蓋類的 hashCode() 方法,這樣如果使用二級緩存,就無法區別 hotel_1 的 guest 和 hote_2 的 guest。
- 在共享數據表的模式下的緩存, 可以同時使用 Hibernate的一級緩存和二級緩存, 因爲在共享的數據表中,主鍵是唯一的,數據表中的每條記錄屬於對應的租戶,在二級緩存中的對象也具有唯一性。Hibernate 分別爲 EhCache、OSCache、SwarmCache 和 JBossCache 等緩存插件提供了內置的 CacheProvider 實現,讀者可以根據需要選擇合理的緩存,修改 Hibernate 配置文件設置並啓用它,以提高多租戶應用的性能。
總結:
根據打印出來的sql語句,我們會發現,hibernate主要是通過在執行sql語句之前,使用Use +數據庫名稱實現多租戶效果的。比如:
User hotel_1
Select * from Guest
個人覺得hibernate對多租戶的實現還是很簡陋的,目前看並沒有跟jpa很好的結合。希望hibernate5中能有所改進,但是,多租戶這種思想,以及實現的這樣“雲”效果,還是很值得我們借鑑和學習的。
下篇文章我們繼續說EclipseLink 對多租戶的實現。