スマートニュース株式会社の瀬良(@seratch) と申します。

普段は VP of Engineering として、横断的な取り組みやエンジニアリング組織の課題解決、プロジェクトのスケジュール管理などに注力していますが、元々はサーバサイドエンジニアで、今もコードレビューなどで開発に関わっています。

OSS を公開しました

スマートニュースは、創業以来、サーバサイドにおいては Java や JVM 系の技術を多く使っている企業ですが、Java に関連した自社で開発したコードを OSS として公開することはあまり行なっていませんでした。

最近、groupId com.smartnews でライブラリを公開できるようにしました。今後は、コードの公開も今まで以上にやっていきたいと考えています。結果として、微力ながら技術コミュニティに何らかの貢献できれば嬉しく思います。今回、まず一つ目として、私が社内で開発し 2 年近く社内で使ってきたライブラリを公開しましたので、それについて紹介します。

公開したライブラリは「jpa-entity-generator」というものです。その名の通り JPA の仕様に準拠したエンティティクラスを自動生成するライブラリです。Gradle、Maven のプラグインとして公開しており、この二つの build tool を使っているプロジェクトであればすぐに使うことができます。

https://github.com/smartnews/jpa-entity-generator

この手のものに馴染みのある方であれば、どのようなものかおわかりかとは思いますが、すでに存在するデータベースのテーブル群に対してテーブルの情報、カラムの情報を取得し、それに基づいてエンティティクラスのソースコードを自動生成するものです。

ライブラリが生まれた背景

近年、弊社においても RDBMS 以外のデータストアを使うケースも多いですが、RDB も引き続き重要なデータストアの一つです。

私が入社したのは 2016 年 3 月で、気がつけば 2 年 2 ヶ月前ですが、当時、所属していたチームの中で RDB の管理において、以下のような課題がありました。

  • 課題1: データベース変更のレビューのやり方が統一されていない
  • 課題2: エンティティクラスが共通化されていない

課題1 については、当時、非常に少人数のチームであったため、差し当たりの支障はない状況でしたが、チーム・サービス数の拡大に伴って難しくなることは予想されました。特に過去の経緯・レビュー内容が記録として残っている状態にすることは、将来のメンテナにとって非常に重要ですので、プロセスを改善したいと考えていました。

課題2 については、もちろん意図的に共通化しないケースはありえますし、実際にそのようなケースもありました。一方で「本当は共通化されたものを使いたいが、単にコピーされている」というケースもありました。

課題1 の解決策としては、以下のやり方を導入しました。このチームでは DB マイグレーションを仕組み化していないので、そこは変えずに適用前の開発プロセスを改善しました。

  • データベーススキーマ管理用の GitHub レポジトリを用意(Flyway で差分の DDL を管理)
  • pull request で変更内容レビューを行うようチーム内の認識合わせ(PR テンプレートも用意)
  • その DDL が実行可能かどうかというレベルの検証の実行を CI(CircleCI)設定

これによって「レビュアーに依存せずレビューの観点が揃うようになる」「最新のスキーマの再現が楽になる」「カラムコメントをつける人が増える」などの効果がありました。

課題2 の解決策としては、上記の GitHub リポジトリに対して追加で以下のことを行うようにしました。

  • 複数のリポジトリで管理されていたエンティティクラスのソースコードを merge して、共通リポジトリ管理に徐々に移行
  • データベース変更の pull request で同時にエンティティも更新するように統一

と、方針を決めるところまではよかったのですが、できれば手動で更新するのではなく、ミスが発生しないためにもツールによるコード生成に移行したいと考えていました。

しかし、それを実現するためには、生成ツールが柔軟な対応をできるようにする必要がありました。例えば、既存のエンティティクラスでは共通の interface を実装していたり、アクセサで何か必要な処理が実装されていたり、追加でメソッドや定数が定義していたり、といったことがありました。これらをスムーズに移行できるようなツールが必要で、当時私が調べた限り、既存のものでそれを満たすものは見つかりませんでした。

私自身はこのような自動生成ツールを作った経験は何度かあったので、時間を見つけて数日程度で実装し、ある程度できたタイミングからこの管理リポジトリに組み込み、徐々にチームの中で使っていくよう促しました。

