多租户--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




<hibernate-configuration>

    <session-factory>
        <property name="connection.url">jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8property>
        <property name="connection.username">rootproperty>
        <property name="connection.password">wyjproperty>
        <property name="connection.driver_class">com.mysql.jdbc.Driverproperty>
        <property name="dialect">org.hibernate.dialect.MySQLInnoDBDialectproperty>
        <property name="hibernate.connection.autocommit">falseproperty>
        <property name="hibernate.cache.use_second_level_cache">falseproperty>
        <property name="show_sql">falseproperty>

        <property name="hibernate.multiTenancy">SCHEMAproperty>
        
        
        <property name="hibernate.tenant_identifier_resolver">hotel.dao.hibernate.TenantIdResolverproperty>

        
        <property name="hibernate.multi_tenant_connection_provider">hotel.dao.hibernate.SchemaBasedMultiTenantConnectionProviderproperty>

        <mapping class="hotel.model.Guest" />
        

    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 unwrap(Class 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




<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 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("
"
); } //提交事务 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




<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 = new ThreadLocal();

    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 对多租户的实现。

你可能感兴趣的:(java,多租户)