10

私の特定のケースでは、識別子列戦略を利用しています。これは、私の JPA 実装 (Hibernate) が特別なDTYPE列を持つusersテーブルを作成することを意味します。この列には、エンティティのクラス名が含まれます。たとえば、ユーザーテーブルにはTrialUserPayingUserのサブクラスを含めることができます。これらのクラス名はDTYPE列にあるため、EntityManager がデータベースからエンティティをロードするときに、どのタイプのクラスをインスタンス化するかがわかります。

エンティティ型を変換する 2 つの方法を試しましたが、どちらも汚いハックのように感じます。

  1. ネイティブ クエリを使用して列に対して手動で UPDATE を実行し、その値を変更します。これは、プロパティの制約が類似しているエンティティに対して機能します。
  2. ターゲット タイプの新しいエンティティを作成し、BeanUtils.copyProperties()呼び出しを実行してプロパティを移動し、新しいエンティティを保存してから、新しい Id を古い Id に手動で置き換える名前付きクエリを呼び出して、すべての外部キー制約が維持されます。

#1の問題は、この列を手動で変更すると、JPAがこのエンティティを更新/永続コンテキストに再アタッチする方法がわからないことです。ID 1234のPayingUserではなく、ID 1234 のTrialUserが必要です。失敗します。ここで、おそらく EntityManager.clear() を実行して、すべてのエンティティをデタッチ/Per をクリアすることができます。コンテキストですが、これはサービス Bean であるため、システムのすべてのユーザーの保留中の変更が消去されます。

#2 の問題は、TrialUserを削除すると、Cascade=ALL に設定したすべてのプロパティも削除されることです。すべての拡張オブジェクト グラフを削除するのではなく、別のユーザーにスワップしようとしているだけなので、これは悪いことです。

更新 1 : #2 の問題により、ほとんど使用できなくなったため、動作させることをあきらめました。より洗練されたハックは間違いなくナンバー 1 であり、私はこの点である程度の進歩を遂げました。重要なのは、最初に基礎となる Hibernate Session への参照を取得し (JPA 実装として Hibernate を使用している場合)、Session.evict(user) メソッドを呼び出して、永続化コンテキストからその単一のオブジェクトのみを削除することです。残念ながら、これに対する純粋な JPA サポートはありません。サンプルコードは次のとおりです。

  // Make sure we save any pending changes
  user = saveUser(user);

  // Remove the User instance from the persistence context
  final Session session = (Session) entityManager.getDelegate();
  session.evict(user);

  // Update the DTYPE
  final String sqlString = "update user set user.DTYPE = '" + targetClass.getSimpleName() + "' where user.id = :id";
  final Query query = entityManager.createNativeQuery(sqlString);
  query.setParameter("id", user.getId());
  query.executeUpdate();

  entityManager.flush();   // *** PROBLEM HERE ***

  // Load the User with its new type
  return getUserById(userId); 

この例外をスローする手動のflush()に注意してください。

org.hibernate.PersistentObjectException: detached entity passed to persist: com.myapp.domain.Membership
at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:102)
at org.hibernate.impl.SessionImpl.firePersistOnFlush(SessionImpl.java:671)
at org.hibernate.impl.SessionImpl.persistOnFlush(SessionImpl.java:663)
at org.hibernate.engine.CascadingAction$9.cascade(CascadingAction.java:346)
at org.hibernate.engine.Cascade.cascadeToOne(Cascade.java:291)
at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:239)
at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:192)
at org.hibernate.engine.Cascade.cascadeCollectionElements(Cascade.java:319)
at org.hibernate.engine.Cascade.cascadeCollection(Cascade.java:265)
at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:242)
at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:192)
at org.hibernate.engine.Cascade.cascade(Cascade.java:153)
at org.hibernate.event.def.AbstractFlushingEventListener.cascadeOnFlush(AbstractFlushingEventListener.java:154)
at org.hibernate.event.def.AbstractFlushingEventListener.prepareEntityFlushes(AbstractFlushingEventListener.java:145)
at org.hibernate.event.def.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:88)
at org.hibernate.event.def.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:58)
at org.hibernate.impl.SessionImpl.autoFlushIfRequired(SessionImpl.java:996)
at org.hibernate.impl.SessionImpl.executeNativeUpdate(SessionImpl.java:1185)
at org.hibernate.impl.SQLQueryImpl.executeUpdate(SQLQueryImpl.java:357)
at org.hibernate.ejb.QueryImpl.executeUpdate(QueryImpl.java:51)
at com.myapp.repository.user.JpaUserRepository.convertUserType(JpaUserRepository.java:107)

Userが OneToMany Set を持つMembershipエンティティが問題を引き起こしていることがわかります。舞台裏で何が起こっているのか、このナットをクラックするのに十分なほど知りません.

