ドメイン駆動設計の開発プロセスに自動生成ツールを活用する - Java Web

ドメイン駆動設計は

Eric Evans氏が提唱するソフトウェア設計手法です。[Domain-driven design | DDD]
手法については、
・ 「エリック・エヴァンスのドメイン駆動設計」- 翔泳社
・ 「Domain-Driven Design Quickly 日本語版」
などを参考にしてください。
ここでは、ドメイン駆動設計の開発プロセスに自動生成ツールを活用する目的と効果について述べます。

ドメイン駆動設計と開発プロセス

ドメイン駆動設計では、繰り返し型と呼ばれる開発プロセスが推奨されます。繰り返し型とは、ドメインモデルを一気に作り上げるのではなく、モデルを繰り返し洗練 (リファクタリング) し、段階的に完成度を上げていく手法です。
※このサイトでは反復型、繰り返し型、インタラクティブ、イテレーティブという考えは特に区別なく使用しています。

繰り返し型開発の流れ


繰り返す単位をイテレーションと呼び、1回のイテレーションのなかで、分析から設計、実装、検証までを完結させます。そのため、イテレーションはミニウォータフォールとも呼ばれます。分析・設計では、UML図などでモデルを表現し、実装・検証では、ソフトウェアで表現されたモデル (動くソフトウェア) を作成し、実際に動かして検証します。

実践のポイント

繰り返し型開発では、イテレーションをどれだけスムーズに、かつ本質的な部分に集中して回せるかが重要です。 つまり、ドメインモデルの問題に集中し、軽快に回せるかです。 1回1回のイテレーションが重くて手間がかかるようでは、繰り返し型になりません。

開発時に起きやすい問題

ドメイン駆動設計でよい結果を得るためには、ドメインモデルの洗練と検証を何度でも繰り返せることが重要です。そのためには、開発プロセスの中の繰り返し (イテレーション) を、素早く何度でも繰り返すための手段やツールが必要です。 しかし現実には、ドメインモデルを洗練 (リファクタリング) すると、それを検証するための「動くソフトウェアモデル」も変更しなければなりません。 変更にはドメインモデル以外の本質的ではない単調な変更作業も多く含まれます。そのためイテレーションは、時間のかかるタスクになりがちです。 そして次第に、何度でも繰り返すことが非現実的になってきます。 そうなると、開発チームはドメインに集中できなくなり、進捗が停滞し、手法の変更を迫られることにもなります。 この問題を解決するためには、開発プロセスの一部を自動化するなど、何らかの実践的な対策が必要でしょう。

動くソフトウェアモデルの必要性

動くソフトウェアが必要な理由としては、
・ 動くソフトウェアは、各イテレーションの成果物のひとつです。
・ 実際に動かすことで、ドメインモデルの検証をより正確に、具体的にできます。
・ 開発チーム内でのドメインモデルに対する知識や理解のバラつきを抑止します。
・ 過剰な机上分析状態に陥ることを予防します。
・ DDDの用語やパターン名に拘り過ぎて、理解しにくいドメインモデルになることを抑止します。
・ 技術寄りではないメンバーであっても、Webアプリケーションとして操作することで深く理解し検証できます。
などが、あげられます。
もしも、動くソフトウェアを持たずに、机上のUML図や、単体テストレベルのテストコードだけでドメインモデルを検証しようとすると、やはり、検証の深さや分かり易さという点で不足を感じるでしょう。また、技術寄りではないメンバーにとって検証は難しくなります。

動くソフトウェアモデルを維持する手間

ただ、現実的な視点では、イテレーションを何度も繰り返すなかで、動くソフトウェアを ”いつでも動く状態” に維持することは、簡単ではありません。 例えば、ドメインモデルをWebアプリケーション上で動かす場合、イテレーションの度に (ドメインモデルを変更する度に) ビュー層や永続化層などの更新も必要になるからです。 イテレーションの主目的はドメインモデルの洗練です。しかし、動くソフトウェアの維持に時間がかかると、集中力もそがれ易くなります。

DDBuilderとは

Java Webアプリケーションを生成するソフトウェアツールです。
入力はJavaで実装されたドメインモデル、出力は「動くソフトウェアモデル」となるWebアプリケーションです。
ドメインモデルから動くソフトウェアモデルを自動生成する図

DDBuilderの役割

DDBuilderはドメインモデルからWebアプリケーションを自動生成することで、繰り返し (イテレーション) を軽量化します。上図に示したように、開発チーム が設計し、Javaで実装したドメインモデル から、検証可能な動くソフトウェア (Java Webアプリケーション) を生成することで、動くソフトウェアモデルの実装時間と手間を軽減します。 イテレーションの度に行っていたビュー層や永続化層の実装作業がドメイン中心になり、開発チームはドメインモデルの分析・設計・実装・評価に集中できるようになります。単純な実装ミスも減少します。

成果物はドメインモデル

ただし、DDBuilderが生成するWebアプリケーションに縛られる必要はありません。本当の成果物はドメインモデルであり、Webアプリケーションはそれを評価するための入れ物にすぎません。従って、DDBuilderは、Webアプリケーション生成ツールというよりも、ドメインモデルのインキュベータ (孵化器) と考えた方がより適切です。

DDBuilderを使ったイテレーションの流れ



DDBuilderとEclipseの関係

DDBuilderとEclipseの役割図
@生成/読込み更新
   生成は、初回に実行します。
   読込み更新は、Bで更新されたドメイン実装を読取り、Webアプリケーションに反映します。
   主に、WebアプリケーションのUI層が更新されます。
   この処理は、DDBuilder画面のボタン押下で実行されます。
Aインポート/リフレッシュ
   インポートは、初回に実行します。
   リフレッシュは@の更新結果をEclipseに通知・反映するために行います。
   この処理は、Eclipseの操作で行います。
Bドメインモデルの実装
   モデル図などをもとにEclipseでJavaプログラミングします。
   これは、開発チームのタスクです。

期待できる効果

