问题
分库分表的方式在当前的程序设计中经常出现,一个实体类对应数据库多张表的情况屡见不鲜(数据库中表的字段当然也是一样的),Hibernate框架在处理单个实体类对应单个数据库表的情况下出来没有任何问题.但是问题来了,如何单个实体类对应多个数据库表?
想要的结果
@Entity
@Table(name = "clues")
public class CluesDto {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "clues_name")
private String cluesName;
//......一系列的setter/getter方法
}
现在想把这个实体类映射到数据库当中,让他按照月份自动生成:clues_01,clues_02,clues_03......clues_12
(有可能有按照天生成366张表),虽然,你可以写脚本,但是如果是一个项目性质的东西,需要部署到不同类型
的数据库(mysql,oracle,db2,postgresql,sysbase等),修改脚本很麻烦.
解决方案
跟踪Hibernate源码
跟踪Hibernate创建表结构的源码,跟踪到最后发现org.hibernate.tool.schema.internal.SchemaCreatorImpl这
个类负责了一切的数据库初始化操作,而最关键的就是第103行的这个方法.
@Override
public void doCreation(
Metadata metadata,
ExecutionOptions options,
SourceDescriptor sourceDescriptor,
TargetDescriptor targetDescriptor) {
if ( targetDescriptor.getTargetTypes().isEmpty() ) {
return;
}
final JdbcContext jdbcContext = tool.resolveJdbcContext( options.getConfigurationValues() );
final GenerationTarget[] targets = tool.buildGenerationTargets(
targetDescriptor,
jdbcContext,
options.getConfigurationValues(),
true
);
doCreation( metadata, jdbcContext.getDialect(), options, sourceDescriptor, targets );
}
debug这段代码,发现只有第一个参数Metadata带有表数据,那么就说明Metadata这个参数是一个主导者,只要
我们修改了Metadata-->Datasource-Namespaces→Tables信息,那么我们就有可能单个实体类创建多个数据库表,
跟随这个想法,那么就看看如何修改Metadata这个数据了,沿着水源往上找,发现:
在org.hibernate.internal.SessionFactoryImpl类的构造方法中(不知道为啥构造方法写那么多逻辑,也不拆
分开几个方法),在285行的样子
for ( Integrator integrator : serviceRegistry.getService( IntegratorService.class ).getIntegrators() ) {
integrator.integrate( metadata, this, this.serviceRegistry );
integratorObserver.integrators.add( integrator );
}
查看Integrator接口,看到是在:org.hibernate.integrator.spi包下面,那么就可以使用Java的SPI技术了,
之后通过反射技术拿到Metadata当中的Tables的Map对象,接着就可以操作实验了
Integrator接口实现(SPI机制的形式,类似于Java的数据库驱动的写法)
public class MultipleTableIntegrator implements Integrator {
private static final Logger logger = LoggerFactory.getLogger(MultipleTableIntegrator.class);
private static final String MONTH_DISTRIBUTION_TABLE_NAME_CLUES = "clues";
@Override
public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
logger.info("可以通过SPI的方式来操作这个接口");
Optional.ofNullable(metadata)
.map(Metadata::getDatabase)
.map(Database::getNamespaces)
.ifPresent(namespaces -> namespaces.forEach(namespace -> {
try {
//这个地方主要是想获取Namespace当中的tables属性,只有通过反射获取,拿到这个属性之后,
//这个属性是一个Map<Identifier,Table>,之后你把需要重复的表往Map里面装就行了
Field fieldTables = namespace.getClass().getDeclaredField("tables");
fieldTables.setAccessible(true);
Object value = fieldTables.get(namespace);
if (value instanceof Map) {
Object cluesTable = ((Map) value).get(Identifier.toIdentifier(MONTH_DISTRIBUTION_TABLE_NAME_CLUES));
IntStream.rangeClosed(1, 12)
.boxed()
.forEach(month -> {
String name = String.join("_", MONTH_DISTRIBUTION_TABLE_NAME_CLUES, month.toString());
Identifier key = Identifier.toIdentifier(name);
((Map) value).put(key, cloneTable((Table) cluesTable, name));
});
}
} catch (NoSuchFieldException | IllegalAccessException e) {
logger.info("反射出现错误...");
logger.error(e.getMessage(), e);
}
}));
}
@Override
public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
logger.info("可以通过SPI方式....");
}
//这个方法请根据自己的需要,朝向这个Map里面装,注意IdentifierName不要重复了
//如果存在外键的标识重复,请自己手动修改下
private Table cloneTable(Table source, String identifierName) {
Table target = new Table();
BeanUtils.copyProperties(source, target);
target.setName(identifierName);
source.getColumnIterator().forEachRemaining(o -> {
target.addColumn((Column) o);
});
return target;
}
}
配置META-INF信息
在resource下面,新建META-INF文件夹,接着建立services文件夹,接着创建:org.hibernate.integrator.spi.Integrator为文件名的文件,内容写上:org.lx.bird.framework.springboot.client.spi.MultipleTableIntegrator(这个是你本地实现的类全路径)
一定要记得在pom.xml修改打包的资源过滤,在resources那个过滤环节修改过滤的文件规则,要不然上面的文件打包不进去
不明白Java的SPI机制的,可以上网搜索下
后话
在本次探索过程中,认识到了字节码技术的强大,感觉,拥有了字节码技术,也就是javaagent启动带有这个参数,你就可以随便修改源码 了,在载入class文件的时候,你可以修改这个tables所在的调用,然后手动的直接添加进去,不过,我还没有试验成功,已经试验到可以 在某个确定的行数添加一行代码了!