26

EclipseLinkを使用してJPA2にテンポラルテーブルを実装する方法を知りたいです。時間的とは、有効期間を定義するテーブルを意味します。

私が直面している問題の1つは、参照テーブルの性質上、主キーに有効期間が含まれているため、参照テーブルが参照テーブル(一時テーブル)に対する外部キー制約を持たなくなることです。

  • エンティティの関係をどのようにマッピングしますか?
  • それは、私のエンティティがそれらの有効な時間のエンティティとの関係をもはや持つことができないことを意味しますか?
  • これらの関係を初期化する責任は、ある種のサービスまたは特殊なDAOで手動で行う必要がありますか?

私が見つけた唯一のものは、これを処理するDAOFusionと呼ばれるフレームワークです。

  • これを解決する他の方法はありますか?
  • このトピックに関する例またはリソース(時制データベースを備えたJPA)を提供していただけますか?

これは、データモデルとそのクラスの架空の例です。これは、時間的な側面を扱う必要のない単純なモデルとして始まります。

最初のシナリオ:非時間モデル

データモデル非時間データモデル

チーム

@Entity
public class Team implements Serializable {

    private Long id;
    private String name;
    private Integer wins = 0;
    private Integer losses = 0;
    private Integer draws = 0;
    private List<Player> players = new ArrayList<Player>();

    public Team() {

    }

    public Team(String name) {
        this.name = name;
    }


    @Id
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SEQTEAMID")
    @SequenceGenerator(name="SEQTEAMID", sequenceName="SEQTEAMID", allocationSize=1)
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Column(unique=true, nullable=false)
    public String getName() {
        return name;
    }

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

    public Integer getWins() {
        return wins;
    }

    public void setWins(Integer wins) {
        this.wins = wins;
    }

    public Integer getLosses() {
        return losses;
    }

    public void setLosses(Integer losses) {
        this.losses = losses;
    }

    public Integer getDraws() {
        return draws;
    }

    public void setDraws(Integer draws) {
        this.draws = draws;
    }

    @OneToMany(mappedBy="team", cascade=CascadeType.ALL)
    public List<Player> getPlayers() {
        return players;
    }

    public void setPlayers(List<Player> players) {
        this.players = players;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Team other = (Team) obj;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }


}

プレイヤー

@Entity
@Table(uniqueConstraints={@UniqueConstraint(columnNames={"team_id","number"})})
public class Player implements Serializable {

    private Long id;
    private Team team;
    private Integer number;
    private String name;

    public Player() {

    }

    public Player(Team team, Integer number) {
        this.team = team;
        this.number = number;
    }

    @Id
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SEQPLAYERID")
    @SequenceGenerator(name="SEQPLAYERID", sequenceName="SEQPLAYERID", allocationSize=1)
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @ManyToOne
    @JoinColumn(nullable=false)
    public Team getTeam() {
        return team;
    }

    public void setTeam(Team team) {
        this.team = team;
    }

    @Column(nullable=false)
    public Integer getNumber() {
        return number;
    }

    public void setNumber(Integer number) {
        this.number = number;
    }

    @Column(unique=true, nullable=false)
    public String getName() {
        return name;
    }

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

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((number == null) ? 0 : number.hashCode());
        result = prime * result + ((team == null) ? 0 : team.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Player other = (Player) obj;
        if (number == null) {
            if (other.number != null)
                return false;
        } else if (!number.equals(other.number))
            return false;
        if (team == null) {
            if (other.team != null)
                return false;
        } else if (!team.equals(other.team))
            return false;
        return true;
    }


}

テストクラス:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"/META-INF/application-context-root.xml"})
@Transactional
public class TestingDao {

    @PersistenceContext
    private EntityManager entityManager;
    private Team team;

    @Before
    public void setUp() {
        team = new Team();
        team.setName("The Goods");
        team.setLosses(0);
        team.setWins(0);
        team.setDraws(0);

        Player player = new Player();
        player.setTeam(team);
        player.setNumber(1);
        player.setName("Alfredo");
        team.getPlayers().add(player);

        player = new Player();
        player.setTeam(team);
        player.setNumber(2);
        player.setName("Jorge");
        team.getPlayers().add(player);

        entityManager.persist(team);
        entityManager.flush();
    }

