PersistentObjectException: thực thể tách rời được chuyển sang bền bỉ bởi JPA và Hibernate


237

Tôi có một mô hình đối tượng bền vững JPA có chứa mối quan hệ nhiều-một: một Accountcó nhiều Transactions. A Transactioncó một Account.

Đây là một đoạn mã:

@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
    private Account fromAccount;
....

@Entity
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @OneToMany(cascade = {CascadeType.ALL},fetch= FetchType.EAGER, mappedBy = "fromAccount")
    private Set<Transaction> transactions;

Tôi có thể tạo một Accountđối tượng, thêm giao dịch vào nó và duy trì Accountchính xác đối tượng. Nhưng, khi tôi tạo một giao dịch, sử dụng Tài khoản đã tồn tại và duy trì Giao dịch , tôi nhận được một ngoại lệ:

Nguyên nhân là do

Vì vậy, tôi có thể duy trì một Accountgiao dịch có chứa các giao dịch, nhưng không phải là Giao dịch có Account. Tôi nghĩ điều này là do Accountcó thể không được đính kèm, nhưng mã này vẫn cho tôi ngoại lệ tương tự:

if (account.getId()!=null) {
    account = entityManager.merge(account);
}
Transaction transaction = new Transaction(account,"other stuff");
 // the below fails with a "detached entity" message. why?
entityManager.persist(transaction);

Làm thế nào tôi có thể lưu chính xác a Transaction, được liên kết với một Accountđối tượng đã tồn tại ?


15
Trong trường hợp của tôi, tôi đã thiết lập id của một thực thể mà tôi đang cố gắng sử dụng Entity Manager. Khi tôi gỡ bỏ setter cho id, nó bắt đầu hoạt động tốt.
Rushi Shah

Trong trường hợp của tôi, tôi đã không đặt id, nhưng có hai người dùng sử dụng cùng một tài khoản, một trong số họ vẫn tồn tại một thực thể (chính xác) và lỗi đã xảy ra khi người thứ hai cố gắng duy trì cùng một thực thể, điều đó đã xảy ra kiên trì.
sergioFC

Câu trả lời:


130

Đây là một vấn đề nhất quán hai chiều điển hình. Nó cũng được thảo luận trong liên kết này cũng như liên kết này.

Theo các bài viết trong 2 liên kết trước, bạn cần sửa setters của mình trong cả hai mặt của mối quan hệ hai chiều. Một setter ví dụ cho One side nằm trong liên kết này.

Một ví dụ setter cho nhiều bên nằm trong liên kết này.

Sau khi bạn sửa setters của bạn, bạn muốn khai báo loại truy cập Thực thể là "Thuộc tính". Cách tốt nhất để khai báo loại truy cập "Thuộc tính" là di chuyển TẤT CẢ các chú thích từ các thuộc tính thành viên sang các biểu đồ tương ứng. Một lời cảnh báo lớn là không trộn lẫn các loại truy cập "Trường" và "Thuộc tính" trong lớp thực thể nếu không hành vi không được xác định bởi các đặc tả của JSR-317.


2
ps: chú thích @Id là chú thích mà hibernate sử dụng để xác định loại truy cập.
Diego Plentz 10/03/2015

2
Ngoại lệ là: detached entity passed to persistTại sao cải thiện tính nhất quán làm cho nó hoạt động? Ok, tính nhất quán đã được sửa chữa nhưng đối tượng vẫn bị tách ra.
Gilgamesz

1
@Sam, cảm ơn rất nhiều vì lời giải thích của bạn. Nhưng tôi vẫn không hiểu. Tôi thấy rằng nếu không có "đặc biệt" thì mối quan hệ hai chiều sẽ không được bão hòa. Nhưng, tôi không thấy lý do tại sao đối tượng bị tách ra.
Gilgamesz

27
Xin vui lòng không gửi một câu trả lời mà chỉ trả lời với các liên kết.
El Mac

6
Không thể thấy câu trả lời này có liên quan đến câu hỏi như thế nào?
Eugen Labun

270

Giải pháp rất đơn giản, chỉ cần sử dụng CascadeType.MERGEthay vì CascadeType.PERSISThoặc CascadeType.ALL.

Tôi đã có cùng một vấn đề và CascadeType.MERGEđã làm việc cho tôi.

Tôi hy vọng bạn được sắp xếp.