・イテレーションを軽量化(工数削減・期間短縮)できます。
・手組み実装によるミスとロスを排除できます。
・開発チームは、非本質的な作業から解放されます。
・コードファーストで実践できます。

必要になる知識

・ オブジェクト指向モデリング技術 (考え方)
・ ドメイン駆動設計 (考え方)
・ 繰り返し型開発 (考え方)
・ Javaプログラミング
Java EE JPA(Java Persistence API)入門レベル
Wicket 入門レベル
・ Eclipseの基本操作

ドキュメント

ユーザガイド [PDF][2.5MB]

動作確認済みの環境

・ Java 8
・ Windows7 pro 32bit/64bit, Windows10 pro 64bit
・ Eclipse IDE for Java EE Developers 4.4, 4.3
・ Apache Tomcat 7.0, 8.0, 8.5, 9.0

インストールとアンインストール

DDBuilderはスタンドアロンのJavaデスクトップアプリケーションです。
インストールは、ダウンロードしたzipファイルを適当な場所に解凍してください。
アンインストールは、 解凍結果をエクスプローラなどで削除してください。

ダウンロードページへ


謝辞

DDBuilderは次のソフトウェアを利用しています。
Apache Wicket 7.4.0
hibernate-entitymanager4.3.6.Final
Derby10.10.2.0
log4j2.5
Eclipse jdt3.9.1

サポートとソースコード公開について

使いはじめで、ご不明な点がありましたら ここ からご連絡ください。
なお、弊社の都合で返信が遅くなる場合があります。
また、有料でのサポートも用意しております。
サポート内容や形態などは柔軟に対応できますので、お問合せ ください。
DDBuilderのソースコードは、準備が整い次第、公開する方向です。



補足説明

例 Web アプリケーションの構成
例 クラス図の扱い
例 オブジェクト指向やドメイン駆動設計ではじめるとき
例 イテレーションの流れ
例 サポートするドメインモデルの関連の種類と属性型



例 Web アプリケーションの構成


DDBuilderが自動生成する Web アプリケーションの構成図です。
自動生成するアプリケーション構造

DDBuilderが生成する「動くソフトウェア」は、Java Web アプリケーションです。
Eclipseのプロジェクト形式になっていますので、Eclipseにインポートして、編集・実行できます。
DDBuilder固有のコンテナなどはなく、フレームワークとして次を使用しています。
ビュー層: Apache Wicket
永続化層: Java EE JPA/ Hibernate
※上図中の「DDBuilder基底」部分に、いくつかの簡単な基底クラスを含んでいます。

ロックインフリー

利用者チームは、開発時でも実行時でも、いつでもDDBuilderを切り離すことができます。
DDBuilderにロックインされることはありません。
ドメインモデルがある程度安定し、頻繁な繰り返しが必要ない段階になれば、DDBuilderは必要なくなるかもしれません。そして、ドメインモデルを他の言語やアプリケーションフレームワークに移植することもあるでしょう。
DDBuilderはドメインモデルのインキュベータ (孵化器・育成器) です。
もちろん、そのまま運用することもできます。

自作ページの追加

自動生成されたWebアプリケーションに自作のページを追加できます。
DDBuilderは自作ページを上書きしません。
自作ページの作成は、自動生成されたページクラスを参考にすれば、効率よく自作できるでしょう。

Apache Wicket を採用

Wicketは、Java Webアプリケーションフレームワークです。Struts系と違い、 オブジェクト指向プログラミングモデルを前提としたフレームワークです。HTMLとの独立性が高く、基本的にすべてをJavaコードで実装するアーキテクチャは、 自動生成機能を実現するうえでも好都合で相性がよいと言えます。
ドメインモデルは利用者が実装しますが、Wicketを意識することはありません。Wicketを意識するのは自作ページを追加する場合です。

JPA Hibernate を採用

HibernateはJavaEE JPA実装の1つです。
DDBuilderが生成する動くソフトウェアは、デフォルトではJava標準のJavaDBを使用します。Hibernateは、 PostgreSQL, MySQL, Oracle, SQLServer, DB2など代表的なデータベースをサポートしており、設定変更だけで他のデータベースに変更できます。
動くソフトウェアは、Hibernateを「Javaアプリケーションのスタンドアロン型」で使用し、Springなどのコンテナは前提としません。
ドメインモデル層のクラスを実装するときには、JPA / Hibernateの知識が必要になります。 主に、関連を定義するためのアノテーションの書き方などの知識が必要になります。

ダウンロードページへ

[戻る]



例 クラス図の扱い


DDBuilderにはクラス図をサポートする機能はありませんが、クラス図は必要です。
分析時のクラス図は手書きラフスケッチとし、正式なクラス図はUMLツールなどで Java ソースからリバースエンジニアリングされることを推奨します。リバースエンジニアリングであれば常に実装と一致させることができます。もしも、表計算ソフトなどで作成すると不一致が起き、イテレーションが回らなくなる原因にもなると思います。
また当社では、JavaクラスレポートというJavaソースからメトリクスなどの情報抽出を行うツールも公開[無料]していますのでご参考ください。
[戻る]



例 オブジェクト指向やドメイン駆動設計をはじめるとき


プロジェクトをドメイン駆動設計やオブジェクト指向で始めても途中で断念したり、方針変更を余儀なくされた、という経験はないでしょうか。
そのような結果を生む原因の1つに、下図に示すような「立ち上がり期」の停滞感があると思います。 そのために、オブジェクト指向やドメイン駆動設計で始めたけれど、進捗が遅い、先が不安などと判断され、過去の手法に戻すことになったりします。 原因としては、パターン名や用語に振り回されて空転しているようなケースもありますが、やはり、トランザクションスクリプト型などの方が、とりあえず何らかの進捗が出やすいというのも事実でしょう。

導入時の立ち上がりの比較

※注意:上のグラフは感覚的なものです。

この立ち上がり期を乗り越えないと、その効果が見える前に従来型に戻されてしまいます。この停滞感を、手法に対する想いや熱意だけで乗り越えるのも難しいでしょう。