    @Test
    public void testPersistence() {
        String strQuery = "select t from Team t where t.name = :name";
        TypedQuery<Team> query = entityManager.createQuery(strQuery, Team.class);
        query.setParameter("name", team.getName());
        Team persistedTeam = query.getSingleResult();
        assertEquals(2, persistedTeam.getPlayers().size()); 

        //Change the player number
        Player p = null;
        for (Player player : persistedTeam.getPlayers()) {
            if (player.getName().equals("Alfredo")) {
                p = player;
                break;
            }
        }
        p.setNumber(10);        
    }


}

ここで、チームとプレーヤーが特定の時点でどのようにいたかを履歴に残すように求められます。そのため、追跡するテーブルごとに期間を追加する必要があります。それでは、これらの時間列を追加しましょう。から始めPlayerます。

2番目のシナリオ:時間モデル

データ・モデル: 時間データモデル

ご覧のとおり、主キーを削除して、日付(期間)を含む別のキーを定義する必要がありました。また、テーブル内で繰り返すことができるようになったため、一意の制約を削除する必要がありました。これで、テーブルに現在のエントリと履歴を含めることができます。

Teamを一時的にする必要がある場合、状況はかなり醜くなります。この場合、Playerテーブルが必要とする外部キー制約を削除する必要がありますTeam。問題は、JavaとJPAでそれをどのようにモデル化するかです。

IDは代理キーであることに注意してください。ただし、サロゲートキーには日付を含める必要があります。含めない場合、同じエンティティの複数の「バージョン」を(タイムライン中に)保存することはできません。

4

4 に答える 4

9

私はこのトピックに非常に興味があります。私はこれらのパターンを使用するアプリケーションの開発に数年間取り組んでいます。このアイデアは、ドイツの卒業論文から生まれました。

「DAOFusion」フレームワークを知りませんでした。興味深い情報とリンクを提供してくれました。この情報を提供してくれてありがとう。特にパターンページアスペクトページは素晴らしいです!

あなたの質問に:いいえ、私は他のサイト、例またはフレームワークを指摘することはできません。DAO Fusionフレームワークを使用するか、この機能を自分で実装する必要があります。本当に必要な機能の種類を区別する必要があります。「DAOFusion」フレームワークの観点から言えば、「有効な時間的」と「記録的な時間的」の両方が必要ですか?変更がデータベースに適用されたときの一時的な状態(通常は問題の監査に使用されます)、変更が実際に発生したときの有効な一時的な状態、または実際の生活で有効なときの有効な一時的な状態(アプリケーションによって使用されます)を記録します。これは一時的な記録とは異なる場合があります。ほとんどの場合、1つの次元で十分であり、2番目の次元は必要ありません。

とにかく、一時的な機能はデータベースに影響を与えます。あなたが述べたように:「現在、それらの主キーには有効期間が含まれています」。では、エンティティのIDをどのようにモデル化しますか?私は代理キーの使用を好みます。その場合、これは次のことを意味します。

  • エンティティの1つのID
  • データベース内のオブジェクトの1つのID(行)
  • 時間列

テーブルの主キーはオブジェクトIDです。各エンティティには、オブジェクトIDで識別される1つ以上の(1-n)エントリがテーブルにあります。テーブル間のリンクは、エンティティIDに基づいています。一時的なエントリはデータの量を増やすため、標準の関係は機能しません。標準の1-n関係は、ax * 1-y*n関係になる可能性があります。

これをどのように解決しますか?標準的なアプローチはマッピングテーブルを導入することですが、これは自然なアプローチではありません。1つのテーブルを編集するためだけに(たとえば、居住地の変更が発生した場合)、すべてのプログラマーにとって奇妙なマッピングテーブルを更新/挿入する必要があります。

もう1つのアプローチは、マッピングテーブルを使用しないことです。この場合、参照整合性と外部キーを使用できません。各テーブルは分離されて動作します。あるテーブルから他のテーブルへのリンクは、JPA機能ではなく手動で実装する必要があります。

