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