次のセクションでは、具体的な利用例とともに、どのような点を工夫し、設定可能にしたかを紹介します。

使い方

基本的には GitHub リポジトリの README が最新なのでそちらを参考にしていただければと思いますが、簡単に紹介します。

必要なものは以下の二つです。

  • build.gradle or pom.xml
  • src/main/resources/entityGenConfig.yml

build.gradle

Gradle または Maven の設定で entity generator のプラグインを有効にします。この状態で ./gradelw entityGen のように実行すると DB に接続し、必要なソースコードを生成します。

buildscript {
  dependencies {
    classpath 'com.h2database:h2:1.4.197' // アクセスする DB の JDBC ドライバ
    classpath 'com.smartnews:jpa-entity-generator:0.99.2'
  }
}

// plugin を有効にします
apply plugin: 'entitygen'
entityGen {
  // ツール実行時に使う YAML 設定ファイル
  configPath = 'src/main/resources/entityGenConfig.yml'
}

// 生成したコードは Lombok に依存しています
dependencies {
  providedCompile 'org.projectlombok:lombok:1.16.20'
  providedCompile 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
}

src/main/resources/entityGenConfig.yml

最低限必要な情報は以下の 2 つだけです。jdbcSettings は DB への接続情報、packagename は生成したコードを配置する package です。ごくシンプルなエンティティを生成したいだけの場合はこれだけで利用することが可能です。

// DB への接続情報
jdbcSettings:
  url: "jdbc:h2:file:./db/blog;MODE=MySQL"
  username: "user"
  password: "pass"
  driverClassName: "org.h2.Driver"

// 生成したコードを置く package を指定
packageName: "com.example.entity"