データベースオブジェクトを初期化する機能は、(DAO Fusionフレームワークのように)オブジェクト内にある必要があります。私はそれをサービスに入れませんでした。それをDAOに渡すか、ActiveRecordパターンを使用するかはあなた次第です。

私の答えは、「すぐに使える」フレームワークを提供していないことを認識しています。あなたは非常に複雑な分野にいます。私の経験リソースからこの使用シナリオまで、見つけるのは非常に困難です。ご質問ありがとうございます!しかしとにかく、私はあなたのデザインであなたを助けたことを願っています。

この回答には、リファレンスブック「SQLでの時間指向データベースアプリケーションの開発」があります。https://stackoverflow.com/a/800516/734687を参照してください。

更新:例

  • 質問:「id」という名前のフィールドである代理キーを持つPERSONテーブルがあるとします。この時点でのすべての参照テーブルには、外部キー制約としてその「ID」があります。ここで一時列を追加する場合は、主キーを「id + from_date+to_date」に変更する必要があります。主キーを変更する前に、まずすべての参照テーブルのすべての外部制約をこの参照テーブル(Person)にドロップする必要があります。私は正しいですか?それが代理キーの意味だと思います。IDは、シーケンスによって生成される可能性のある生成されたキーです。PersonテーブルのビジネスキーはSSNです。
  • 回答:正確ではありません。SSNは自然キーであり、オブジェクトIDには使用しません。また、「id + from_date + to_date」は複合キーになりますが、これも避けます。この例を見ると、人と住居の2つのテーブルがあり、この例では、外部キー住居と1-nの関係があるとします。次に、各テーブルに時間フィールドを追加します。はい、すべての外部キー制約を削除します。個人は2つのIDを取得します。1つは行を識別するためのID(ROW_IDと呼びます)、もう1つは個人自体を識別するためのID(ENTIDY_IDと呼びます)とそのIDのインデックスです。その人も同じです。もちろん、あなたのアプローチも機能しますが、その場合、ROW_IDを変更する操作(時間間隔を閉じるとき)がありますが、これは避けます。

上記の仮定(2つのテーブル、1-n)で実装されたを拡張するには:

  • データベース内のすべてのエントリを表示するクエリ(すべての有効性情報とレコード-別名テクニカル-情報が含まれています):

    SELECT * FROM Person p, Residence r
    WHERE p.ENTITY_ID = r.FK_ENTITY_ID_PERSON          // JOIN 
  • レコード(別名テクニカル情報)を非表示にするクエリ。これは、エンティティのすべての有効な変更を示しています。

    SELECT * FROM Person p, Residence r
    WHERE p.ENTITY_ID = r.FK_ENTITY_ID_PERSON AND
    p.recordTo=[infinity] and r.recordTo=[infinity]    // only current technical state
  • 実際の値を表示するためのクエリ。

    SELECT * FROM Person p, Residence r
    WHERE p.ENTITY_ID = r.FK_ENTITY_ID_PERSON AND
    p.recordTo=[infinity] and r.recordTo=[infinity] AND
    p.validFrom <= [now] AND p.validTo > [now] AND        // only current valid state person
    r.validFrom <= [now] AND r.validTo > [now]            // only current valid state residence

ご覧のとおり、ROW_IDは使用していません。[now]をタイムスタンプに置き換えて、過去にさかのぼります。

更新を反映するように更新
する次のデータモデルをお勧めします。

「PlaysInTeam」テーブルを導入します。

  • ID
  • IDチーム(チームへの外部キー)
  • IDプレーヤー(プレーヤーへの外部キー)
  • から有効
  • ValidTo

チームのプレーヤーをリストするときは、関係が有効であり、[ValdFrom、ValidTo)に含まれている必要がある日付を照会する必要があります。

チームを一時的にするために、私には2つのアプローチがあります。

アプローチ1:シーズンの妥当性をモデル化する「シーズン」テーブルを導入する

  • ID
  • シーズン名(例:2011年夏)
  • から(季節がいつかは誰もが知っているので、おそらく必要ありません)
  • に(季節がいつかは誰もが知っているので、おそらく必要ではありません)

