EObjectAlreadyPersistent - Cannot turn persistent object of class Txxx with id x. The object is already in persistent context.

Hello!

I have the error, mentioned in the Topic title and I'm stuck. I checked other articles, but haven't found the solution.

Using Win32 app that saves a document through XData service, the error happens while saving.

The objects

Note: The classes are massively simplified. The classes are done winth Data modeler (latest version)

A TDocument can contain many TDocItems, each item has a TProduct.

  [Entity]
  [Table('doc_headers')]
  [Id('FId', TIdGenerator.IdentityOrSequence)]
  TDocument = class
  private
    [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
    FId: Integer;
    FBusinessYear: string;
    FDocId: Integer;
        
    [Association([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
    [JoinColumn('DOC_TYPE', [TColumnProp.Required], 'ID')]
    FDocType: Proxy<TTypeDocument>;
        
    [Association([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
    [JoinColumn('PARTNER_ID', [TColumnProp.Required], 'ID')]
    FPartner: Proxy<TPartner>;
       
    [ManyValuedAssociation([TAssociationProp.Lazy, TAssociationProp.Required], [TCascadeType.SaveUpdate, TCascadeType.Merge, TCascadeType.Remove], 'FHeader')]
    FItems: Proxy<TList<TDocItem>>;
  public
    constructor Create;
    destructor Destroy; override;
    property Id: Integer read FId write FId;
    property BusinessYear: string read FBusinessYear write FBusinessYear;
    property DocId: Integer read FDocId write FDocId;
    property DocType: TTypeDocument read GetDocType write SetDocType;
    property Partner: TPartner read GetPartner write SetPartner;
    property Items: TList<TDocItem> read GetItems;
  end;

  [Entity]
  [Table('doc_items')]
  [Id('FIdx', TIdGenerator.None)]
  [Id('FHeader', TIdGenerator.None)]
  TDocItem = class
  private
    [Column('IDX', [TColumnProp.Required])]
    FIdx: Integer;
    
    [Column('QTY', [TColumnProp.Required], 23)]
    FQty: Double;
    
    [Association([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
    [JoinColumn('HEADER_ID', [TColumnProp.Required], 'ID')]
    FHeader: Proxy<TDocument>;
    
    [Association([TAssociationProp.Lazy, TAssociationProp.Required], CascadeTypeAll - [TCascadeType.Remove])]
    [JoinColumn('PRODUCT_ID', [TColumnProp.Required], 'ID')]
    FProduct: Proxy<TProduct>;
  public
    property Idx: Integer read FIdx write FIdx;
    property Qty: Double read FQty write FQty;
    property Header: TDocument read GetHeader write SetHeader;
    property Product: TProduct read GetProduct write SetProduct;
  end;

  [Entity]
  [Table('products')]
  [Id('FId', TIdGenerator.None)]
  TProduct = class
  private
    [Column('ID', [TColumnProp.Required])]
    FId: Integer;
        
    [Column('NAME', [], 255)]
    FName: Nullable<string>;
    
  public
    constructor Create;
    destructor Destroy; override;
    property Id: Integer read FId write FId;
    property Name: Nullable<string> read FName write FName;
  end;

Client code

procedure TForm1.SaveNewDocument(Sender: TObject);
var newDoc: TDocument;
begin
  var EasyService := Client.Service<IEasyService>;

  newDoc := TDocument.Create;
  newDoc.DocType := EasyService.GetDocType(1); 
  newDoc.Partner := EasyService.GetPartner(3);
  newDoc.DateExpire := Now;
  newDoc.DateEntry  := Now;

  var itm := TDocItem.Create;
  itm.Product := EasyService.GetProduct(10);  // Add existing product
  itm.Qty := 2;
  itm.PriceRetail := itm.Product.Prices[0].PriceRetail;
  newDoc.Items.Add(itm);
  EasyService.SaveDocument(newDoc);
end;

The service

Service method code

Basically just prepares and saves the document

function TEasyService.SaveDocument(Doc: TDocument): TRestResult;
var
  par: TDocParam;
  itm: TDocItem;
  Statement: IDBStatement;
begin
  try
    Result := TRestResult.Create;

    var mngr := TXDataOperationContext.Current.CreateManager;

    {$REGION 'Preparation'}
    Doc.DocTime := Now;

    // Get latest DocId+1...
    if (Doc.DocId=0) and Assigned(Doc.DocType) then begin
      var lastDoc := mngr.Find<TDocument>
                .Where(Dic.Document.DocType.Id = Doc.DocType.Id)
                .Where(Dic.Document.BusinessYear = Doc.BusinessYear)
                .OrderBy(Dic.Document.DocId, false)
                .Take(1)
                .UniqueResult;

      if Assigned(lastDoc) then
        Doc.DocId := lastDoc.DocId+1
      else
        Doc.DocId := 1;
    end;
    {$ENDREGION}

    {$REGION 'Save document'}
    // Save all items
    for itm in Doc.Items do begin
      itm.Header := Doc;
    end;

    mngr.SaveOrUpdate(Doc);
    mngr.Flush(Doc);

    Result.Id := Doc.Id.ToString;
  except
    on E: Exception do begin
      Result.Add(E.Message);
      Result.Id := '';
    end;
  end;
end;

The problem

The error I get is "Project raised exception class EObjectAlreadyPersistent with message 'Cannot turn persistent object of class TProduct with id 10. The object is already in persistent context.'."

Obviously the product already existed, I just wanted to link it to the TDocItem.

I checked that XDataServer.PostMode is Replicate.

I'm stuck :)

Kind regards,
m@rko

XDataServer.PostMode only affects automatic CRUD endpoints.

Since you are using a service operation, you should also do that at your side, because the object tree you are sending has repeated objects (multiple objects of the same class and same id).

Use Replicate or Merge instead of SaveOrUpdate. Don't forget that the former creates a copy of the Doc instance inside the manager. For example (untested, of course):

   NewDoc := mngr.Replicate(Doc);
   mngr.Flush(NewDoc);