5
Đáng ngạc nhiên là một trong những làm việc cho tôi quá. Không có nghĩa gì vì CascadeType.ALL bao gồm tất cả các loại tầng khác ... WTF? Tôi có mùa xuân 4.0.4, dữ liệu mùa xuân jpa 1.8.0 và ngủ đông 4.X .. Có ai có bất kỳ suy nghĩ nào tại sao TẤT CẢ không hoạt động, nhưng MERGE không?
Vadim Kirilchuk

22
@VadimKirilchuk Điều này cũng làm việc cho tôi và nó hoàn toàn có ý nghĩa. Vì Giao dịch được PERSISTED, nó cũng cố gắng vào Tài khoản PERSIST và điều đó không hoạt động vì Tài khoản đã có trong db. Nhưng với CascadeType.MERGE, Tài khoản sẽ tự động được hợp nhất thay thế.
Gunslinger

4
Điều này có thể xảy ra nếu bạn không sử dụng giao dịch.
lanoxx

1
một giải pháp khác: cố gắng không chèn đối tượng đã tồn tại :)
Andrii Plotnikov

3
Cảm ơn bạn. Không thể tránh việc chèn đối tượng bền bỉ, nếu bạn có hạn chế cho khóa tham chiếu là KHÔNG NULL. Vì vậy, đây là giải pháp duy nhất. Cám ơn bạn một lần nữa.
makkasi

22

Xóa tầng khỏi thực thể conTransaction , nó sẽ chỉ là:

@Entity class Transaction {
    @ManyToOne // no cascading here!
    private Account account;
}

( FetchType.EAGERcó thể được gỡ bỏ cũng như mặc định cho @ManyToOne)

Đó là tất cả!

Tại sao? Bằng cách nói "xếp tầng TẤT CẢ" trên thực thể con, Transactionbạn yêu cầu mọi hoạt động DB được truyền đến thực thể mẹ Account. Nếu bạn sau đó làm persist(transaction), persist(account)sẽ được gọi là tốt.

Nhưng chỉ các thực thể thoáng qua (mới) có thể được chuyển đến persist( Transactiontrong trường hợp này). Các trạng thái tách rời (hoặc trạng thái không thoáng qua khác) có thể không ( Accounttrong trường hợp này, như đã có trong DB).

Do đó, bạn nhận được ngoại lệ "thực thể tách rời được duy trì" . Các Accountthực thể có nghĩa là! Không phải là Transactionbạn gọi persistvào.


Bạn thường không muốn tuyên truyền từ trẻ em sang cha mẹ. Thật không may, có rất nhiều ví dụ mã trong sách (ngay cả trong những cái tốt) và thông qua mạng, làm chính xác điều đó. Tôi không biết, tại sao ... Có lẽ đôi khi chỉ đơn giản là sao chép lặp đi lặp lại mà không cần suy nghĩ nhiều ...

Đoán xem điều gì xảy ra nếu bạn gọi remove(transaction)vẫn có "thác TẤT CẢ" trong @ManyToOne đó? Các account(btw, với tất cả các giao dịch khác!) Sẽ bị xóa khỏi DB là tốt. Nhưng đó không phải là ý định của bạn, phải không?


Chỉ muốn thêm, nếu ý định của bạn thực sự là cứu đứa trẻ cùng với cha mẹ và cũng xóa cha mẹ cùng với con, như người (cha mẹ) và địa chỉ (con) cùng với addressId được DB tự động tạo ra trước khi gọi lưu vào Person, chỉ cần thực hiện một cuộc gọi để lưu địa chỉ trong phương thức giao dịch của bạn. Bằng cách này, nó sẽ được lưu cùng với id cũng được tạo bởi DB. Không ảnh hưởng đến hiệu suất vì Hibenate vẫn thực hiện 2 truy vấn, chúng tôi chỉ thay đổi thứ tự truy vấn.
Vikky

Nếu chúng ta không thể vượt qua bất cứ điều gì thì nó sẽ mất giá trị mặc định nào cho mọi trường hợp.
Dhwanil Patel

15

Sử dụng hợp nhất là rủi ro và khó khăn, vì vậy đó là một cách giải quyết bẩn trong trường hợp của bạn. Bạn cần nhớ ít nhất rằng khi bạn chuyển một đối tượng thực thể để hợp nhất, nó sẽ ngừng được gắn vào giao dịch và thay vào đó, một thực thể mới, được đính kèm được trả lại. Điều này có nghĩa là nếu bất cứ ai có đối tượng thực thể cũ vẫn thuộc quyền sở hữu của họ, các thay đổi đối với nó sẽ âm thầm bị bỏ qua và vứt bỏ theo cam kết.

