久しぶりの投稿は、またまたMyBatisGeneratorネタです。
お仕事でも急遽対応してプラグインを作成したので、その備忘録も兼ねて書いときます。
MySQLで大量のデータをテーブル管理したときとかに同じテーブル名にSuffixをつけてアプリ側で条件によってSuffixを出し分けて使ったりする手法があります。
主にソーシャルゲームとかで使われる手法なのですが、
大量のユーザーを管理するのに一つのテーブルだとインデックス等が肥大化してスロークエリにつながってしまうので、
アプリ側でシャーディングしてテーブルのレコード件数を数百万程度に抑えるという手法です。
では、実際にSuffixに対応する方法ですが、
1.SqlMapFileをtable_${Suffix}で生成する。
2.マッピングクラスでSuffixを設定できるようにする。
の2点が主に対応する要件になります。
ます2を実現するために、モデルクラスにメンバーを追加します。
/**
* ModelクラスにSuffix対応を入れる
*
* @param topLevelClass
*/
private void setTableNameSuffixModel(TopLevelClass topLevelClass) {
Field tableNameSuffix = new Field();
tableNameSuffix.setName("tableNameSuffix");
tableNameSuffix.setVisibility(JavaVisibility.PROTECTED);
tableNameSuffix.setType(FullyQualifiedJavaType.getStringInstance());
topLevelClass.addField(tableNameSuffix);
Method tableNameSuffixSet = new Method();
tableNameSuffixSet.setVisibility(JavaVisibility.PUBLIC);
tableNameSuffixSet.setName("setTableNameSuffix");
tableNameSuffixSet.addParameter(new Parameter(FullyQualifiedJavaType.getStringInstance(), "tableNameSuffix"));
tableNameSuffixSet.addBodyLine("this.tableNameSuffix = tableNameSuffix;");
topLevelClass.addMethod(tableNameSuffixSet);
Method tableNameSuffixGet = new Method();
tableNameSuffixGet.setVisibility(JavaVisibility.PUBLIC);
tableNameSuffixGet.setReturnType(FullyQualifiedJavaType.getStringInstance());
tableNameSuffixGet.setName("getTableNameSuffix");
tableNameSuffixGet.addBodyLine("return tableNameSuffix;");
topLevelClass.addMethod(tableNameSuffixGet);
}
これは何度となくやってきたので、簡単に実現できます。
問題なのは1のSqlMapFileのカスタマイズです。
プラグインの処理は各ジェネレート処理の最後に呼ばれるので、メソッドを追加したり処理を入れたりということは簡単にできます。
今回やりたいことは、既存のジェネレートした部分を書き換えなければいけないので、XMLエレメントを追加して終わりというわけにはいかないのです。
<if test="tableNameSuffix == null">tableName</if>
<if test="tableNameSuffix != null">tableName${tableNameSuffix}</if>
テーブル名が記述されている箇所すべてをこういう処理に置き換える必要があります。
Javaなんだから内部でXMLのコレクションからテーブルの要素を検索して、
置き換えればいけるんじゃないかなんていう淡い期待は見事に打ち砕かれます(笑)
内部処理を解析するとSQLベタ書き状態になっているところもあって、
コレクションをちょいちょいと置き換えるなんてスマートなやり方もできません。
また、テーブル名を出力している処理が共通メソッドに切り出されていたりもしないので、
ジェネレートクラスを継承して独自に実装しても生成処理部分を全部実装する羽目になります。
なので、こんな時には最終手段のString.replace作戦ですw
/**
* SQLMAPファイルのテーブル名部分をSuffix対応に置き換える
*
* @param element
* @param introspectedTable
*/
private void setTableNameSuffixElement(XmlElement element, IntrospectedTable introspectedTable) {
StringBuilder sb = getTableNameSuffixContent(introspectedTable);
List<Element> elements = new ArrayList<Element>();
for (Element el : element.getElements()) {
String st = el.getFormattedContent(0)
.replace(introspectedTable.getFullyQualifiedTableNameAtRuntime(), sb.toString());
elements.add(new TextElement(st));
}
element.getElements().clear();
element.getElements().addAll(elements);
}
/**
* suffix対応のContentを返す
*
* @param introspectedTable
* @return
*/
private StringBuilder getTableNameSuffixContent(IntrospectedTable introspectedTable) {
XmlElement noSuffixElement;
noSuffixElement = new XmlElement("if");
noSuffixElement.addAttribute(new Attribute("test", "tableNameSuffix == null"));
noSuffixElement.addElement(new TextElement(introspectedTable.getFullyQualifiedTableNameAtRuntime()));
XmlElement suffixElement;
suffixElement = new XmlElement("if");
suffixElement.addAttribute(new Attribute("test", "tableNameSuffix != null"));
suffixElement.addElement(
new TextElement(introspectedTable.getFullyQualifiedTableNameAtRuntime() + "${tableNameSuffix}"));
StringBuilder sb = new StringBuilder();
OutputUtilities.newLine(sb);
sb.append(noSuffixElement.getFormattedContent(0));
OutputUtilities.newLine(sb);
sb.append(suffixElement.getFormattedContent(0));
OutputUtilities.newLine(sb);
return sb;
}
生成されたXMLを全部コピーしながら、置き換え処理をしていきます。
Javaの言語仕様でメソッド引数は参照渡しになります。
なので、引数のオブジェクトを違うオブジェクトに置き換えることはできないのですが、
引数のコレクションをclearしてaddAllするという荒業で上書きして書き換えることができます(笑)
これで何とかSqlMapFileの中身は書き換えることに成功しました。
ただ、これで終わりではありませんw
MyBatisGeneratorのデフォルト設定ではPrimaryKeyがひとつのテーブルの場合、
SelectByPrimaryKeyのメソッド引数をご親切にプリミティブのラッパークラスにしてくれちゃいます。
SelectByPrimaryKey(Integer key)ってされてしまうと、せっかくモデルクラスにSuffixを持たせてもSqlにマッピングすることができなくなってしまいます。
幸いなことにその場合でもモデルクラスを渡すようにジェネレートしてくれる設定があります。
generatorConfigurationのtableタグにmodelType=”hierarchical”を指定します。
<table tableName="%" modelType="hierarchical" />
この設定にするとPrimaryKeyにマッピングするEntityクラスを生成して、引数にしてくれるのでSuffixをマッピングすることが可能になります。
最後は、それぞれの処理をジェネレートのタイミングに合わせて呼ばれるように各メソッドをオーバーライドしていきます。
まずはモデルクラスに追加します。
通常のEntityクラス、Exampleクラス、PrimaryKeyクラスの3つ。
@Override
public boolean modelBaseRecordClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
setTableNameSuffixModel(topLevelClass);
return true;
}
@Override
public boolean modelExampleClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
setTableNameSuffixModel(topLevelClass);
return true;
}
@Override
public boolean modelPrimaryKeyClassGenerated(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
setTableNameSuffixModel(topLevelClass);
return true;
}
あとは各SqlMapファイルのクエリごとにSuffix対応に置き換えていきます。
@Override
public boolean sqlMapCountByExampleElementGenerated(XmlElement element, IntrospectedTable introspectedTable) {
setTableNameSuffixElement(element, introspectedTable);
return true;
}
@Override
public boolean sqlMapDeleteByExampleElementGenerated(XmlElement element, IntrospectedTable introspectedTable) {
setTableNameSuffixElement(element, introspectedTable);
return true;
}
@Override
public boolean sqlMapDeleteByPrimaryKeyElementGenerated(XmlElement element, IntrospectedTable introspectedTable) {
setTableNameSuffixElement(element, introspectedTable);
return true;
}
@Override
public boolean sqlMapInsertElementGenerated(XmlElement element, IntrospectedTable introspectedTable) {
setTableNameSuffixElement(element, introspectedTable);
return true;
}
@Override
public boolean sqlMapInsertSelectiveElementGenerated(XmlElement element, IntrospectedTable introspectedTable) {
setTableNameSuffixElement(element, introspectedTable);
return true;
}
@Override
public boolean sqlMapSelectAllElementGenerated(XmlElement element, IntrospectedTable introspectedTable) {
setTableNameSuffixElement(element, introspectedTable);
return true;
}
@Override
public boolean sqlMapSelectByExampleWithBLOBsElementGenerated(XmlElement element,
IntrospectedTable introspectedTable) {
setTableNameSuffixElement(element, introspectedTable);
return true;
}
@Override
public boolean sqlMapSelectByExampleWithoutBLOBsElementGenerated(XmlElement element,
IntrospectedTable introspectedTable) {
setTableNameSuffixElement(element, introspectedTable);
return true;
}
@Override
public boolean sqlMapSelectByPrimaryKeyElementGenerated(XmlElement element, IntrospectedTable introspectedTable) {
setTableNameSuffixElement(element, introspectedTable);
return true;
}
@Override
public boolean sqlMapUpdateByExampleSelectiveElementGenerated(XmlElement element,
IntrospectedTable introspectedTable) {
setTableNameSuffixElement(element, introspectedTable);
return true;
}
@Override
public boolean sqlMapUpdateByExampleWithBLOBsElementGenerated(XmlElement element,
IntrospectedTable introspectedTable) {
setTableNameSuffixElement(element, introspectedTable);
return true;
}
@Override
public boolean sqlMapUpdateByExampleWithoutBLOBsElementGenerated(XmlElement element,
IntrospectedTable introspectedTable) {
setTableNameSuffixElement(element, introspectedTable);
return true;
}
@Override
public boolean sqlMapUpdateByPrimaryKeySelectiveElementGenerated(XmlElement element,
IntrospectedTable introspectedTable) {
setTableNameSuffixElement(element, introspectedTable);
return true;
}
@Override
public boolean sqlMapUpdateByPrimaryKeyWithBLOBsElementGenerated(XmlElement element,
IntrospectedTable introspectedTable) {
setTableNameSuffixElement(element, introspectedTable);
return true;
}
@Override
public boolean sqlMapUpdateByPrimaryKeyWithoutBLOBsElementGenerated(XmlElement element,
IntrospectedTable introspectedTable) {
setTableNameSuffixElement(element, introspectedTable);
return true;
}
一応、BLOBとかSelectAllとか通常は生成されないクエリにも入れてあります。
だいぶ長くなりましたが、これでやっと完成です。
まぁここまでいじくると流石に内部処理もだいぶ見えてきて、なんでも来い状態ですな(笑)
次回はMyBatisからちょっと離れて久々にSpringBatchとかやってみようかしら。。
最近のバージョンはアドミンツールとかあるらしく、ぜひ試してみたいですね。
コメント