DDBuilderを利用することで、この停滞感を少しでも解消できればと思います。
机上だけではなく、実際に動くソフトウェアを素早く試すことができれば、プロジェクトに存在する不安を払しょくし、技術やドメイン知識の習得スピードも上がると思います。

ダウンロードページへ

[戻る]



例 イテレーションの流れ


DDBuilderを利用したイテレーションの流れを具体的に示します。
ここでは、ドメインとして在庫管理業務を想定して、倉庫、製品、在庫、在庫管理サービスといったドメインモデルを作り上げていく流れを説明します。

インストール (DDBuilderのダウンロードと解凍) は完了しているものとします。
まず、DDBuilderを起動すると下図の画面が表示されます。
※DDBuilderはJavaで作成されたスタンドアロンアプリケーションです。Eclipseとは関係なく独立して動作します。

DDBuilderを最初に起動したときの画面

起動したらまず、Webアプリケーションの作成場所やアプリケーション名などの基本情報を入力します。
下図は入力例です。
生成するWebアプリケーションの基本情報の入力例

基本情報を入力し、作成/更新ボタンを押下するとWebアプリケーションの基本形が作成されます。
この時点ではドメインモデルはひとつも定義されていませんので、
生成されたWebアプリケーションは最小限の画面 (トップ画面など) を含むだけです。
次に、生成されたWebアプリケーションを編集できるように、Eclipseにインポートします。
ここでの編集とは、ドメインモデルを Java で実装したり、Web アプリケーションとしてデバッグ実行して、ドメインモデルを検証することを指します。
※Webアプリケーションは、インポート可能なEclipseプロジェクトとして生成されています。
Eclipse画面例

インポートが完了すると、上図のように「プロジェクトエクスプローラ」ビューに表示されます。
確認のために、EclipseでWebアプリケーションを実行してみます。
この時点では、ドメインモデルは1つもありませんが、基本的な画面遷移の動作確認はできます。
下図はEclipse内で実行した時の例です。
Eclipseでのデバッグ実行画面

上図はEclipse内蔵のブラウザで表示された状態ですが、Eclipse外のブラウザからもアクセスしてテストできます。
下図はChromeからアクセスした例です。例:http://localhost:8080/zaiko/
Chromeブラウザでアクセスした例

ここまでで、Webアプリケーションの基本動作ができました。
この先は、ドメイン分析、ドメインモデルの抽出、設計、実装、検証する作業が始まります。
つまり、初回のイテレーションの開始です。
一気にドメイン全体をカバーしようとするのではなく、1〜2週間で行える範囲や優先度の高いユースケースが通る範囲などをイテレーションのゴールとして設定します。
期間の目安や、ユースケースの優先度などはプロジェクトによって違ってくるでしょう。
繰り返し型開発プロセスやアジャイルなどの手法解説を参考にしてください。

もっとも簡単な例として、下図のProductオブジェクトを1つだけ作成してみます。
単純なドメインモデルの例

/**
 * 製品
 */
