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”这种模式。
配置文件 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>
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);
}
}
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");
}
hibernate4可以利用Hibernate Filter来实现该模式,不同租户通过的数据通过 tenant_id字段或者称为鉴别器来区分。在上述例子中只需要进行下面的修改就可以实现:
在每个数据表需要添加一个字段 tenant_id 以判定数据是属于哪个租户的。
<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>
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 版本中得到根本改变。
根据打印出来的sql语句,我们会发现,hibernate主要是通过在执行sql语句之前,使用Use +数据库名称实现多租户效果的。比如:
User hotel_1
Select * from Guest
个人觉得hibernate对多租户的实现还是很简陋的,目前看并没有跟jpa很好的结合。希望hibernate5中能有所改进,但是,多租户这种思想,以及实现的这样“云”效果,还是很值得我们借鉴和学习的。
下篇文章我们继续说EclipseLink 对多租户的实现。