web-dev-qa-db-ja.com

JPAを使用してテンポラルテーブルを実装するにはどうすればよいですか?

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

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

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

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

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

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

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

データモデル: Non Temporal Data Model

チーム

@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番目のシナリオ:時間モデル

データモデル: Temporal Data Model

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

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

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

25
Alfredo Osorio

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

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

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

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

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

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

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

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

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

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

この回答には、リファレンスブック「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」は 複合キー になりますが、これも避けます。 example を見ると、personとresidenceの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シーズン
  • Win
  • 損失
  • .。

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

TeamInterval

  • ID
  • IDチーム
  • から
  • Win
  • 損失
  • .。

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

7
ChrLipp

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

2
James

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

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

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

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

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

1
Vojtech Szocs

テーブル名とスキーマ全体が静的であると想定しているため、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.
1
Alex M