Bạn không hiển thị mã hoàn chỉnh ở đây, vì vậy tôi không thể kiểm tra kỹ mẫu giao dịch của bạn. Một cách để có được tình huống như thế này là nếu bạn không có giao dịch hoạt động khi thực hiện hợp nhất và tồn tại. Trong trường hợp đó, nhà cung cấp kiên trì dự kiến ​​sẽ mở một giao dịch mới cho mọi hoạt động JPA bạn thực hiện và ngay lập tức cam kết và đóng nó trước khi cuộc gọi trở lại. Nếu đây là trường hợp, hợp nhất sẽ được chạy trong một giao dịch đầu tiên và sau đó sau khi phương thức hợp nhất trở lại, giao dịch được hoàn thành và đóng lại và thực thể được trả lại bây giờ được tách ra. Việc tồn tại bên dưới nó sau đó sẽ mở một giao dịch thứ hai và cố gắng đề cập đến một thực thể được tách ra, đưa ra một ngoại lệ. Luôn luôn bọc mã của bạn trong một giao dịch trừ khi bạn biết rất rõ những gì bạn đang làm.

Sử dụng giao dịch được quản lý container, nó sẽ trông giống như thế này. Lưu ý: điều này giả sử phương thức nằm trong bean phiên và được gọi thông qua giao diện Local hoặc Remote.

@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void storeAccount(Account account) {
    ...

    if (account.getId()!=null) {
        account = entityManager.merge(account);
    }

    Transaction transaction = new Transaction(account,"other stuff");

    entityManager.persist(account);
}

. Tôi đang phải đối mặt với cùng một vấn đề, tôi đã sử dụng @Transaction(readonly=false). Ở tầng dịch vụ, vẫn im nhận được cùng một vấn đề,
Senthil Arumugam SP

Tôi không thể nói rằng tôi hoàn toàn hiểu lý do tại sao mọi thứ hoạt động theo cách này nhưng việc đặt phương thức và chế độ xem liên tục vào ánh xạ Thực thể cùng với chú thích Giao dịch đã khắc phục vấn đề của tôi rất cảm ơn.
Deadron

Đây thực sự là một giải pháp tốt hơn so với giải pháp nâng cao nhất.
Aleksei Maide

13

Có lẽ trong trường hợp này, bạn đã thu được accountđối tượng của mình bằng cách sử dụng logic hợp nhất và persistđược sử dụng để duy trì các đối tượng mới và nó sẽ khiếu nại nếu hệ thống phân cấp đang có một đối tượng đã tồn tại. Bạn nên sử dụng saveOrUpdatetrong những trường hợp như vậy, thay vì persist.


3
đó là JPA, vì vậy tôi nghĩ phương thức tương tự là .merge (), nhưng điều đó mang lại cho tôi ngoại lệ tương tự. Để rõ ràng, Giao dịch là một đối tượng mới, Tài khoản thì không.
Paul Sanwald

@PaulSanwald Sử dụng mergetrên transactionđối tượng bạn có bị lỗi tương tự không?
dan

Thật ra, không, tôi đã nói sai. nếu tôi .merge (giao dịch), thì giao dịch không được duy trì.
Paul Sanwald

@PaulSanwald Hmm, bạn có chắc transactionlà không kiên trì? Làm thế nào bạn kiểm tra. Lưu ý rằng mergeđang trả về một tham chiếu đến đối tượng kiên trì.
dan

đối tượng được trả về bởi .merge () có id null. Ngoài ra, tôi đang thực hiện một .find ALL () và đối tượng của tôi không ở đó.
Paul Sanwald

12

Đừng truyền id (pk) cho phương thức kiên trì hoặc thử phương thức save () thay vì vẫn tồn tại ().


2
Lời khuyên tốt! Nhưng chỉ khi id được tạo. Trong trường hợp được chỉ định, việc đặt id là bình thường.
Aldian

Điều này làm việc cho tôi. Ngoài ra, bạn có thể sử dụng TestEntityManager.persistAndFlush()để tạo một cá thể được quản lý và liên tục sau đó đồng bộ hóa bối cảnh bền vững với cơ sở dữ liệu cơ bản. Trả về thực thể nguồn ban đầu
Anyul Rivas

7

Vì đây là một câu hỏi rất phổ biến, tôi đã viết bài viết này , trên đó câu trả lời này dựa trên.

Để khắc phục sự cố, bạn cần thực hiện theo các bước sau:

1. Xóa tầng liên kết trẻ em

Vì vậy, bạn cần phải loại bỏ @CascadeType.ALLkhỏi @ManyToOnehiệp hội. Các thực thể con không nên xếp tầng cho các hiệp hội phụ huynh. Chỉ các thực thể cha mẹ nên xếp tầng cho các thực thể con.

