多租戶--hibernate實現

概述:

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&amp;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緩存

  1. 基於獨立 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。
  2. 在共享數據表的模式下的緩存, 可以同時使用 Hibernate的一級緩存和二級緩存, 因爲在共享的數據表中,主鍵是唯一的,數據表中的每條記錄屬於對應的租戶,在二級緩存中的對象也具有唯一性。Hibernate 分別爲 EhCache、OSCache、SwarmCache 和 JBossCache 等緩存插件提供了內置的 CacheProvider 實現,讀者可以根據需要選擇合理的緩存,修改 Hibernate 配置文件設置並啓用它,以提高多租戶應用的性能。

總結:

根據打印出來的sql語句,我們會發現,hibernate主要是通過在執行sql語句之前,使用Use +數據庫名稱實現多租戶效果的。比如:
User hotel_1
Select * from Guest

個人覺得hibernate對多租戶的實現還是很簡陋的,目前看並沒有跟jpa很好的結合。希望hibernate5中能有所改進,但是,多租戶這種思想,以及實現的這樣“雲”效果,還是很值得我們借鑑和學習的。
下篇文章我們繼續說EclipseLink 對多租戶的實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章