チームテーブルを分割します。チームに属し、時間に関連しないフィールド(名前、住所、...)とシーズンに関連するフィールド(勝ち、負け、..)があります。その場合、TeamとTeamInSeasonを使用します。PlaysInTeamは、TeamではなくTeamInSeasonにリンクできます(考慮する必要があります-Teamを指すようにします)

TeamInSeason

  • ID
  • IDチーム
  • IDシーズン
  • 勝つ
  • 損失
  • ..。

アプローチ2:季節を明示的にモデル化しないでください。チームテーブルを分割します。チームに属し、時間に関連しないフィールド(名前、住所、...)と時間に関連するフィールド(勝ち、負け、..)があります。その場合、TeamとTeamIntervalを使用します。TeamIntervalには、間隔の「from」フィールドと「to」フィールドがあります。PlaysInTeamは、TeamではなくTeamIntervalにリンクできます(Teamにリンクさせます)

TeamInterval

  • ID
  • IDチーム
  • から
  • 勝つ
  • 損失
  • ..。

どちらのアプローチでも、時間に関連するフィールドがないために個別のチームテーブルが必要ない場合は、分割しないでください。

于 2012-03-05T12:03:18.007 に答える
2

正確にはわかりませんが、EclipseLinkは履歴を完全にサポートしています。@DescriptorCustomizerを使用して、ClassDescriptorでHistoryPolicyを有効にできます。

于 2012-03-05T15:30:36.840 に答える
2

DAO Fusionでは、両方のタイムライン(有効性とレコード間隔)でエンティティを追跡することは、そのエンティティをでラップすることによって実現されBitemporalWrapperます。

両耳側性半盲のドキュメントOrderは、通常のエンティティがエンティティによってラップされている例を示していBitemporalOrderます。BitemporalOrderテーブルの行ごとに、有効性とレコード間隔の列、およびOrder(via @ManyToOne)への外部キー参照を含む個別のデータベーステーブルにマップします。

ドキュメントには、各両耳側性ラッパー(例BitemporalOrder)が両耳側性半盲のレコードチェーン内の1つのアイテムを表すことも示されています。したがって、両耳側性ラッパーコレクションを含む上位レベルのエンティティが必要です。たとえば、を含むエンティティが必要Customerです@OneToMany Collection<BitemporalOrder> orders

したがって、「論理的な子」エンティティ(egOrderまたはPlayer)を両耳側性で追跡する必要があり、その「論理的な親」エンティティ(egCustomerまたはTeam)も両耳側性で追跡する必要がある場合は、両方に両耳側性ラッパーを提供する必要があります。あなたが持っているBitemporalPlayerBitemporalTeamBitemporalTeam宣言でき@OneToMany Collection<BitemporalPlayer> playersます。@OneToMany Collection<BitemporalTeam> teamsただし、上記のように、を含むためのより高いレベルのエンティティが必要です。たとえば、コレクションGameを含むエンティティを作成できます。BitemporalTeam

ただし、レコード間隔が不要で、有効期間だけが必要な場合(たとえば、両耳側ではなく、エンティティの単時間追跡)、最善の策は、独自のカスタム実装をロールすることです。

于 2012-04-01T14:03:41.790 に答える
1

テーブル名とスキーマ全体が静的であると想定しているため、JPAでは実行できないようです。

最良のオプションは、JDBCを介してそれを行うことです(たとえば、DAOパターンを使用して)

パフォーマンスが問題である場合、数千万のレコードについて話しているのでない限り、クラスを動的に作成し、コンパイルしてからロードする方が良いとは思えません。

別のオプションとして、ビューを使用することができます(JPAを使用する必要がある場合)。テーブルを抽象化する(@Entity(name = "myView"をマップする)場合は、CREATE OR REPLACEのようにビューを動的に更新/置換する必要があります)。 VIEW usernameView AS SELECT * FROM prefix_sessionId

たとえば、次のように1つのビューを記述できます。

if (EVENT_TYPE = 'crear_tabla' AND ObjectType = 'tabla ' && ObjectName starts with 'userName') 
then CREATE OR REPLACE VIEW userNameView AS SELECT * FROM ObjectName //the generated table.
于 2012-03-03T19:43:24.310 に答える