src/main/java/com/example/entity/*

指定された package 配下に以下のようなコードが生成されます。このコードは Lombok に依存しています。

package com.example.entity;

import java.sql.*;
import javax.persistence.*;
import lombok.Data;

@Data
@Entity(name = "com.example.entity.Blog")
@Table(name = "blog")
public class Blog {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "\"id\"")
  private Integer id;
  @Column(name = "\"name\"")
  private String name;
  @Column(name = "\"active\"")
  private Byte active;
  @Column(name = "\"created_at\"")
  private Timestamp createdAt;
}

弊社では、私が入社してから広く Lombok が使われるようになりました。

あまり特殊な使い方はしておらず、 @Data などを使って Java beans の定義を楽にする用途がほとんどです。それよりも便利な言語機能を使いたいケースでは Kotlin や Scala など他の JVM 上で動作する言語を使う方がよいというコンセンサスが社内にあるように思います。

最近、Lombok は JDK アップデート対応に関する課題がみられるようになりました。現時点において、当社では特に問題は発生しておらず、広く使われているライブラリなので、現時点では将来を悲観する必要はあまりないと考えています。しかし、今まで以上に JDK のバージョンとの整合性についてアンテナを高くしておく必要はありそうです。

jpa-entity-generator は、現状 Lombok に依存しないコードを生成する機能を提供していませんが、オープンソースになりましたし、将来、そのようなオプションのニーズがあれば対応できるかもしれません。

機能・設定の紹介

GitHub リポジトリに entityGenConfig.yml のサンプルが置いてありますので、全ての設定項目については、そちらを見ていただければと思いますが、代表的なものを日本語で紹介していきます。

ちなみにフォーマットに YAML を選択した理由はそれなりに普及しており、簡潔さから見ても妥当な選択肢であろうということと、コメントが書けるという JSON に対する優位性が決め手でした。

JDBC 接続設定

データベースの情報を取得するために JDBC 経由で接続する必要があります。以下の通り urlusernamepassworddriverClassName を指定します。

もし指定した JDBC ドライバーのクラスが見つからないエラーが発生したら Gradle のビルド設定を見直してみてください。buildscript.dependencies に JDBC ドライバーが追加されていないかもしれません。

余談ですが、以下の H2 の trace 関係のパラメータは細かい情報が出力されて便利なので H2 を使ったテストでハマったりした場合は試してみてください。

# ---------------------------------------------------------
# *** JDBC configuration to fetch metadata ***

jdbcSettings:
  url: "jdbc:h2:file:./db/blog;MODE=MySQL;TRACE_LEVEL_FILE=2;TRACE_LEVEL_SYSTEM_OUT=2"
  username: "user"
  password: "pass"
  driverClassName: "org.h2.Driver"

コード生成における共通基本設定

以下にある通り、コード生成の出力先や package 名などですが、それ以外に興味深いものとして jpa1SupportRequiredpackageNameForJpa1 というものがあります。

これは深遠なる事情により、一部 JPA1 互換の entity が必要だったので実装したものです。JPA1 と JPA2 のエンティティ定義は微妙に異なるので、それに対応するために追加しました。早く不要になるとよいですね。

デフォルトでは JPA2 の entity のみを生成しますので、もし JPA1 向けの実装が必要な場合は jpa1SupportRequired を true に設定すれば、そちらも生成されるようになります。

# ---------------------------------------------------------
# *** Basic/global configuration ***

# If you need to specify non-standard source directory, set the following setting as needed
#   - string value: the relative path from the project root directory.
outputDirectory: "src/main/java"

# The package name used when generating entity classes
#   - string value: full package name
packageName: "com.example.entity"

# If you need to set a specific strategy attribute for @GeneratedValue, specify the name here.
#   - string value: "TABLE", "SEQUENCE", "IDENTITY", "AUTO", or null
generatedValueStrategy: "IDENTITY"

# If you need to generate JPA 1 compatible entity classes as well, set the following attributes
#   - boolean value: true if you need to generate JPA 1 compatible entities as well
jpa1SupportRequired: true
# The package to put JP 1 compatible entity classes.
#   - string value: full package name separate from the "packageName"
packageNameForJpa1: "com.example.entity.jpa1"

除外設定

歴史のあるデータベースというのは色々な事情やユースケースがあります。何らかの事情からエンティティクラスを生成する必要のないテーブルについては部分一致で除外する設定を入れることができます。以下の例だと xxx_tmp のようなテーブルが対象から除外されます。

# ---------------------------------------------------------
# *** Rules for exclusion ***

# Define the following rules if you'd like to exclude specific tables when generating entity classes
#   - array of TableExclusionRule objects
#     - tableName (string) / tableNames (array): string value that partially matches table names (case sensitive)
tableExclusionRules:
  - tableNames: ["_tmp"]

クラス名の変換ルール

テーブル名をそのままキャメルケースでクラス名にしたくないというケースはそれなりにあるかと思います。例えば abtest_configABTestConfig にしたい、プレフィックスがついているテーブル名からプレフィックス部分を除きたいなど。後ほど WordPress のデータベースでの実行例も紹介しますが、そのようなケースに対応する設定です。

考えられるニーズとしては Ruby on Rails のような inflector が欲しいというケースもあるかもしれません。そのようなルールを実装することも可能ですが、弊社ではニーズがなかったため、現在は未対応です。

# ---------------------------------------------------------
# *** Rules for table/class name conversion ***

# If you need some rules that convert table names to entity class names, list the mapping rules as below.
#   - array of ClassNameRule objects
#     - tableName: table name (full name, case sensitive)
#     - className: Java class name to be used (you cannot include package namme in front of the class name)
classNameRules:
  - tableName: "article"
    className: "BlogArticle"
  - tableName: "article_tag"
    className: "BlogArticleTag"
  - tableName: "abtest"
    className: "ABTest"

クラスに関する設定(アノテーション、実装するインターフェース、追加のクラスコメント)

生成したエンティティクラスに対するいくつかの設定です。まず、一つ目はクラスに追加でアノテーションを付与したい場合です。

対象のクラス名を classNames で配列指定し、付与したいアノテーションも複数指定できます。アノテーションの属性(@Something(name = "foo")name = "foo" の部分)を指定することもできます。classNames の指定を省略した場合は全てのエンティティクラスに適用します。

# ---------------------------------------------------------
# *** Rules on how to attach class annotations ***
#   - array of ClassAnnotationRule objects
#     - (optional) className (string) / classNames (array): target Java class names (case sensitive)
#     - annotations: array of Annotation objects
#       - className: the annotation class name
#       - (optional) attributes (array of AnnotationAttribute objects): attributes if exist
#         - name (string): the name of the attribute
#         - (optional) value (string): the value of the attribute
#         or
#         - code (string): writing code in a string value instead of specifying name + value
#       or
#       - code (string): write the whole code in a string value instead of specifying className + attributes
classAnnotationRules:
  # If you just specify the annotations, all the generated classes'll have them.
  - annotations:
    - className: "lombok.ToString"

  # You can specify the classes to have the class annotations.
  - {classNames: ["ABTest",], annotations: [className: "Deprecated"]}
  - className: "Blog"
    annotations:
      - className: "lombok.Builder"
        attributes:
          - name: "toBuilder"
            value: "true"

次に、先ほど背景のところで説明した通り、エンティティに特定の interface を implement してほしいケースもあろうかと思います。その場合は FQDN でその名前を複数指定することができます。こちらも classNames の指定を省略した場合は全てのエンティティクラスに適用します。

# ---------------------------------------------------------
# *** Java interfaces to let generated classes implement ***
#   - array of InterfaceRule objects
#     - (optional) className (string) / classNames (array): target Java class names (case sensitive)
#     - interfaces: array of Interface objects
#       - name: FQDN of the interface
#       - (optional) genericsClassNames: array of string values if the interface has generics
interfaceRules:
  # If you just specify the interfaces, all the generated classes'll implement the interfaces.
  - interfaces: [{name: "java.io.Serializable"}]

  - classNames: ["ABTest"]
    interfaces: [{name: "com.example.util.ExpirationPredicate"}]

最後にクラスコメントのカスタマイズです。jpa-entity-generator は table に設定されているコメントがあれば、それを読み取り自動的にクラスコメントとして書き出しますが、それに加えて何かコメントを加えたい場合は以下のようにルールを設定します。こちらも classNames の指定を省略した場合は全てのエンティティクラスに適用します。

# ---------------------------------------------------------
# *** Rules on how to append class comments ***
#   - array of ClassAdditionalCommentRule objects
#     - (optional) className (string) / classNames (array): target Java class names (case sensitive)
#     - comment (string): comment value to be appended to the class definition
classAdditionalCommentRules:
  # If you just specify the classNames, all the generated classes'll have them.
  - comment: "Note: auto-generated by jpa-entity-generator"

  - classNames: [
      "ABTest",
    ]
    comment: "TODO: This A/B testing mechanism is no longer used"

フィールドに関する設定(型、アノテーション、デフォルト値、追加のフィールドコメント)

フィールドについての設定項目は、型、アノテーション、デフォルト値、コメントの 4 つです。

まず、型ですが、例えばよくあるケースとしては tinyint 型を真偽値として使うケースが考えられます。これを boolean として認識できるかは JDBC ドライバーの metadata API の実装次第です。もし期待と異なって Integer 型としてコードが生成されているが、実際は boolean でも動作するので変更したいという場合、以下のように設定によって型を上書きすることが可能です。

以下のコメントにあるように fieldName は必須ですが、className / classNames の指定を省略した場合は全てのエンティティクラスでその名前のフィールドに対してルールを適用します。

# ---------------------------------------------------------
# *** Rules to convert types of the fields in generated classes ***
#   - array of FieldTypeRule objects
#     - (optional) className (string) / classNames (array): target Java class names (case sensitive)
#     - fieldName (string) / fieldNames (array): the field name to convert its type
#     - typeName (string): the type name to be converted
fieldTypeRules:
  - {classNames: ["ABTest"], fieldName: "config", typeName: "String"}

  # If you don't specify classNames in a rule, all the generated classes will be affected.
  - {                        fieldName: "active", typeName: "boolean"}

フィールドに追加でアノテーションを付与したい場合は以下のようにします。クラスの方と基本的には同様です。

# ---------------------------------------------------------
# *** Rules on how to attach field annotations ***
#   - array of FieldAnnotationRule objects
#     - (optional) className (string) / classNames (array): target Java class names (case sensitive)
#     - fieldName (string) / fieldNames (array): the field name to attach annotations
#     - annotations: array of Annotation objects
#       - className: the annotation class name
#       - (optional) attributes (array of AnnotationAttribute objects): attributes if exist
#         - name (string): the name of the attribute
#         - (optional) value (string): the value of the attribute
#         or
#         - code (string): writing code in a string value instead of specifying name + value
#       or
#       - code (string): write the whole code in a string value instead of specifying className + attributes
fieldAnnotationRules:
  - className: "BlogArticle"
    fieldNames: ["tags"]
    annotations: [{className: "Deprecated"}]

  - classNames: ["ABTest"]
    fieldNames: ["config"]
    annotations: [{
      className: "com.example.annotation.Experimental",
      attributes: [{name: "comment", value: '"The expected data format is JSON"'}],
      # code: '@com.example.annotation.Experimental(comment = "The expected data format is JSON")',
    }]

フィールドにデフォルト値を設定したい場合は、以下の通り設定します。何も指定しない場合、各フィールドのデフォルト値は明には指定されません。Lombok の @Builder を使うときは追加で @Builder.Default アノテーションを指定する必要があります(Lombok が警告してくれます)。

# ---------------------------------------------------------
# *** Rules on how to set default values to the fields ***
#   - array of FieldDefaultValueRule objects
#     - (optional) className (string) / classNames (array): target Java class names (case sensitive)
#     - fieldName (string) / fieldNames (array): the field name to attach annotations
#     - defaultValue (string): the default value part in source code (specify '"something"' if you have a string value)
fieldDefaultValueRules:
  # If you don't specify classNames in a rule, all the generated classes will be affected.
  - {                        fieldNames: ["name"],   defaultValue: '"Anonymous"'}
  - {classNames: ["ABTest"], fieldNames: ["active"], defaultValue: "0"}

最後はクラスの時と同様ですが、テーブルコメントを反映してくれたクラスと同様、フィールドにもカラムのコメントは自動で挿入されます。それに加えて、何らかの追加コメントをつけたい場合は以下のように指定します。className / classNames の指定を省略した場合は全てのエンティティクラスでその名前のフィールドに対してルールを適用します。

# ---------------------------------------------------------
# *** Rules on how to append field comments ***
#   - array of FieldAdditionalCommentRule objects
#     - (optional) className (string) / classNames (array): target Java class names (case sensitive)
#     - fieldName (string) / fieldNames (array): the field name to attach annotations
#     - comment (string): comment value to be appended to the field definition
fieldAdditionalCommentRules:
  - {className: "ABTest",         fieldName: "active",                comment: "true if the AB test is still active."}
  - {className: "BlogArticleTag", fieldNames: ["articleId", "tagId"], comment: "The field is non-null value"}

とにかく追加のコードを挿入

最後は力技な感じですが、とにかくコードを付け加えたい時に使います。例えば、@OneToMany、@ManyToOne などのリレーション定義、追加でメソッドを定義したいとき、アクセサや既存のメソッドを override したいときなどがあります。

デフォルトではクラスのソースコードの末尾に追記する挙動ですが、クラスの先頭に追記したい場合は position: "Top" のように指定します。

また、JPA1 互換のコードは JPA2 のものとは異なることも多いので、jpa1Code として別のコードを指定できるようになっています。jpa1Cocde を指定しない場合は JPA1、JPA2 どちらにも code のコードを挿入します。

# ---------------------------------------------------------
# *** Rules to append additional code to generated classes ***
#   - array of AdditionalCodeRule objects
#     - (optional) className (string) / classNames (array): target Java class names (case sensitive)
#     - code (string): writing code in a string value
#     - (optional) position (string): "Top" or "Bottom" (default: "Bottom")
#     - (optional) jpa1Code (string): writing code in a string value if you need to overwrite code only for JPA 1 compatible entities
additionalCodeRules:
  - classNames: ["BlogArticle", "BlogArticleTag"]
    position: "Top"
    code: |
            public Integer getId() { return this.id; }
  - className: "BlogArticle"
    # position: "Bottom"
    code: |
            @ManyToOne
            @JoinColumn(name = "`blog_id`", insertable = false, updatable = false)
            private Blog blog;
    jpa1Code: |
            @lombok.Setter(lombok.AccessLevel.NONE)
            @ManyToOne
            @JoinColumn(name = "\"blog_id\"", referencedColumnName = "\"id\"", insertable = false, updatable = false)
            private Blog blog;

利用例 - WordPress のデータベースで試してみる

参考までに実際のデータベースを例に実行してみましょう。ここでは皆さんご存知の WordPress のテーブル定義を使って実行してみました。

すべての動作するコードはこちらで公開していますので、興味のある方は動かしてみてください。

src/main/resources/entityGenConfig.yml

最低限の設定としては jdbcSettingspackageName だけで十分ですが、全てのテーブルが wp_ というプレフィックスがついていて、かつ複数形の命名になっているので、対象のテーブルにそれぞれ対応するクラス名を指定しました。あと、例として Post と PostMeta のリレーションを追加コードとして指定しています。

# ---------------------------------------------------------
# *** JDBC configuration to fetch metadata ***
jdbcSettings:
  url: "jdbc:mysql://127.0.0.1:3306/wordpress?useSSL=false&useInformationSchema=true"
  username: "wordpress"
  password: "wordpress"
  driverClassName: "com.mysql.jdbc.Driver"

# ---------------------------------------------------------
# *** Basic/global configuration ***

# The package name used when generating entity classes
#   - string value: full package name
packageName: "com.example.entity"

# ---------------------------------------------------------
# *** Rules for table/class name conversion ***

# If you need some rules that convert table names to entity class names, list the mapping rules as below.
#   - array of ClassNameRule objects
#     - tableName: table name (full name, case sensitive)
#     - className: Java class name to be used (you cannot include package namme in front of the class name)
classNameRules:
  - tableName: "wp_commentmeta"
    className: "CommentMeta"
  - tableName: "wp_comments"
    className: "Comment"
  - tableName: "wp_ec3_schedule"
    className: "Ec3Schedule"
  - tableName: "wp_links"
    className: "Link"
  - tableName: "wp_options"
    className: "Option"
  - tableName: "wp_postmeta"
    className: "PostMeta"
  - tableName: "wp_posts"
    className: "Post"
  - tableName: "wp_terms"
    className: "Term"
  - tableName: "wp_term_relationships"
    className: "TermRelationship"
  - tableName: "wp_term_taxonomy"
    className: "TermTaxonomy"
  - tableName: "wp_usermeta"
    className: "UserMeta"
  - tableName: "wp_users"
    className: "User"

# ---------------------------------------------------------
# *** Rules to append additional code to generated classes ***
#   - array of AdditionalCodeRule objects
#     - (optional) className (string) / classNames (array): target Java class names (case sensitive)
#     - code (string): writing code in a string value
#     - (optional) position (string): "Top" or "Bottom" (default: "Bottom")
#     - (optional) jpa1Code (string): writing code in a string value if you need to overwrite code only for JPA 1 compatible entities
additionalCodeRules:
  - className: "Post"
    code: |
            @OneToMany(fetch = FetchType.EAGER, mappedBy = "post", cascade = CascadeType.ALL)
            private java.util.List<PostMeta> postMetaList;
  - className: "PostMeta"
    code: |
            @ManyToOne
            @JoinColumn(name = "\"post_id\"", insertable = false, updatable = false)
            private Post post;

src/main/java/com/example/entity/Post.java

./gradlew entityGen を実行すると、このようなソースコードが生成されます。

package com.example.entity;

import java.sql.*;
import javax.persistence.*;
import lombok.Data;

@Data
@Entity(name = "com.example.entity.Post")
@Table(name = "wp_posts")
public class Post {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "\"ID\"")
  private Long id;
  @Column(name = "\"post_author\"")
  private Long postAuthor;
  @Column(name = "\"post_date\"")
  private Timestamp postDate;
  @Column(name = "\"post_date_gmt\"")
  private Timestamp postDateGmt;
  @Column(name = "\"post_content\"")
  private String postContent;
  @Column(name = "\"post_title\"")
  private String postTitle;
  @Column(name = "\"post_excerpt\"")
  private String postExcerpt;
  @Column(name = "\"post_status\"")
  private String postStatus;
  @Column(name = "\"comment_status\"")
  private String commentStatus;
  @Column(name = "\"ping_status\"")
  private String pingStatus;
  @Column(name = "\"post_password\"")
  private String postPassword;
  @Column(name = "\"post_name\"")
  private String postName;
  @Column(name = "\"to_ping\"")
  private String toPing;
  @Column(name = "\"pinged\"")
  private String pinged;
  @Column(name = "\"post_modified\"")
  private Timestamp postModified;
  @Column(name = "\"post_modified_gmt\"")
  private Timestamp postModifiedGmt;
  @Column(name = "\"post_content_filtered\"")
  private String postContentFiltered;
  @Column(name = "\"post_parent\"")
  private Long postParent;
  @Column(name = "\"guid\"")
  private String guid;
  @Column(name = "\"menu_order\"")
  private Integer menuOrder;
  @Column(name = "\"post_type\"")
  private String postType;
  @Column(name = "\"post_mime_type\"")
  private String postMimeType;
  @Column(name = "\"comment_count\"")
  private Long commentCount;

  @OneToMany(fetch = FetchType.EAGER, mappedBy = "post", cascade = CascadeType.ALL)
  private java.util.List<PostMeta> postMetaList;
}

全ての生成されたファイルは以下の通りです。

$ ls src/main/java/com/example/entity/
Comment.java		Ec3Schedule.java	Option.java		PostMeta.java		TermRelationship.java	User.java
CommentMeta.java	Link.java		Post.java		Term.java		TermTaxonomy.java	UserMeta.java

Spring Data JPA コード例

このエンティティが本当に動作するのか、試してみました。以下のようなビルド設定で

dependencies {
  compile 'mysql:mysql-connector-java:5.1.46'
  compile 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
  providedCompile 'org.projectlombok:lombok:1.16.20'

  testCompile 'org.springframework.data:spring-data-jpa:2.0.6.RELEASE'
  testCompile 'org.springframework.boot:spring-boot-starter-data-jpa:2.0.1.RELEASE'
  testCompile 'org.springframework.boot:spring-boot-starter-test:2.0.1.RELEASE'
}

以下のテストが動作します。正しく使える JPA エンティティクラスになっているようです。

package com.example.repository;

// -------------------------------------------------------------------
@Repository
public interface PostMetaRepository extends CrudRepository<PostMeta, Long> {
}

// -------------------------------------------------------------------
@Repository
public interface PostRepository extends CrudRepository<Post, Long> {
}

// -------------------------------------------------------------------
@Configuration
@PropertySource("application.properties")
@EnableAutoConfiguration
@EntityScan("com.example.entity")
@EnableJpaRepositories("com.example.repository")
public class TestConfig {
}

// -------------------------------------------------------------------
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = TestConfig.class)
public class RepositoriesTest {

    @Autowired
    private PostRepository postRepository;
    @Autowired
    private PostMetaRepository postMetaRepository;

    @Test
    public void testPosts() {
        Post p = new Post();
        // 値の設定部分は省略
        postRepository.save(p);

        PostMeta pm = new PostMeta();
        pm.setPostId(p.getId());
        // 値の設定部分は省略
        postMetaRepository.save(pm);

        Optional<Post> foundPost = postRepository.findById(p.getId());
        assertThat(foundPost.isPresent(), is(true));
        foundPost.ifPresent(post -> {
            assertThat(post.getPostMetaList().size(), is(greaterThan(0)));
        });
        long count = postRepository.count();
        assertThat(count, is(greaterThan(0L)));
    }
}

今後

以上、簡単ですが jpa-entity-generator のご紹介でした。

この手のことをやろうとしたことがある方には、よくある必要なカスタマイズに対応していて、一定便利だと感じていただけたのではないでしょうか。

もし、「気に入った」「実際にプロジェクトで使い始めた」という方は、ぜひ GitHub 上で Star ボタンを押していただければ嬉しく思います。GitHub リポジトリはこちらです。

一方で、現状は今後の拡張性は考慮しつつも、弊社でのユースケースに絞って開発してきたものなので、他の環境では足りない機能もあると思います。

オープンソースになりましたので、対応できるとよさそうなユースケースがあれば、ぜひお気軽にご連絡ください。pull request による貢献もお待ちしています。なお、ライブラリのバージョンについては、この記事公開時点では 0.99.x となっていますが、早々に 1.0.0 をリリースする予定です。

Java サーバサイドエンジニアを募集しております

このように実際の仕事の中で生まれた成果をコミュニティに貢献することに興味のあるエンジニアの方、ぜひ一緒に働きませんか?

Java でサーバサイド開発をしたい方は特に以下のポジションが最適です。ぜひアクセスしてみてください。

それ以外にも多くのポジションで新しいメンバーを募集しております。

正式応募の前にカジュアル面談、社食ランチなどご希望の方は、私・他の当社スタッフまでお気軽にご連絡ください。