@ManyToOne(fetch= FetchType.LAZY)

Lưu ý rằng tôi đặt fetchthuộc tính là FetchType.LAZYtìm nạp háo hức rất tệ cho hiệu suất .

2. Đặt cả hai mặt của hiệp hội

Bất cứ khi nào bạn có một liên kết hai chiều, bạn cần đồng bộ hóa cả hai bên bằng cách sử dụng addChildremoveChildphương thức trong thực thể mẹ:

public void addTransaction(Transaction transaction) {
    transcations.add(transaction);
    transaction.setAccount(this);
}

public void removeTransaction(Transaction transaction) {
    transcations.remove(transaction);
    transaction.setAccount(null);
}

Để biết thêm chi tiết về lý do tại sao việc đồng bộ hóa cả hai đầu của một hiệp hội hai chiều, hãy xem bài viết này .


4

Trong định nghĩa thực thể của bạn, bạn không chỉ định @JoinColumn cho việc Accounttham gia vào a Transaction. Bạn sẽ muốn một cái gì đó như thế này:

@Entity
public class Transaction {
    @ManyToOne(cascade = {CascadeType.ALL},fetch= FetchType.EAGER)
    @JoinColumn(name = "accountId", referencedColumnName = "id")
    private Account fromAccount;
}

EDIT: Chà, tôi đoán điều đó sẽ hữu ích nếu bạn đang sử dụng @Tablechú thích trên lớp của mình. Heh. :)


2
vâng tôi không nghĩ đây là nó, tất cả đều giống nhau, tôi đã thêm @JoinColumn (name = "fromAccount_id", ReferenceencedColumnName = "id") và nó không hoạt động :).
Paul Sanwald

Vâng, tôi thường không sử dụng tệp xml ánh xạ để ánh xạ các thực thể vào các bảng, vì vậy tôi thường cho rằng nó dựa trên chú thích. Nhưng nếu tôi phải đoán, bạn đang sử dụng một hibernate.xml để ánh xạ các thực thể vào các bảng, phải không?
NemesisX00

không, tôi đang sử dụng dữ liệu mùa xuân JPA, vì vậy tất cả đều dựa trên chú thích. Tôi có một chú thích "mappedBy" ở phía bên kia: @OneToMany (cascade = {CascadeType.ALL}, fetch = FetchType.EAGER, mappedBy = "fromAccount")
Paul Sanwald

4

Ngay cả khi các chú thích của bạn được khai báo chính xác để quản lý chính xác mối quan hệ một-nhiều, bạn vẫn có thể gặp phải ngoại lệ chính xác này. Khi thêm một đối tượng con mới Transaction, vào mô hình dữ liệu đính kèm, bạn sẽ cần quản lý giá trị khóa chính - trừ khi bạn không được yêu cầu . Nếu bạn cung cấp một giá trị khóa chính cho một thực thể con được khai báo như sau trước khi gọi persist(T), bạn sẽ gặp ngoại lệ này.

@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
....

Trong trường hợp này, các chú thích đang tuyên bố rằng cơ sở dữ liệu sẽ quản lý việc tạo các giá trị khóa chính của thực thể khi chèn. Việc tự cung cấp một cái (chẳng hạn như thông qua bộ cài đặt của Id) gây ra ngoại lệ này.

Ngoài ra, nhưng hiệu quả giống nhau, tuyên bố chú thích này dẫn đến cùng một ngoại lệ:

@Entity
public class Transaction {
    @Id
    @org.hibernate.annotations.GenericGenerator(name="system-uuid", strategy="uuid")
    @GeneratedValue(generator="system-uuid")
    private Long id;
....

Vì vậy, đừng đặt idgiá trị trong mã ứng dụng của bạn khi nó đã được quản lý.


4

Nếu không có gì giúp ích và bạn vẫn nhận được ngoại lệ này, hãy xem lại các equals()phương pháp của bạn - và không bao gồm bộ sưu tập con trong đó. Đặc biệt nếu bạn có cấu trúc sâu của các bộ sưu tập nhúng (ví dụ A chứa Bs, B chứa Cs, v.v.).

Trong ví dụ về Account -> Transactions:

  public class Account {

    private Long id;
    private String accountName;
    private Set<Transaction> transactions;

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (!(obj instanceof Account))
        return false;
      Account other = (Account) obj;
      return Objects.equals(this.id, other.id)
          && Objects.equals(this.accountName, other.accountName)
          && Objects.equals(this.transactions, other.transactions); // <--- REMOVE THIS!
    }
  }