更新 2 : これまでのところ、上記のコードに示すように DTYPE を変更してから、 entityManager.clear()を呼び出すだけで機能します。

永続化コンテキスト全体をクリアすることの影響を完全には理解していません。代わりに、更新される特定のエンティティでSession.evict()を動作させたいと思っていました。

4

5 に答える 5

3

だから私はついに実用的な解決策を見つけました:

DTYPEを更新するためにEntityManagerを破棄します。これは主に、Query.executeUpdate()をトランザクション内で実行する必要があるためです。既存のトランザクション内で実行してみることができますが、それはおそらく、変更しているエンティティの同じ永続コンテキストに関連付けられています。これが意味するのは、 DTYPEを更新した後、エンティティをevict()する方法を見つける必要があるということです。簡単な方法はentityManager.clear()を呼び出すことですが、これはあらゆる種類の副作用をもたらします(JPA仕様でそれについて読んでください)。より良い解決策は、基礎となるデリゲート(私の場合はHibernateセッション)を取得し、 Session.evict(user)を呼び出すことです。。これはおそらく単純なドメイングラフで機能しますが、私のものは非常に複雑でした。@OneToOne(cascade = CascadeType.ALL)のような既存のJPAアノテーションで@Cascade(CascadeType.EVICT)を正しく機能させることができませんでした。また、ドメイングラフを手動でセッションに渡し、各親エンティティにその子を削除させてみました。これも不明な理由で機能しませんでした。

entityManager.clear()だけが機能する状況に置かれましたが、副作用を受け入れることができませんでした。次に、エンティティ変換専用の個別の永続性ユニットを作成してみました。clear()操作を変換を担当するPCだけにローカライズできると思いました。新しいPC、対応する新しいEntityManagerFactory 、それに対応する新しいトランザクションマネージャーをセットアップし、適切なPCに対応するトランザクションでexecuteUpdate()を手動でラップするために、このトランザクションマネージャーをリポジトリに手動で挿入します。ここで、Spring / JPAコンテナ管理のトランザクションについては十分に理解していないと言わざるを得ません。これは、ローカル/手動トランザクションを取得しようとする悪夢になってしまったためです。executeUpdate()を使用すると、コンテナー管理トランザクションがサービス層から引き込まれます。

