mybatis-generatorを使うことで、各テーブルを操作するためのクラス群を容易に準備することができます。しかしながら、mybatis-generatorが提供する機能では、システム開発で求められる要件を満たせない場合があります。
このような状況に対応できるよう、ここではmybatis-generatorプラグインの実装方法について説明します。
概要
mybatis-generatorが生成するクラスやファイルを要件に応じてカスタマイズ方法として、mybatis-generatorクラスを継承する方法と、プラグインを作成する方法があります。ここでは、プラグインを使って成果物をカスタマイズする方法を説明します。
mybatis-generator自体の使い方については、公式サイトやその他参考サイトをご覧ください。
ここで説明するサンプルのプロジェクトはこちらで公開しています。
前提条件
- 使用しているmybatis-generatorのバージョンは1.4.0です。開発や動作確認は、Windows 10 + Eclipse Photon Release (4.8.0)で行っています。
- 動作確認で使用しているDB環境は、CentOS7 + mariadb5.6です。
- 以降では、mybatis-generatorを「MBG」と表記します。mybatis-generatorの設定ファイルであるgeneratorConfig.xmlのことを「設定ファイル」と表記します。
MBGの成果物
mybatisではDB操作を行うためのクラス・ファイルとして、Javaクライアント、モデル、SQLマップ(XML)を使用できます。動的SQLの使用、Kotlin環境等の実行環境によって、使用されるクラス・ファイルが変わってきます。
MBGでは、いくつかの実行環境に向けたクラス・ファイル(以後、成果物と表記)を生成することができます。サポートする実行環境とその環境に合わせた成果物の詳細については公式サイトをご覧ください。
(どの環境向けの成果物を生成するかは、設定ファイルのcontext要素のtargetRuntime属性で指定します。)
成果物 | 説明 |
---|---|
Javaクライアント | Javaのプログラムからmybatisを操作するためのクライアントクラス/インターフェイスであり、insert/update等のテーブル操作を行うためのメソッドを含んでいます。プログラマは、このクラス/インターフェイスを使って、DB操作を行うことになります。具体的な成果物例として、マッパーインターフェイスがあります。 |
モデル | テーブル検索時の条件や検索結果のデータを格納するクラスであり、JavaクライアントやSQLマップで使用します。 指定されている実行環境に応じて、PrimaryKey、Record、RecordWithBlob、Exampleクラスが作成されます。 |
SQLマップ | Javaクライアントに定義されたinsertメソッド等に対応するSQL文がXMLで定義されたファイルです。 指定されている実行環境によっては作成されません。 |
MBGの拡張方法
MBGの拡張方法として、MBGの主要クラスを継承した独自クラスを作成する方法、MBGの成果物作成処理の過程の処理を修正するためのプラグインクラスを作成する方法があります。
ほとんどの場合はプラグインクラスを作成する方法で十分であるため、本稿ではプラグインを使ったカスタマイズ方法について説明します。
なお、前者の継承を使う場合、MBGでは次のクラスを継承できる仕組みになっています。
詳細は公式サイトの説明をご覧ください。
拡張ポイント(クラス) | 説明 |
---|---|
IntrospectedTable | FreeMakerやVelocityを使って成果物の内容を大きく変更する場合は良いが、基本的にはこのクラスを拡張するべきではない。 このクラスを拡張する場合、設定ファイルのcontext要素のtargetRuntime属性値に拡張したクラス名を指定する。(通常、この属性値はMyBatis3DynamicSql, MyBatis3等の既定値を指定するが、独自クラスを使う場合はクラス名を指定する。) |
IntrospectedColumn | カラムに関するDBのメタ情報を格納するクラスである。 このクラスを拡張する場合は、設定ファイルのcontext要素のintrospectedColumnImpl属性値に拡張したクラス名を指定する。 |
JavaTypeResolver | DB構造を解析する際にDBの型(JDBC型)からJava型に変換するためのクラスであり、独自の型変換を実装したい場合はこのクラスを拡張する。 このクラスを拡張する場合、設定ファイルのjavaTypeResolver要素で拡張したクラス名を指定する。 |
ShellCallback | プロジェクト/パッケージ情報に基づいたディレクトリの決定や、成果物出力時に既存ファイルが存在した場合のマージ方法等を決定するためのコールバッククラスである。 |
ProgressCallback | MBG実行時の進捗を報告するためのコールバッククラスである。 MBGをAntやIDEに統合する場合に使用できる。 |
Introspectedという言葉が頻出します。翻訳すると「内省」とかが出てきますが、どうも直感的に「これだ!」という説明がありません。
直接関係ありませんが、JavaBeanのAPI仕様の説明を見ると”Introspection is the automatic process of analyzing a bean’s design patterns to reveal the bean’s properties, events, and methods.”, “The Introspector class provides a standard way for tools to learn about the properties, events, and methods supported by a target Java Bean.”という説明がありました。これを参考として、”Introspection”とは「対象の構造を分析するプロセス」と理解しました。
なお、ここではreflectionとintrospectionの違いが記載されています。introspectionを行うためにreflectionを使用する、という関係性のようです。
プラグインによるカスタマイズ方法
MBGがクラス、メソッド、XML等の成果物を生成したタイミングで、プラグインの所定のメソッドを実行する仕組みになっています。Javaクライアントやモデルのクラスやメソッドが生成された際に実行されるプラグインのメソッドを実装することで、成果物を期待通りに変更することができます。
公式サイトでは、いくつかのプラグインが公開されているので、これらのプラグインで要件を満たせるか検討してください。これらのプラグインで要件を満たせない場合、独自にプラグインを実装する必要があります。
プラグインのライフサイクル
MBGが実行するプラグインのメソッドやその呼出し順番は本家サイト(Plugin Lifecycle)をご覧ください。
公式サイトでは、メソッドの説明が乏しく、私には理解が難しかったです。そのため、独自に纏めたメソッドの説明を本稿の最後に記載しましたので、参考になれば幸いです。
プラグインのメソッドの実行順番をトレースしたい場合、こちらのサンプルをご覧ください。このプラグインを使用することで、プラグインのメソッドの呼び出し順番がログに出力されます。
プラグイン実装のイメージ
自分がカスタマイズしたい成果物が生成される際に呼び出されるメソッドを特定し、そのメソッドを実装します。
MBGは成果物を作成した後に、その成果物を引数としてプラグインの該当メソッドを呼び出します。開発者はメソッドの中で、引数の成果物オブジェクトを操作して、成果物をカスタマイズします。
成果物の作成自体を中止する場合は、メソッドの戻り値としてfalseを指定します。
例えば、モデルクラスのgetterメソッドの出力をカスタマイズする場合、プラグインのmodelGetterMethodGenerated()メソッドを実装します。
引数として、getterメソッドの情報(Methodオブジェクト)が渡されるので、このオブジェクトのメソッドやプロパティを使用して成果物の内容を変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @Override public boolean modelGetterMethodGenerated( Method method, // 作成されたgetterメソッドの情報 TopLevelClass topLevelClass, // このメソッドを格納するモデルクラスの情報 IntrospectedColumn introspectedColumn, // メソッドに対応するDBカラム情報 IntrospectedTable introspectedTable, // 処理対象のテーブル情報 Plugin.ModelClassType modelClassType // モデルクラス種類(PrimaryKey/Record等) ) { ... String tableName = introspectedTable.getFullyQualifiedTableNameAtRuntime(); if( tableName.equals("test") ) { method.setName(method.getName() + "Test"); // メソッド名を修正 } ... return true; } |
MBGがプラグインのメソッドを実行する際に引き渡される主要な引数は次の通りです。
引数(クラス名) | 説明 |
---|---|
TopLevelClass | 成果物として作成予定のJavaクラスを表している。 クラスコメント/import文一覧/クラス名/基底クラス等の情報を保持し、これらの変更を行うためのメソッドを提供する。 import文を追加するためのaddImportedType()がよく使用される。 |
Interface | 成果物として作成予定のJavaインターフェイスを表している。 TopLevelClassと同様の機能を提供する。 |
Method | 成果物として作成予定のメソッドを表している。 メソッド名や引数の変更、アノテーションの追加等が可能。 |
Field | 成果物として作成予定のモデルクラスのフィールド(getter/setterに対応する変数宣言)を表している。 |
IntrospectedTable IntrospectedColumn | MBGが解析したDBのテーブルやカラムを表している。 DB上のテーブルやカラムの解析結果の他に、成果物を生成するための各種条件を格納する。 成果物のカスタマイズを行うために条件の取得元としてよく使用する。 高度なカスタマイズを行う場合、このクラスに対する変更操作が必要な場面がある。 |
プラグインの実装の流れ
プラグインがMBGによって実行される流れは、プラグインクラスのインスタンスの生成、プラグイン初期化メソッドの実行、処理対象となるテーブル単位で成果物作成メソッドの実行、後処理メソッドの実行の流れになります。(一度プラグインインスタンスが作成されると、MBG終了までそのインスタンスが使いまわされます。)
そのため、基本的な実装の流れとしては、プラグイン初期化のためのメソッドを実装し、カスタイマイズしたい成果物が生成されたタイミングで実行される成果物作成メソッドを実装します。MBGが作成する成果物とは別に、JavaクラスやXMLを追加したい場合は後処理メソッドを実装します。
なお、設定ファイルで指定している実行環境の定義によって作成される成果物が変わるために、期待するメソッドが呼び出されない場合があります。
フェーズ | 説明 |
---|---|
プラグイン初期化メソッドの実装 |
|
成果物作成メソッドの実装 |
|
後処理メソッドの実装 |
|
プラグインの実装詳細
ExamplePluginというサンプルのプラグインをベースに説明します。
完全なソースコードはこちらで公開しています。
準備
プラグインの開発ではMBGのAPIを使用するので、プロジェクトにMBGライブラリを追加する必要があります。
mavenの場合、次のように依存関係を追加します。
1 2 3 4 5 6 7 8 9 10 11 | ... <dependencies> ... <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.4.0</version> </dependency> ... </dependencies> ... |
クラスの定義
MBGのプラグインとして動作するためにはPluginインターフェイスを実装する必要があります。
PluginインターフェイスにはMBGから呼び出される全てのメソッドの定義がされています。このクラスを実装する場合、使用するメソッドは少数であるにもかかわらず、全てのメソッドの実装が必要になり大変です。
この手間を減らすために、基本的には次のようにPluginAdapterを継承してプラグインクラスを開発します。
(こうすることで、必要なメソッドのみの実装になります。)
1 2 3 4 5 | ... import org.mybatis.generator.api.PluginAdapter; ... public class ExamplePlugin extends PluginAdapter { ... |
プラグイン初期化
プラグイン初期化のために、必要に応じてsetContext(), setProperties()を実装します。
プラグイン実行の前提条件を検証するためのvalidate()メソッドは必ず実装する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | ... public class ExamplePlugin extends PluginAdapter { private String arg1; private String arg2; @Override public void setProperties(Properties properties) { this.arg1 = properties.getProperty("arg1"); this.arg2 = properties.getProperty("arg2"); } @Override public boolean validate(List<String> warnings) { if (this.arg1 == null || this.arg2 == null) { warnings.add("please specify arg1 and arg2"); } // メッセージがない場合は正常と見なす。 return warnings.size() <= 0; } ... |
- 設定ファイルのプラグイン定義で指定されたプロパティがsetProperties()メソッド渡されるので、必要に応じてクラス内に保持します。12345678...<generatorConfiguration><context ...><plugin type="example.ExamplePlugin"><property name="arg1" value="arg1value" /><property name="arg2" value="arg2value" /></plugin>...
- 取得したプロパティ値の検証等のプラグイン実行に必要な条件をvalidate()メソッドに実装(必須)します。
validate()でfalseを返却するとプラグインが無効になり、それ以降のプラグインの処理は実行されません。
引数のwarningsに追加したメッセージは、MBG実行時のログにWARNINGで出力されます。 - generatorConfig.xmlの定義情報の参照や変更を行いたい場合、setContext()メソッドを実装することもできます。
- setContext(), setProperties()の引数で渡されるContextやProperitesを保持するだけなら、PluginAdapterのメソッドで実装されているので、あえてこれらのメソッドを実装する必要はありません。
成果物作成のカスタマイズ
テーブル単位でJavaクライアント、モデル、SQLマップが作成されます。
これらの作成をカスタマイズする方法を説明します。
テーブルに対応する成果物全般
initialized()メソッドを実装することで、テーブル毎に作成されるJavaクライアント、モデル、SQLマップをカスタマイズできます。
次の例では、clientXXX()、modelXXX()、sqlMapXXX()で作成される各クラスやXMLから、特定カラムを除外しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Override public void initialized(IntrospectedTable introspectedTable) { // 特定の名前のカラムについては、後続処理から除外 // (モデルやXMLからtest_dummy_col1の存在が消される。) Iterator<IntrospectedColumn> cols = introspectedTable.getBaseColumns().iterator(); while (cols.hasNext()) { IntrospectedColumn ic = cols.next(); if (ic.getActualColumnName().equalsIgnoreCase("test_dummy_col1")) { cols.remove(); } } } |
- 引数のIntrospectedTableは後続のclientXXX(), modelXXX(), sqlMap()メソッドの引数として利用されます。そのため、このオブジェクトを変更することで、後続処理の動作を制御できます。
- IntrospectedTableでも変更できるものとそうでないものがあることに注意してください。例えば、getBaseColumns()で取得されるリストはIntrospectedTable内で保持するインスタンスなので、このリストに対する操作はIntrospectedTableに反映されます。getAllColumns()で取得されたリストはIntrospectedTable内部で保持するリストではなく複製されたリストであるため、このリストに対する操作はIntrospectedTableに反映されません。詳細はIntrospectedTableのソースコードを見た方が安全です。
Javaクライアント
clientXXX()メソッドを実装することで、生成されるJavaクライアントをカスタマイズできます。
次の例では、JavaクライアントのselectByPrimary()メソッドを生成する際、このメソッドを複製した新しいメソッドを作成し、作成対象メソッドに追加しています。併せてアノテーションを追加しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Override public boolean clientSelectByPrimaryKeyMethodGenerated(Method method, Interface interfaze, IntrospectedTable introspectedTable) { // 既存メソッドから新しいメソッドを作成してインターフェイスに追加 Method addMethod = new Method(method); addMethod.setName(method.getName() + "AddedTest"); // selectByPrimaryKeyAddedTestを追加 addMethod.addAnnotation("@" + reqanno.getShortName()); // アノテーションを追加 interfaze.addMethod(addMethod); // Javaインターフェイスにメソッド追加 interfaze.addImportedType(reqanno); // import文一覧に宣言を追加 return true; } |
モデル
modelXXX()を実装することで、生成するモデルをカスタマイズできます。
次の例では、特定カラムに対応するフィールドにアノテーションを追加しています。
1 2 3 4 5 6 7 8 9 10 11 | @Override public boolean modelFieldGenerated(Field field, TopLevelClass topLevelClass, IntrospectedColumn introspectedColumn, IntrospectedTable introspectedTable, ModelClassType modelClassType) { String columnName = introspectedColumn.getActualColumnName(); if (columnName.equalsIgnoreCase("test_dummy_col2")) { field.addAnnotation("@" + reqanno.getShortName()); // アノテーションを追加 topLevelClass.addImportedType(reqanno); // import文一覧に宣言を追加 } return true; } |
- 対象となるカラムかどうかの判定方法として、Field#getName()を使う方法もあります。このようなJavaに関わる変数は実装場所によって”testDummyCol2″, “TestDummyCol2″等のように変わってくるので、可能な限りIntrospectedColumn#getActualColumnName()等のIntrospected系のオブジェクトを使うように統一することをお薦めします。
SQLマップ
sqlMapXXX()メソッドを実装することで、生成されるSQLマップ(XML)をカスタマイズできます。
次の例は、SQLマップXMLのinsert要素(id=”insert”)にあるINSERT文のパラメータに関数を挿入しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | @Override public boolean sqlMapInsertElementGenerated(XmlElement element, IntrospectedTable introspectedTable) { // 特定のカラムに関数を付与 List<VisitableElement> veList = element.getElements(); for (int i = 0; i < veList.size(); i++) { VisitableElement ve = veList.get(i); String line = ((TextElement) ve).getContent(); // 正規表現でパラメータ部分を検索・置換 // ※下記は適当なサンプルになっているので業務使用時は注意! Matcher m = paramPattern.matcher(line); StringBuffer sb = new StringBuffer(); // 置換後文字列を格納 while (m.find()) { String all = m.group(0); // "#{testDummyCol2,jdbcType=...}" String col = m.group(1); // "testDummyCol2,jdbcType=..." if (col.startsWith("testDummyCol2,")) { String replace = "LOWER(" + all + ")"; m.appendReplacement(sb, replace); } } m.appendTail(sb); String newText = sb.toString(); // 上記で変更があった場合、要素を差し替える if (!line.equals(newText)) { veList.set(i, new TextElement(newText)); } } return true; } |
- clientXXX(), modelXXX()ではメソッドやプロパティ操作で出力内容をカスタマイズできましたが、ここでは文字列操作が基本となります。
- XMLに出力される内容はTextElementクラスのリスト(便宜上「要素リスト」と表記)で表現されており、XML上の1行がTextElementインスタンス1つに対応します。引数のXmlElementから、この要素リストを取得し、XML1行単位の文字列を取得します。要素リストにはTextElement以外(MBGで動的SQLを生成するための条件を格納したXmlElement)が含まれる場合があるので注意が必要です。
- 出力を変更する場合は、TextElementインスタンスを生成して要素リストを変更します。
- 参考ですが、引数のXmlElementは
org.mybatis.generator.codegen.mybatis3.xmlmapper.XMLMapperGenerator
で定義されているジェネレータ(クラス)で生成されおり、例えばInsert文を生成する場合は、InsertElementGeneratorクラスを使うよう定義されています。XMLElementを作り直す場合は、これらのジェネレータのコードが参考になると思います。 - この例で生成されたSQLマップファイルは次のようになります。123456789101112131415161718...<insert id="insert" parameterType="example.mybatis.entity.MstEmployee"><!--WARNING - @mbg.generatedThis element is automatically generated by MyBatis Generator, do not modify.-->insert into mst_employee (cid, eid, name, addr,age, note, created_timestamp,created_userid, updated_timestamp, updated_userid,version, test_dummy_col2, name_enc)values (#{cid,jdbcType=CHAR}, #{eid,jdbcType=CHAR}, #{name,jdbcType=VARCHAR}, #{addr,jdbcType=VARCHAR},#{age,jdbcType=INTEGER}, #{note,jdbcType=VARCHAR}, #{createdTimestamp,jdbcType=TIMESTAMP},#{createdUserid,jdbcType=VARCHAR}, #{updatedTimestamp,jdbcType=TIMESTAMP}, #{updatedUserid,jdbcType=VARCHAR},#{version,jdbcType=INTEGER}, LOWER(#{testDummyCol2,jdbcType=VARCHAR}), #{nameEnc,jdbcType=VARBINARY})</insert>...
プラグイン後処理
MBGの成果物とは別に、独自のクラスやXMLファイルを追加することもできます。
次の例では、Javaクライアントのフォルダに独自のJavaクラスを追加しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | @Override public List<GeneratedJavaFile> contextGenerateAdditionalJavaFiles() { // Java Client系のファイルを独自に追加 // (JavaClientの設定をベースにサンプルJavaクラスを追加) // generatorConfig.xmlの // javaClientGenerator要素の定義値を取得 Context ctx = super.context; JavaClientGeneratorConfiguration config = ctx.getJavaClientGeneratorConfiguration(); String targetPackage = config.getTargetPackage(); String targetProject = config.getTargetProject(); // クラス定義の作成 String className = targetPackage + ".AddedExample"; FullyQualifiedJavaType clazz = new FullyQualifiedJavaType(className); TopLevelClass topLevelClass = new TopLevelClass(clazz); topLevelClass.addFileCommentLine(""); // 最終成果物を生成 JavaFormatter formatter = new DefaultJavaFormatter(); GeneratedJavaFile gjf = new GeneratedJavaFile(topLevelClass, targetProject, formatter); return Arrays.asList(gjf); } |
参考:プラグインのメソッド説明 ★作成中★
メソッド | 説明 |
---|---|
setContext(Context) | 設定情報の参照や編集時に使用します。 引数のContextには、主にgeneratorConfig.xmlで指定した設定情報が格納されています。 |
setProperties(Properties) | プラグインのプロパティを保持する場合に使用します。 使用するプラグインの宣言はgeneratorConfig.xmlのPlugin要素で行いますが、この際に必要であればproperty要素でプロパティを定義することができます。この宣言されたプロパティが、引数として引き渡されます。 プロパティが不要なプラグインの場合、このメソッドの実装は不要です。 |
validate(List<String>) | プラグイン実行の前提条件を検証する場合に使用します。 プラグインの実行に必要なプロパティが正しく宣言しているか、等の検証ロジックを実装します。 検証に成功した場合は戻り値としてtrueを返却するよう実装します。 検証に失敗した場合、引数のListにエラーメッセージを追加し、falseを返却するよう実装します。戻り値がfalseの場合、このプラグインは無効(使用されない)になります。 |
分類 | メソッド | 説明 |
---|---|---|
初期化 | initialized( IntrospectedTable) | 後続処理で使用するInspectedTable情報を変更する場合に使用します。 InspectedTableの属性情報やデータベースの解析結果を修正したい場合、引数のIntrospectedTableインスタンスが保持する情報を編集します。 |
Java Client | clientXXXMethodGenerated( Method, Interface, IntrospectedTable) | マッパーインターフェイスのselectByExample/insertSelective等のメソッド定義を編集する場合に使用します。(XXXは、MBGが作成しようとするメソッド名になります。) マッパーインターフェイスの個々のメソッドが作成されたタイミングで、このメソッドが実行されます。この際、作成されたメソッド・インターフェイス情報が引数(Method, Interface)として渡されます。 Methodインスタンスにはメソッド情報(修飾子、メソッド名、引数、戻り値、アノテーション等)が含まれており、これらを編集できます。 import文の追加が必要になる場合、InterfaceインスタンスのaddImportedType()を使用できます。 ※リファレンスでは第2引数にTopLevelClassを持つメソッドも記載されています。現在の実装では基本的にマッパーインターフェイスしか作成しないので除外されたのだと思います。 |
clientGenerated( Interface, IntrospectedTable) | マッパーインターフェイスの定義を編集する場合に使用します。 上記のclientXXXMethodGeneratedはメソッド作成時に実行されますが、このメソッドは全てのメソッド作成が完了した後に実行されます。 この際、作成されたインターフェイス情報が引数(Interface)として渡されます。 Intefaceインスタンスにはインターフェイス名、コメント、import文一覧が含まれており、これらを編集できます。 | |
Model Methods | modelFieldGenerated(Field, TopLevelClass, IntrospectedColumn, IntrospectedTable, ModelClassType) | |
modelGetterMethodGenerated | ||
modelSetterMethodGenerated | ||
modelExampleClassGenerated | ||
modelPrimaryKeyClassGenerated | ||
modelBaseRecordClassGenerated | ||
modelRecordWithBLOBsClassGenerated |
メソッド | 説明 |
---|---|
contextGenerateAdditionalJavaFiles( IntrospectedTable) | |
contextGenerateAdditionalXmlFiles( IntrospectedTable) |