Trong ví dụ trên loại bỏ các giao dịch từ equals()séc. Điều này là do ngủ đông sẽ ngụ ý rằng bạn không cố cập nhật đối tượng cũ, nhưng bạn truyền một đối tượng mới để duy trì, bất cứ khi nào bạn thay đổi thành phần trên bộ sưu tập con.
Tất nhiên giải pháp này sẽ không phù hợp với tất cả các ứng dụng và bạn nên cẩn thận thiết kế những gì bạn muốn đưa vào equalshashCodephương thức.


1

Có thể đó là lỗi của OpenJPA, Khi khôi phục, nó đặt lại trường @Version, nhưng pcVersionInit vẫn đúng. Tôi có một AbstraceEntity đã khai báo trường @Version. Tôi có thể giải quyết nó bằng cách đặt lại trường pcVersionInit. Nhưng nó không phải là một ý tưởng tốt. Tôi nghĩ rằng nó không hoạt động khi có tầng thực thể liên tục.

    private static Field PC_VERSION_INIT = null;
    static {
        try {
            PC_VERSION_INIT = AbstractEntity.class.getDeclaredField("pcVersionInit");
            PC_VERSION_INIT.setAccessible(true);
        } catch (NoSuchFieldException | SecurityException e) {
        }
    }

    public T call(final EntityManager em) {
                if (PC_VERSION_INIT != null && isDetached(entity)) {
                    try {
                        PC_VERSION_INIT.set(entity, false);
                    } catch (IllegalArgumentException | IllegalAccessException e) {
                    }
                }
                em.persist(entity);
                return entity;
            }

            /**
             * @param entity
             * @param detached
             * @return
             */
            private boolean isDetached(final Object entity) {
                if (entity instanceof PersistenceCapable) {
                    PersistenceCapable pc = (PersistenceCapable) entity;
                    if (pc.pcIsDetached() == Boolean.TRUE) {
                        return true;
                    }
                }
                return false;
            }

1

Bạn cần đặt Giao dịch cho mọi Tài khoản.

foreach(Account account : accounts){
    account.setTransaction(transactionObj);
}

Hoặc nó là đủ (nếu thích hợp) để đặt id thành null ở nhiều phía.

// list of existing accounts
List<Account> accounts = new ArrayList<>(transactionObj.getAccounts());

foreach(Account account : accounts){
    account.setId(null);
}

transactionObj.setAccounts(accounts);

// just persist transactionObj using EntityManager merge() method.

1
cascadeType.MERGE,fetch= FetchType.LAZY

1
Xin chào James và chào mừng, bạn nên thử và tránh mã chỉ trả lời. Vui lòng cho biết cách giải quyết vấn đề được nêu trong câu hỏi (và khi nào nó có thể áp dụng được hay không, mức API, v.v.).
Maarten Bodewes

Người đánh giá VLQ: xem meta.stackoverflow.com/questions/260411/ . câu trả lời chỉ có mã không đáng xóa, hành động thích hợp là chọn "Có vẻ ổn".
Nathan Hughes

1

Câu trả lời dựa trên dữ liệu mùa xuân của tôi dựa trên JPA: Tôi chỉ cần thêm một @Transactionalchú thích vào phương thức bên ngoài của mình.

Tại sao nó hoạt động

Thực thể con ngay lập tức bị tách ra vì không có bối cảnh Phiên Hibernate hoạt động. Cung cấp giao dịch Mùa xuân (Dữ liệu JPA) đảm bảo Phiên Hibernate có mặt.

Tài liệu tham khảo:

https://vladmihalcea.com/a-beginners-guide-to-jpa-hibernate-entity-state-transitions/


0

Trong trường hợp của tôi, tôi đã cam kết giao dịch khi phương thức kiên trì được sử dụng. Khi thay đổi liên tục để lưu phương thức, nó đã được giải quyết.


0

Nếu các giải pháp trên không hoạt động chỉ cần bình luận một lần, các phương thức getter và setter của lớp thực thể và không đặt giá trị của id. (Khóa chính) thì điều này sẽ hoạt động.


0

@OneToMany (mappedBy = "xxxx", cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE}) đã làm việc cho tôi.


0

Giải quyết bằng cách lưu đối tượng phụ thuộc trước khi tiếp theo.

Điều này đã xảy ra với tôi vì tôi không đặt Id (không được tạo tự động). và cố gắng lưu với mối quan hệ @ManytoOne

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.