この時点で、私はすべてを捨てて、このクラスを作成しました。

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public class JdbcUserConversionRepository implements UserConversionRepository {

@Resource
private UserService userService;

private JdbcTemplate jdbcTemplate;

@Override
@SuppressWarnings("unchecked")
public User convertUserType(final User user, final Class targetClass) {

        // Update the DTYPE
        jdbcTemplate.update("update user set user.DTYPE = ? where user.id = ?", new Object[] { targetClass.getSimpleName(), user.getId() });

        // Before we try to load our converted User back into the Persistence
        // Context, we need to remove them from the PC so the EntityManager
        // doesn't try to load the cached one in the PC. Keep in mind that all
        // of the child Entities of this User will remain in the PC. This would
        // normally cause a problem when the PC is flushed, throwing a detached
        // entity exception. In this specific case, we return a new User
        // reference which replaces the old one. This means if we just evict the
        // User, then remove all references to it, the PC will not be able to
        // drill down into the children and try to persist them.
        userService.evictUser(user);

        // Reload the converted User into the Persistence Context
        return userService.getUserById(user.getId());
    }

    public void setDataSource(final DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
}

この方法には、機能させるための2つの重要な部分があります。

  1. @Transactional(propagation = Propagation.NOT_SUPPORTED)でマークしました。これ により、サービスレイヤーからのコンテナー管理トランザクションが一時停止され、PCの外部で変換が実行できるようになります。
  2. 変換されたエンティティをPCに再ロードする前に、 userService.evictUser(user);を使用して現在PCに保存されている古いコピーを削除します。。このためのコードは、単にSessionインスタンスを取得し、 evict(user)を呼び出すことです。詳細については、コードのコメントを参照してください。ただし、基本的にこれを行わない場合、getUserを呼び出すと、タイプが異なるというエラーがスローされることを除いて、PCに残っているキャッシュエンティティを返そうとします。

私の最初のテストはうまくいきましたが、このソリューションにはまだいくつかの問題があるかもしれません。彼らが明らかにされたので、私はこれを更新し続けます。

于 2009-04-18T15:47:17.647 に答える
2

ユーザー タイプでサブスクリプション レベルを表す必要は本当にありますか? システムにサブスクリプション タイプを追加してみませんか?

この関係は次のように表されます。ユーザーは 1 つのサブスクリプションを持っています。サブスクリプションの履歴を保持したい場合は、1 対多の関係としてモデル化することもできます。

次に、ユーザーのサブスクリプション レベルを変更する場合は、新しいユーザーをインスタンス化する代わりに、新しいサブスクリプションを作成してユーザーに割り当てます。

// When you user signs up
User.setSubscription(new FreeSubscription());

// When he upgrades to a paying account
User.setSubscription(new PayingSubscription());

TrialUser は PayingUser と本当に違うのですか? おそらくそうではありません。継承ではなく集約を使用する方が適切です。

サブスクリプション データは、別のテーブルに格納する (エンティティとしてマップ) か、ユーザー テーブル内に格納する (コンポーネントとしてマップ) ことができます。

于 2009-04-14T19:18:46.380 に答える
1

この問題は、何よりも Java に関連しています。インスタンスのランタイム タイプを (実行時に) 変更することはできないため、hibernate はこの種のシナリオを提供しません (たとえばレールとは異なります)。

ユーザーをセッションから削除する場合は、関連付けられているエンティティを削除する必要があります。いくつかの選択肢があります:

  • evict=cascade を使用してエンティティをマップします ( Session#evict javadoc を参照) 。
  • 関連付けられているすべてのエンティティを手動で削除します (これは面倒な場合があります)。
  • セッションをクリアして、すべてのエンティティを削除します (もちろん、ローカル セッション キャッシュは失われます)。

私はgrailsで同様の問題を抱えていましたが、私の解決策はgrigoryの解決策に似ています。関連するエンティティを含むすべてのインスタンス フィールドをコピーし、古いエンティティを削除して新しいエンティティを書き込みます。それほど多くの関係がない場合、これは簡単ですが、データ モデルが複雑な場合は、説明したネイティブ SQL ソリューションを使用する方がよいでしょう。

興味のある方のために、ソースは次のとおりです。

def convertToPacient = {
    withPerson(params.id) {person ->
      def pacient = new Pacient()
      pacient.properties = person.properties
      pacient.id = null
      pacient.processNumber = params.processNumber
      def ap = new Appointment(params)
      pacient.addToAppointments(ap);

      Person.withTransaction {tx ->
        if (pacient.validate() && !pacient.hasErrors()) {

          //to avoid the "Found two representations of same collection" error
          //pacient.attachments = new HashSet(person.attachments);
          //pacient.memberships = new HashSet(person.memberships);
          def groups = person?.memberships?.collect {m -> Group.get(m.group.id)}
          def attachs = []
          person.attachments.each {a ->
            def att = new Attachment()
            att.properties = a.properties
            attachs << att
          }

          //need an in in order to add the person to a group
          person.delete(flush: true)
          pacient.save(flush: true)
          groups.each {g -> pacient.addToGroup(g)};
          attachs.each {a -> pacient.addToAttachments(a)}
          //pacient.attachments.each {att -> att.id = null; att.version = null; att.person = pacient};

          if (!pacient.save()) {
            tx.setRollbackOnly()
            return
          }
        }
      }
于 2009-04-16T17:58:02.313 に答える
0

この問題は、何よりもJavaに関連しています。インスタンスのランタイムタイプを(実行時に)変更することはできないため、hibernateはこの種のシナリオを提供しません(たとえば、レールとは異なります)。

私は同様の問題を抱えている(またはそう思っていた)ので、この投稿に遭遇しました。問題はテクノロジーのタイプを変更するメカニズムにあるのではないと私は信じるようになりましたが、タイプを一般的に進化させることは決して簡単でも簡単でもありません。問題は、タイプがさまざまな属性を持っていることが非常に多いことです。動作の違いのためだけに異なるタイプにマッピングしない限り、追加情報をどう処理するかは明確ではありません。このため、新しいオブジェクトの構築に基づいて型を移行することは理にかなっています。

この問題は「Javaに関連する」ものではありません。これは、一般的な型理論、より具体的にはここでのオブジェクトリレーショナルマッピングに関連しています。あなたの言語がタイピングをサポートしているなら、実行時にオブジェクトのタイプを自動的に変更し、残った情報を使って魔法のようにインテリジェントなことを行う方法を聞きたいです。FUDを広めている限り、アドバイスを受ける相手に注意してください。Railsはフレームワークであり、Rubyは言語です。

于 2009-06-08T23:10:10.377 に答える
-1

あなたが改宗と言うとき、あなたはおそらく問題を誤って伝えています。私の意見では、あなたが実際にやろうとしているのは、あるクラスのインスタンスを他のクラスのインスタンスに基づいて構築することです。たとえば、次のようになります。

public PayingUser(TrialUser theUser) {
...
}

その後、古い試用ユーザーを削除し、新しい有料ユーザーを維持できます。

于 2009-04-13T17:24:27.740 に答える