@Entity     <------------- JPAアノテーション
public class Product extends DdBaseEntity {   <------------- DDBuilderエンティティ基底クラス
    private static final long serialVersionUID = 1L;
    /**
     * 製品名
     */
    private String name;
    /**
     * コンストラクタ
     */
    public Product(){
        super();
        this.name = "";
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

実装作業は、下図のように、EclipseでProductクラスを実装します。
domainパッケージの直下にProduct.javaを追加します。サブパッケージを追加することもできます。
EclipseでProduct.javaを作成する例

これで、Productクラスは追加できました。
ただし、Productクラスのインスタンスを登録したり、表示したりするためのビュー層や永続化層はありません。
これらは、DDBuilderが自動生成します。
そのために、DDBuilderでWebアプリケーションを更新します。
DDBuilderはProduct.javaファイルを読み込み、ビュー層のクラスなどを追加します。
※なお、前回入力されたDDBuilder画面の基本情報は維持されています。


作成/更新ボタンを押下し、完了ダイアログが表示されたら、Webアプリケーションの中に必要なjavaファイルが追加されています。しかし、Eclipseは、追加されたファイルを感知していません。そのために必ず、Eclipse側でプロジェクトをリフレッシュしてください。(忘れがちです)
リフレッシュしたら、再度Webアプリケーションを起動してテストします。
下図はChromeからアクセスした例です。
動くソフトウェアを実行し、ブラウザからアクセスしたトップ画面の例

ここで、ドメインクラス一覧ボタンをクリックすると次画面に遷移します。
ドメインクラス一覧画面の例

次に、製品Productリンクをクリックすると次画面に遷移します。
製品クラスのインスタンス一覧画面の例

次に、新規作成リンクをクリックすると次画面に遷移します。
製品クラスのインスタンスの編集画面例

次に、製品名を入力し保存ボタンを押下すると、Productインスタンスが永続化され、次画面に遷移します。
エンティティインスタンスの一覧画面の例

1件登録され、一覧に追加されています。
次に、編集リンクをクリックすると次画面に遷移します。
エンティティの編集画面の例

次に、製品名を変更すると次画面に遷移します。
ブラウザからアクセスしたエンティティ編集画面の例

次に、保存すると次画面に遷移します。
動くソフトウェアを実行し、ブラウザからアクセスしたエンティティ一覧画面例

ここまではProductクラスだけでしたが、下図のようにモデルを追加します。
サンプルとして使用するドメインモデルの例

Productを追加した時と同様の手順で、Webアプリケーションを更新し、テスト実行します。
動くソフトウェアのトップ画面

ドメインクラス一覧ボタン → 製品Productリンクをクリックします。
なお、ここでは説明を省略しますが、以降の説明での中では、下図のようにテストデータが作成済みとします。
テストデータの作成についてはユーガイド(PDF)を参照ください。
ドメインクラス一覧ボタンを押下します。
ドメインクラスの一覧画面

製品 Productリンクをクリックします。
Productクラスのインスタンスの一覧画面
例として、2行目の編集リンクをクリックします。
Productインスタンスの編集画面

編集して保存するか、キャンセルで戻ります。
次に、サービスメソッドを実行してみます。
このサービスは、ある倉庫から別の倉庫に在庫を移動させる業務を実現します。
サービス一覧またはホーム → サービス一覧で下図の画面が表示されます。
サービスメソッドの一覧画面

実行リンクをクリックします。
下図の画面が表示されたら、このサービスメソッドに渡す引数を指定します。
from移動元倉庫:選択ボタンを押下します。
サービスメソッドの実行画面

例として「福岡センター」を選択します。
サービスメソッドの引数設定画面1

仮決定ボタンを押下します。
サービスメソッドの引数設定画面(2)

同様に移動先の倉庫と移動する製品と数量を指定します。
サービスメソッドの引数設定画面(3)

サービス実行を押下するとお下図の完了画面が表示されます。
倉庫間移動メソッドの詳細は実装していませんので、戻り値としてtruetrueが表示されています。
サービスメソッドの実行結果画面
実装コード(例:StockService, Warehouse, Stock)

package jp.co.nextdesign.domain;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import javax.persistence.Transient;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;
/**
 * 製品
 */
@Entity
public class Product extends DdBaseEntity {
    private static final long serialVersionUID = 1L;

    /** 製品名 */
    private String name;
    
    /** 在庫 */
    @OneToMany(mappedBy="product", cascade=CascadeType.ALL, orphanRemoval=true)
    private List<Stock> stockList = new ArrayList<Stock>();
    
    /** コンストラクタ */
    public Product(){
        super();
        this.name = "";
    }

    //OneToManyで双方向関連を維持するためのコードを
//含むgetStockList(),setStockList(List<Stock> stockList)の例
    @Transient
    private ArrayList<Stock> latestStockList = new ArrayList<Stock>();
    public List<Stock> getStockList() {
        return this.stockList;
    }
    public void setStockList(List<Stock> stockList) {
        for(Stock newStock : stockList){
            if (!latestStockList.contains(newStock)){
                newStock.setProduct(this);
            }
        }
        for(Stock oldStock : latestStockList){
            if (!stockList.contains(oldStock)){
                oldStock.setProduct(null);
            }
        }
        this.stockList = stockList;
        latestStockList = new ArrayList<Stock>(this.stockList);
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    
    /** DDB一覧表示用タイトル */
    @Override
    public String getDDBEntityTitle(){
        return "製品名:" + this.getName();
    }
}

package jp.co.nextdesign.domain;
import java.util.Calendar;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;
/**
 * 在庫
 */
@Entity
public class Stock extends DdBaseEntity {
    private static final long serialVersionUID = 1L;
        
    /** 数量 */
    private Integer quantity;
        
    /** 製品 */
    @ManyToOne
    private Product product;
        
    /** 倉庫  */
    @ManyToOne
    private Warehouse warehouse;
        
    /** コンストラクタ */
    public Stock(){
        super();
        this.quantity = 0;
    }
        
    /**
     * 次月の入庫予定日を応答する
     * @return 次月の入庫予定日
     */
    public Date getNextMonthWarehousingDate(){
        Date result = Calendar.getInstance().getTime();
        //何らかの実装
        return result;
    }
        
    /** DDB一覧表示用タイトル */
    @Override
    public String getDDBEntityTitle(){
        String result = "製品名=";
        result += this.getProduct() != null ? this.getProduct().getName() : "";
        result += " 倉庫名=";
        result += this.getWarehouse() != null ? this.getWarehouse().getName() : "";
        result += " 数量=" + this.getQuantity();
        return result;
    }
    public Integer getQuantity() {
        return quantity;
    }
    public void setQuantity(Integer quantity) {
        this.quantity = quantity;
    }
    public Product getProduct() {
        return product;
    }
    public void setProduct(Product product) {
        this.product = product;
    }
    public Warehouse getWarehouse() {
        return warehouse;
    }
    public void setWarehouse(Warehouse warehouse) {
        this.warehouse = warehouse;
    }       
}

package jp.co.nextdesign.domain;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import javax.persistence.Transient;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;
/**
 * 倉庫
 */
@Entity
public class Warehouse extends DdBaseEntity {
    private static final long serialVersionUID = 1L;

    /** 倉庫名 */
    private String name;
    
    /** 在庫リスト */
    @OneToMany(mappedBy="warehouse", cascade=CascadeType.ALL, orphanRemoval=true)
    private List<Stock> stockList = new ArrayList<Stock>();

    /** コンストラクタ */
    public Warehouse(){
        super();
        this.name = "";
    }
    
    /** 製品在庫を追加する */
    public boolean addStock(Product product, int quantity){
        //処理(この例では省略)
        return true;
    }

    /** 製品在庫を削減する */
    public boolean removeStock(Product product, int quantity){
        //処理(この例では省略)
        return true;
    }

    //OneToManyで双方向関連を維持するためのコードを
//含むgetStockList(),setStockList(List<Stock> stockList)の例
    @Transient
    private ArrayList<Stock> latestStockList = new ArrayList<Stock>();
    public List<Stock> getStockList() {
        return this.stockList;
    }
    public void setStockList(List<Stock> stockList) {
        for(Stock newStock : stockList){
            if (!latestStockList.contains(newStock)){
                newStock.setWarehouse(this);
            }
        }
        for(Stock oldStock : latestStockList){
            if (!stockList.contains(oldStock)){
                oldStock.setWarehouse(null);
            }
        }
        this.stockList = stockList;
        latestStockList = new ArrayList<Stock>(this.stockList);
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    
    /** DDB一覧表示用タイトル */
    @Override
    public String getDDBEntityTitle(){
        return "倉庫名:" + this.getName();
    }
}

package jp.co.nextdesign.service;
import jp.co.nextdesign.domain.Product;
import jp.co.nextdesign.domain.Warehouse;
import jp.co.nextdesign.service.ddb.DdBaseService;
/**
 * 在庫管理サービス
 */
public class StockService extends DdBaseService {

    /**
     * 倉庫間移動
     * @param from 移動元倉庫
     * @param to 移動先倉庫
     * @param product 移動する商品
     * @param quantity 移動する数量
     * @return
     */
    public Boolean transferStock(Warehouse from, Warehouse to, Product product, Integer quantity) {
        boolean result = false;
        try {
            startService(); //サービス初期化処理
            begin(); //トランザクション開始
            
            // 倉庫間移動処理
            if (from.removeStock(product, quantity)){
                if (to.addStock(product, quantity)){
                    result = true;
                }
            }
            if (result){
                commit(); //トランザクションcommit
            } else {
                rollback(); //トランザクションrollback
            }
        } catch (Exception e) {
            rollback();
        } finally {
            endService(); //サービス終了処理
        }
        return result;
    }
}

詳細はユーザガイド(PDF)をご覧ください。

ダウンロードページへ

[戻る]



例 サポートするドメインモデルの関連の種類と属性型


このドメインモデルは、DDBuilderがサポートする関連の種類、インスタンス属性型の種類を確認することを目的としていますので、DDD的には参考になりません。 [コードをダウンロード]

UMLクラス図
ドメインモデルの例

Javaコード

/*
 * モデル例
 */
package jp.co.nextdesign.domain;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import javax.persistence.Transient;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;

/**
 * 著者
 */
@Entity
public class Author extends DdBaseEntity {
    private static final long serialVersionUID = 1L;

    /** 名前 */
    private String name;
    
    /** 書籍リスト owning/parent */
    @OneToMany(mappedBy="author", cascade=CascadeType.ALL, orphanRemoval=true)
    private List<Book> bookList;

    /** 書籍リスト owning/parent */
    @OneToMany(mappedBy="author2", cascade=CascadeType.ALL, orphanRemoval=true)
    private List<Book> bookList2;

    /** コンストラクタ */
    public Author(){
        super();
        this.name = "---";
        this.bookList = new ArrayList<Book>();
        this.bookList2 = new ArrayList<Book>();
    }
    
    //OneToManyで双方向関連を維持するためのコードを含むgetBookList(),
    //setBookList(List<Book> bookList)の例
    @Transient
    private ArrayList<Book> latestBookList = new ArrayList<Book>();
    public List<Book> getBookList() {
        return this.bookList;
    }
    public void setBookList(List<Book> bookList) {
        for(Book newBook : bookList){
            if (!latestBookList.contains(newBook)){
                newBook.setAuthor(this);
            }
        }
        for(Book oldBook : latestBookList){
            if (!bookList.contains(oldBook)){
                oldBook.setAuthor(null);
            }
        }
        this.bookList = bookList;
        latestBookList = new ArrayList<Book>(this.bookList);
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public List<Book> getBookList2() {
        return bookList2;
    }
    public void setBookList2(List<Book> bookList2) {
        this.bookList2 = bookList2;
    }
    
    @Override
    public String getDDBEntityTitle(){
        return this.name;
    }
    
    /** debug */
    public String getDebugInfo(){
        String info = "<" + this.getClass().getSimpleName() + ">";
        info += "\nname=" + this.getName();
        info += "\n</" + this.getClass().getSimpleName() + ">";
        return info;
    }
}

/*
 * モデル例
 */
package jp.co.nextdesign.domain;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Transient;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;
import jp.co.nextdesign.domain.store.Store;

/**
 * 書籍
 */
@Entity
public class Book extends DdBaseEntity {
    private static final long serialVersionUID = 1L;
    
    /** 書名 */
    private String name;

    /** 書名2 */
    private String name2;

    /** 出版日 */
    private Date publishedAt;
    
    /** 出版日2 */
    private Date publishedAt2;
    
    /** 仕入価格 */
    private BigDecimal cost;
    
    /** 仕入価格2 */
    private BigDecimal cost2;
    
    /** キャンペーン1 isなし */
    private Boolean campaign1;
    
    /** キャンペーン12 isなし */
    private Boolean campaign12;
    
    /** キャンペーン2 is付き */
    private Boolean isCampaign2;
    
    /** キャンペーン22 is付き */
    private Boolean isCampaign22;
    
    /** 言語 */
    @Enumerated(EnumType.STRING)
    private EnumLanguage attEnum;
    
    /** 言語2 */
    @Enumerated(EnumType.STRING)
    private EnumLanguage attEnum2;

    /** ISBN one-to-one owning/parent Hibernate ORM 5.2 User Guide2.7と異なる */
    @OneToOne(cascade=CascadeType.ALL)
    @JoinColumn(name="isbn_id") //自分の列
    private Isbn isbn;
    
    /** ISBN2 one-to-one owning/parent Hibernate ORM 5.2 User Guide2.7と異なる */
    @OneToOne(cascade=CascadeType.ALL)
    @JoinColumn(name="isbn_id2") //自分の列
    private Isbn isbn2;
    
    /** 著者 */
    @ManyToOne
    private Author author;

    /** 著者2 */
    @ManyToOne
    private Author author2;

    /** 版 */
    @OneToMany(mappedBy="book", cascade=CascadeType.ALL, orphanRemoval=true)
    private List<Edition> editionList;
    
    /** 版2 */
    @OneToMany(mappedBy="book2", cascade=CascadeType.ALL, orphanRemoval=true)
    private List<Edition> editionList2;
    
    /**
     * 書店
     * Book編集画面で関連付ける書店を選択/解除しても書店側には反映されない。
     * setStoreList, getStoreListを参照。
     * 同期反映させるにはaddStore/removeStoreを使うかまたは、ManyToManyを
     * 2つのOneToManyで定義する。
     * Hibernate ORM 5.2 User Guide2.7.2 Bidirectional ManyToMany参照
     */
    @ManyToMany(cascade={CascadeType.PERSIST, CascadeType.MERGE}, fetch=FetchType.EAGER)
    private List<Store> storeList;
    
    /** 書店2
     * BookとStoreの間に2つのManyToManyを定義すると、
     * 次のようなBook_Storeテーブルが作成される。
     * このテーブルにinsertするためには、4つのxxxx_idが全てnot nullに限られるので、
     * insert時に(常に)例外が発生する。
     * 同じエンティティ間で複数のMantToMany関連を定義したい場合は
     * @JoinTableを使用する必要があると思われる。
     * ここでは未確認。
     * create table Book_Store (
     *   bookList2_id bigint not null,
     *   storeList2_id bigint not null,
     *   bookList_id bigint not null,
     *   storeList_id bigint not null
     * )
     */
//    @ManyToMany(cascade={CascadeType.PERSIST, CascadeType.MERGE})
//    private List<Store> storeList2;
    
    /** Integer型属性名 */
    private Integer integerAttribute;
    
    /** Integer型属性名2 */
    private Integer integerAttribute2;
    
    /** Byte型属性名 */
    private Byte attByte;
    
    /** Byte型属性名2 */
    private Byte attByte2;
    
    /** Short型属性名 */
    private Short attShort;
    
    /** Short型属性名2 */
    private Short attShort2;
    
    /** Long型属性名 */
    private Long attLong;
    
    /** Long型属性名2 */
    private Long attLong2;
    
    /** Float型属性名 */
    private Float attFloat;
    
    /** Float型属性名2 */
    private Float attFloat2;
    
    /** Double型属性名 */
    private Double attDouble;
    
    /** Double型属性名2 */
    private Double attDouble2;
    
    /** Character型属性名 */
    private Character attCharacter;
    
    /** Character型属性名2 */
    private Character attCharacter2;
    
    /** コンストラクタ */
    public Book(){
        super();
        this.name = "";
        this.storeList = new ArrayList<Store>();
//        this.storeList2 = new ArrayList<Store>();
        this.editionList = new ArrayList<Edition>();
        this.editionList2 = new ArrayList<Edition>();
    }
    
    //ManyToManyで双方向関連を維持するためのaddStore,removeStoreを含む。
    //owning側ではなくmappedBy側から使用するが、両側に実装する。
    public List<Store> getStoreList() {
        return storeList;
    }
    public void setStoreList(List<Store> storeList) {
        this.storeList = storeList;
    }
    public void addStore(Store store){
        if (store != null && !this.storeList.contains(store)){
            this.storeList.add(store);
            store.addBook(this);
        }
    }
    public void removeStore(Store store){
        if (store != null && this.storeList.contains(store)){
            this.storeList.remove(store);
            store.removeBook(this);
        }
    }
    
    //OneToManyで双方向関連を維持するためのコードを含むgetEditionList(),
    //setEditionList(List<Edition> editionList)の例
    @Transient
    private ArrayList<Edition> latestEditionList = new ArrayList<Edition>();
    public List<Edition> getEditionList() {
        return this.editionList;
    }
    public void setEditionList(List<Edition> editionList) {
        for(Edition newEdition : editionList){
            if (!latestEditionList.contains(newEdition)){
                newEdition.setBook(this);
            }
        }
        for(Edition oldEdition : latestEditionList){
            if (!editionList.contains(oldEdition)){
                oldEdition.setBook(null);
            }
        }
        this.editionList = editionList;
        latestEditionList = new ArrayList<Edition>(this.editionList);
    }
    
    /** DDBのviewが使用する */
    @Override
    public String getDDBEntityTitle(){
        String result = this.getName();
        result += this.getAuthor() != null ? this.getAuthor().getName() : "";
        return result;
    }
    
//    public List<Store> getStoreList2() {
//        return storeList2;
//    }
//    public void setStoreList2(List<Store> storeList2) {
//        this.storeList2 = storeList2;
//    }
    public Isbn getIsbn() {
        return isbn;
    }
    public void setIsbn(Isbn isbn) {
        this.isbn = isbn;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Author getAuthor() {
        return author;
    }
    public void setAuthor(Author author) {
        this.author = author;
    }
    public Date getPublishedAt() {
        return publishedAt;
    }
    public void setPublishedAt(Date publishedAt) {
        this.publishedAt = publishedAt;
    }
    //Eclipseのgetter/setter自動生成では(booleanではなく
    //Booleanの場合)is名前形式のgetterは生成されない。
    //Wicket はis名前形式のgetter以外にget名前形式でもよい。
    public Boolean getCampaign1() {
        return campaign1;
    }
    public void setCampaign1(Boolean campaign1) {
        this.campaign1 = campaign1;
    }
    public Boolean getIsCampaign2() {
        return isCampaign2;
    }
    public void setIsCampaign2(Boolean isCampaign2) {
        this.isCampaign2 = isCampaign2;
    }
    public Byte getAttByte() {
        return attByte;
    }
    public void setAttByte(Byte attByte) {
        this.attByte = attByte;
    }
    public Short getAttShort() {
        return attShort;
    }
    public void setAttShort(Short attShort) {
        this.attShort = attShort;
    }
    public Integer getIntegerAttribute() {
        return integerAttribute;
    }
    public void setIntegerAttribute(Integer integerAttribute) {
        this.integerAttribute = integerAttribute;
    }
    public Long getAttLong() {
        return attLong;
    }
    public void setAttLong(Long attLong) {
        this.attLong = attLong;
    }
    public Float getAttFloat() {
        return attFloat;
    }
    public void setAttFloat(Float attFloat) {
        this.attFloat = attFloat;
    }
    public Double getAttDouble() {
        return attDouble;
    }
    public void setAttDouble(Double attDouble) {
        this.attDouble = attDouble;
    }
    public Character getAttCharacter() {
        return attCharacter;
    }
    public void setAttCharacter(Character attCharacter) {
        this.attCharacter = attCharacter;
    }
    public EnumLanguage getAttEnum() {
        return attEnum;
    }
    public void setAttEnum(EnumLanguage attEnum) {
        this.attEnum = attEnum;
    }

    /** debug用 */
    public void debugPrint(){
        String info = "<" + this.getClass().getSimpleName() + ">";
        info += "\nname=" + this.getName();
        info += "\npublishedAt=" + this.getPublishedAt();
        if(this.author != null) info += "\n" + this.getAuthor().getDebugInfo();
        if(this.isbn != null) info += "\n" + this.getIsbn().getDebugInfo();
        for(Edition edition : this.getEditionList()){
            info += "\n" + edition.getDebugInfo();
        }
        for(Store bookStore : this.getStoreList()){
            info += "\n" + bookStore.getDebugInfo();
        }
        info += "\n</" + this.getClass().getSimpleName() + ">";
        System.out.println("--------------------------------------");
        System.out.println(info);
        System.out.println("--------------------------------------");
    }

    public String getName2() {
        return name2;
    }
    public void setName2(String name2) {
        this.name2 = name2;
    }
    public Date getPublishedAt2() {
        return publishedAt2;
    }
    public void setPublishedAt2(Date publishedAt2) {
        this.publishedAt2 = publishedAt2;
    }
    public Boolean getCampaign12() {
        return campaign12;
    }
    public void setCampaign12(Boolean campaign12) {
        this.campaign12 = campaign12;
    }
    public Boolean getIsCampaign22() {
        return isCampaign22;
    }
    public void setIsCampaign22(Boolean isCampaign22) {
        this.isCampaign22 = isCampaign22;
    }
    public EnumLanguage getAttEnum2() {
        return attEnum2;
    }
    public void setAttEnum2(EnumLanguage attEnum2) {
        this.attEnum2 = attEnum2;
    }
    public Isbn getIsbn2() {
        return isbn2;
    }
    public void setIsbn2(Isbn isbn2) {
        this.isbn2 = isbn2;
    }
    public Author getAuthor2() {
        return author2;
    }
    public void setAuthor2(Author author2) {
        this.author2 = author2;
    }
    public List<Edition> getEditionList2() {
        return editionList2;
    }
    public void setEditionList2(List<Edition> editionList2) {
        this.editionList2 = editionList2;
    }
//    public List<Store> getStoreList2() {
//        return storeList2;
//    }
//    public void setStoreList2(List<Store> storeList2) {
//        this.storeList2 = storeList2;
//    }
    public Integer getIntegerAttribute2() {
        return integerAttribute2;
    }
    public void setIntegerAttribute2(Integer integerAttribute2) {
        this.integerAttribute2 = integerAttribute2;
    }
    public Byte getAttByte2() {
        return attByte2;
    }
    public void setAttByte2(Byte attByte2) {
        this.attByte2 = attByte2;
    }
    public Short getAttShort2() {
        return attShort2;
    }
    public void setAttShort2(Short attShort2) {
        this.attShort2 = attShort2;
    }
    public Long getAttLong2() {
        return attLong2;
    }
    public void setAttLong2(Long attLong2) {
        this.attLong2 = attLong2;
    }
    public Float getAttFloat2() {
        return attFloat2;
    }
    public void setAttFloat2(Float attFloat2) {
        this.attFloat2 = attFloat2;
    }
    public Double getAttDouble2() {
        return attDouble2;
    }
    public void setAttDouble2(Double attDouble2) {
        this.attDouble2 = attDouble2;
    }
    public Character getAttCharacter2() {
        return attCharacter2;
    }
    public void setAttCharacter2(Character attCharacter2) {
        this.attCharacter2 = attCharacter2;
    }
    public BigDecimal getCost() {
        return cost;
    }
    public void setCost(BigDecimal cost) {
        this.cost = cost;
    }
    public BigDecimal getCost2() {
        return cost2;
    }
    public void setCost2(BigDecimal cost2) {
        this.cost2 = cost2;
    }
    
//setStoreList,getStoreList,setEditionList,getEditionListに
//対策コードを追加したので以下は使用しない。
//    /*
//     * getter/setterに加えて、このメソッドを追加する理由
//     * OneToManyの関連に関連先を追加するためには、
//     * book.getEditionList().add(newEdition)として、persist(book)としても追加されない。
//     * newEdition.setBook(book)としてから、persist(book)しなければならない。
//     * ただ、シーケンスとしてbook側を変更するだけにしたい場合もあるので、
//     * 以下のようなaddEdition(newEdition)を実装した。
//     * ただし、双方向維持のための常套コードのように
//     * edition.setBook(book)からbook.addEdition(edition)とすると、
//     * 復元時に"復元中にコレクションが変更された"という例外が発生するので、
//     * edition.setBook(book)からbook.addEdition(edition)は使用しないようにした。
//     */
//    public void addEdition(Edition edition){
////        if(edition!=null && !this.editionList.contains(edition)){
////            edition.setBook(this);
////            this.editionList.add(edition);
////        }
//        //Hibernate ORM 5.2 User Guide2.7.2 Bidirectional @OneToMany 例を参考
//        this.editionList.add(edition);
//        edition.setBook(this);
//    }
//
//    /**
//     * Hibernate ORM 5.2 User Guide2.7.2 Bidirectionaln@OneToMany 例を参考
//     * @param edition
//     */
//    public void removeEdition(Edition edition){
//        this.editionList.remove(edition);
//        edition.setBook(null);
//    }
//
//    /**
//     * ManyToManyのowning sideなので自分のリストのみ更新する。
//     * Store(mappedBy側)との間でaddBookから折り返すと復元時に
//     * "復元中にコレクションが変更された"例外が発生すると思われる
//     */
//    public void addStore(Store store){
//        //Hibernate ORM 5.2 User Guide2.7.2 Bidirectionaln@ManyToManyでは
//        //Store(mappedBy側)にHelperメソッドは無い。
////        if (store != null && !this.storeList.contains(store)){
////            this.storeList.add(store);
////        }
//        this.storeList.add(store);
//        store.getBookList().add(this);
//    }
//    
//    /**
//     * 双方向関連を整合させるためのアプリケーションコード
//     */
//    public void removeStore(Store store){
//        this.storeList.remove(store);
//        store.getBookList().remove(this);
//    }
}


/*
 * モデル例
 */
package jp.co.nextdesign.domain;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;

/**
 * 版
 */
@Entity
public class Edition extends DdBaseEntity {
    private static final long serialVersionUID = 1L;

    /** 版番号 */
    private Integer editionNumber;
    
    /** 版名 */
    private String name;
    
    /** 書籍 */
    @ManyToOne
    //@JoinColumn(name="book_id") //省略可
    private Book book;

    /** 書籍 */
    @ManyToOne
    //@JoinColumn(name="book_id") //省略可
    private Book book2;

    public Edition(){
        super();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getEditionNumber() {
        return editionNumber;
    }

    public void setEditionNumber(Integer editionNumber) {
        this.editionNumber = editionNumber;
    }

    public Book getBook() {
        return book;
    }

    //Book#addEdition,removeEditionから使用する
    public void setBook(Book book) {
        this.book = book;
    }    

    public Book getBook2() {
        return book2;
    }

    public void setBook2(Book book2) {
        this.book2 = book2;
    }

    @Override
    public String getDDBEntityTitle(){
        return this.name;
    }

    /** debug */
    public String getDebugInfo(){
        String info = "<" + this.getClass().getSimpleName() + ">";
        info += "\neditionNumber=" + this.getEditionNumber();
        info += "\n</" + this.getClass().getSimpleName() + ">";
        return info;
    }
}


/*
 * モデル例
 */
package jp.co.nextdesign.domain;
public enum EnumLanguage {
    JA("日本語"),
    EN("英語");

    private String fullName;
    private EnumLanguage(String fullName){
        this.fullName = fullName;
    }

    @Override
    public String toString(){
        return this.fullName;
    }
}


/*
 * モデル例
 */
package jp.co.nextdesign.domain;
import java.util.Date;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;

/**
 * ISBN
 */
@Entity
public class Isbn extends DdBaseEntity {
    private static final long serialVersionUID = 1L;
    
    /** 書籍 one-to-one non-owning/child Hibernate ORM 5.2 User Guide2.7と異なる */
    @OneToOne(mappedBy="isbn") //相手側(owning)の属性
    private Book book;

    /** 書籍 one-to-one non-owning/child Hibernate ORM 5.2 User Guide2.7と異なる */
    @OneToOne(mappedBy="isbn2") //相手側(owning)の属性
    private Book book2;

    /** グループ記号 */
    private String groupCode;
    
    /** 出版社記号 */
    private Integer publisherCode;
    
    /** 書名記号 */
    private String itemCode;
    
    /** チェックディジット */
    private String checkDigit;
    
    /** 決定日 */
    private Date determinatedAt;
    
    /** 旧ISBN */
    private Boolean isOldIsbn;
    
    /** コンストラクタ */
    public Isbn(){
        super();
    }

    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }
    
    public Book getBook2() {
        return book2;
    }

    public void setBook2(Book book2) {
        this.book2 = book2;
    }

    public String getGroupCode() {
        return groupCode;
    }

    public void setGroupCode(String groupCode) {
        this.groupCode = groupCode;
    }

    public Integer getPublisherCode() {
        return publisherCode;
    }

    public void setPublisherCode(Integer publisherCode) {
        this.publisherCode = publisherCode;
    }

    public String getItemCode() {
        return itemCode;
    }

    public void setItemCode(String itemCode) {
        this.itemCode = itemCode;
    }

    public String getCheckDigit() {
        return checkDigit;
    }

    public void setCheckDigit(String checkDigit) {
        this.checkDigit = checkDigit;
    }
    
    public Date getDeterminatedAt() {
        return determinatedAt;
    }

    public void setDeterminatedAt(Date determinatedAt) {
        this.determinatedAt = determinatedAt;
    }

    public Boolean getIsOldIsbn() {
        return isOldIsbn;
    }

    public void setIsOldIsbn(Boolean isOldIsbn) {
        this.isOldIsbn = isOldIsbn;
    }

    @Override
    public String getDDBEntityTitle(){
        return "ISBN" + this.getGroupCode() + "-" 
        + this.getPublisherCode() + "-" + this.getItemCode();
    }
    
    /** debug */
    public String getDebugInfo(){
        String info = "<" + this.getClass().getSimpleName() + ">";
        info += "\ngroupCode=" + this.getGroupCode();
        info += "\npublisherCode=" + this.getPublisherCode();
        info += "\nitemNumber=" + this.getItemCode();
        info += "\n</" + this.getClass().getSimpleName() + ">";
        return info;
    }
}


/*
 * モデル例
 */
package jp.co.nextdesign.domain.store;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.ManyToMany;
import jp.co.nextdesign.domain.Book;
import jp.co.nextdesign.domain.ddb.DdBaseEntity;

/**
 * 書店
 */
@Entity
public class Store extends DdBaseEntity {
    private static final long serialVersionUID = 1L;
    
    /** 書店名 */
    private String name;
    
    /** 書籍リスト */
    @ManyToMany(mappedBy="storeList", fetch=FetchType.EAGER)
    private List<Book> bookList;

//    /** 書籍リスト Book側のコメントを参照 */
//    @ManyToMany(mappedBy="storeList2")
//    private List<Book> bookList2;

    /** コンストラクタ */
    public Store(){
        super();
        this.name = "";
        this.bookList = new ArrayList<Book>();
//        this.bookList2 = new ArrayList<Book>();
    }

    /** DDBのviewが使用する */
    @Override
    public String getDDBEntityTitle(){
        return this.name;
    }
    
    //ManyToManyで双方向関連を維持するためのaddStore,removeStoreを含む。
    //owning側ではなくmappedBy側から使用するが、両側に実装する。
    public List<Book> getBookList() {
        return bookList;
    }
    public void setBookList(List<Book> bookList) {
        this.bookList = bookList;
    }
    public void addBook(Book book){
        if (book != null && !this.bookList.contains(book)){
            this.bookList.add(book);
            book.addStore(this);
        }
    }
    public void removeBook(Book book){
        if (book != null && this.bookList.contains(book)){
            this.bookList.remove(book);
            book.removeStore(this);
        }
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    /** debug */
    public String getDebugInfo(){
        String info = "<" + this.getClass().getSimpleName() + ">";
        info += "\nname=" + this.getName();
        info += "\n</" + this.getClass().getSimpleName() + ">";
        return info;
    }
}

ダウンロードページへ

[戻る] [このページの先頭に戻る]


Copyright 2001-2017, All Rights Reserved