2014-12-04 89 views
1

我正在使用MySQL數據庫構建ADF Fusion Web應用程序(12c)。Oracle ADF - 在父實體創建時創建子實體

爲了在插入時獲得自動遞增的PK值,我使用了爲自動遞增PK字段分配「AutoIncrementProperty」屬性集(屬性「AI」,值「true」)的方法。所有實體都從覆蓋doDML()的類擴展而來。這一切都很好,但FYI,這是我使用的代碼:

protected void doDML(int i, TransactionEvent transactionEvent) { 
    super.doDML(i, transactionEvent); 

    if (i == DML_INSERT) { 
     populateAutoincrementAtt(); 
    } 
} 

/* 
* Determines if the Entity PK is marked as an autoincrement col 
* and executes a MySQL function to retrieve the last insert id 
*/ 
private void populateAutoincrementAtt() { 
    EntityDefImpl entdef = this.getEntityDef(); 
    AttributeDef pk = null; 
    //look for primary key with Autoincrement property set 
    for (AttributeDef att : entdef.getAttributeDefs()) { 
     if (att.isPrimaryKey() && (att.getProperty("AI") != null)) { 
      pk = att; 
      break; 
     } 
    } 
    if (pk != null) { 
     try (PreparedStatement stmt = 
      this.getDBTransaction() 
       .createPreparedStatement("SELECT last_insert_id()", 1)) { 
      stmt.execute(); 
      try (ResultSet rs = stmt.getResultSet()) { 
       if (rs.next()) { 
        setAttribute(pk.getName(), rs.getInt(1)); 
       } 
      } 
     } catch (SQLException e) { 
      e.printStackTrace(); 
     } 
    } 
} 

好的,這不是問題。這是應該的。

我有兩個實體與相應的MySQL表。我們稱他們爲「人」和「文件夾」。文件夾實體/表是遞歸之多,因而它看起來像這樣:

文件夾

  • ID(自動遞增的PK整數)
  • parentId的(外鍵父文件夾對象)
  • folderName
  • ...

而且,Person實體/表看起來是這樣的:

  • ID(自動遞增的PK整數)
  • 的userName
  • rootFolderId(Folder tabl的外鍵E)
  • ...

所以,一個人有分配給他/她的根文件夾。 (該文件夾可能會有一個子文件夾層次結構)

我遇到的問題是,當我創建一個Person實體時,我想創建一個新的Folder實體,獲取它的PK值,然後指定整數到新的Person的rootFolderId屬性。

應該很簡單,不是嗎?

在PersonImpl的類(擴展EntityImpl),我有以下方法:

private Integer createRootFolder() { 
    Integer newIdAssigned; 

    String entityName = "com.my.model.entity.Folder"; 
    EntityDefImpl folderDef = EntityDefImpl.findDefObject(entityName); 
    EntityImpl newFolder = (EntityImpl) folderDef.createInstance2(getDBTransaction(), null); 
    newFolder.setAttribute("folderName", "ROOT"); 
    try { 
     getDBTransaction().commit(); 
     newIdAssigned = (Integer) newCollection.getAttribute("Id"); 
    } catch (JboException ex) { 
     getDBTransaction().rollback(); 
     newIdAssigned = null; 
    } 

    return newIdAssigned; 
} 

所以,這種方法工作得很好。它確實插入一個Folder對象並返回它的PK值。問題出現在何時/何地/如何稱此方法。

我可以從PersonImpl的的創建方法調用,就像這樣:

protected void create(AttributeList attributeList) { 
    Integer rootFolderId = null; 

    rootFolderId = createRootFolder(); 

    super.create(attributeList); 
    this.setRootFolderId(rootFolderID); 
} 

但是,當然,這會在根文件夾對象之前所屬Person對象被提交給數據庫。所以,如果Person永遠不會被提交,我們就會有一個我們無緣無故創建的孤立的Folder對象。

我在PersonImpl的類打過電話從doDML()這樣的:

protected void doDML(int operation, TransactionEvent e) { 
    Integer rootFolderId; 

    super.doDML(operation, e); 

    if (operation == DML_INSERT) { 
     rootFolderId = createRootFolder(); 
     if (rootFolderId != null) { 
      this.setRootCollectionId(rootCollID); 
      getDBTransaction().commit(); 
     } 
    } 
} 

但是......當然,當createRootFolder()調用commit(),這將導致doDML()火再次,我們得到一個遞歸問題。我玩過像設置標誌這樣的kludges來防止遞歸doDML()調用,但它們都有問題。而且,我認爲我一定會忽略一些簡單的東西。

這將是理想的,如果我可以在實體本身中以聲明方式執行此操作,而無需代碼。但是,我認爲編碼解決方案會非常簡單。我只是沒有看到它。

任何用語?

+1

你爲什麼不研究viewObjects?在兩者之間創建一個viewLink。在你的案例中,人物大師1-1與person.folderid = folder.id中鏈接的子文件夾的關係。這樣,當您在人的視圖對象中創建一個新人時,您還可以創建一個文件夾行,該行將自動從主服務器獲取FK ID的值。此外,您可以改爲從序列中設置主鍵。尋找SequenceImpl的例子,比你寫的要容易得多。 – MihaiC 2014-12-05 14:16:00

+0

感謝您的建議! – Foswick 2014-12-05 19:54:59

+0

我有你所描述的1-1視圖鏈接。但是,這並不會自動生成該孩子。我用來生成孩子的任何方法都無法自動填充FK。這似乎是由於孩子需要承諾才能找回其PK。第一眼看到你對序列的建議可以通過讓我主動設置PK來解決這個問題。我會仔細閱讀它,看看我能否整理出來。再次感謝。 – Foswick 2014-12-05 20:02:49

回答

1

好的,我給了@MihaiC很多功勞,我已經解決了這個問題。

由於MihaiC建議,最好的解決方案是不使用MySQL的自動增量。但是,ADF不支持SQL92風格的序列(我必須使用其他原因)。我研究了幾種模擬序列的方法,並採用了很好的混合方法。

借用這篇文章:http://www.sqlines.com/oracle-to-mysql/sequence,我創建了一個簡單的表來存儲序列來模擬這種行爲。

表:_sequences

  • 名稱:PK VARCHAR(存儲序列名稱)
  • 下一個:INT(存儲命名序列中的下一個可用值)
  • INC:INT(存儲序列的增量值)

我創建具有以下值的單個記錄:

  • name =「Folder_Seq」;
  • 下一個= 100(因爲我已經與低ID號數測試記錄)
  • INC = 1(不知道爲什麼你會使用別的,但是我可以)

然後,只需要爲給定的已命名序列查找「next」的值,併爲下一次調用遞增該值。我基於_sequences表爲此目的創建了一個Sequence實體對象。

這裏是我我PersonImpl的類中使用的代碼:

protected void doDML(int operation, TransactionEvent e) { 
    if (operation == DML_INSERT) { 
     // We do this only when we are ready to commit. 
     Integer nextSeqVal = getNextFolderSequenceNumber(); 
     createRootFolder(nextSeqVal); 
     setRootFolderId(nextSeqVal); 
     // Need to add logic to bail out if something went wrong in the above steps. 
    } 
    // Nothing actually gets committed until we reach this line 
    super.doDML(operation, e); 
} 

private Integer getNextFolderSequenceNumber() { 
    SequenceImpl seq = getSequenceByName("Folder_Seq"); 
    Integer nextVal = seq.getNext(); 
    if (nextVal != null) { 
     seq.setNext(nextVal + seq.getInc()); 
    } else { 
     // handle this as an error 
    } 
    return nextVal; 
} 

// Looks up the Sequence entity by name 
private SequenceImpl getSequenceByName(String seqName) { 
    EntityDefImpl seqDef = SequenceImpl.getDefinitionObject(); 
    Key seqKey = SequenceImpl.createPrimaryKey(seqName); 
    return (SequenceImpl) seqDef.findByPrimaryKey(getDBTransaction(), seqKey); 
} 

// Creates a Folder object with the passed Integer as its PK 
private void createRootFolder(Integer pkID) { 
    String entityName = "com.my.model.entity.Folder"; 
    EntityDefImpl folderDef = EntityDefImpl.findDefObject(entityName);  
    // I chose to fully-qualify EntityImpl since I have overriden oracle.jbo.server.EntityImpl 
    oracle.jbo.server.EntityImpl newFolder = folderDef.createInstance2(getDBTransaction(), null); 
    newFolder.setAttribute("FolderName", "ROOT"); // Because I want all root folders to be named "ROOT" 
    newFolder.setAttribute("Id", pkID); 
} 

所以(主Person實體的,在doDML),我做四件事提交到數據庫之前:

  • 獲取Folder_Seq序列的下一個可用序列號。
  • 遞增相應序列實體中的「下一個」值。
  • 創建一個新的文件夾實體並使用該下一個序列值設置其PK。
  • 將此值設置爲外鍵「RootFolderId」屬性值。

然後,如果以上全部成功,則在調用super.doDML()時全部提交到數據庫。

也就是說,我相信這個邏輯是合理的。

我有點擔心,在多用戶環境中,兩個用戶可能獲得相同的下一個順序值,導致第二個人嘗試提交時出現問題。如果ADF允許的事件順序如下這將是可能的:

  1. 用戶A得到的...說的「下一個」值...... 1001
  2. 用戶B獲取相同的值,因爲用戶A沒有承諾他遞增的價值。
  3. 用戶A提交所有更改。
  4. 用戶B在嘗試提交時發現錯誤,因爲PK 1001現在存在於數據庫中。

我不知道ADF是否允許一個用戶在另一個用戶處於該過程的中間時啓動doDML()。不關心這個應用程序,但我不會相信這個沒有做進一步研究的大批量多用戶應用程序。

+1

恭喜編輯找到一個解決方案的假 – MihaiC 2014-12-08 07:12:25

+1

一個好的做法是在try {}中也包含seq table viewImpl行中序列值的遞增,這樣只有在可以進行保存時纔會遞增。然後,如果數據無法保存,它將回滾行上的所有內容,包括新的序列。如果一切正常並且可以保存數據,則viewObj行上的更改將持續到實體 – MihaiC 2014-12-08 07:27:32

1

即使你已經找到了解決辦法,我想表明我自己拿到這個:在實體不使用的代碼在所有的,只是代碼與方法ViewObjects

我創建了3個表格:PersonFolderSequences正如您所描述的那樣。然後,我爲他們創建了EntityObjectsViewObjects

PersonViewFolderView被調諧到不返回任何行,即,僅對於插入件使用。

SequenceView調諧總是返回最大一行。

PersonViewFolderView從簡單查詢填充:select * from table

SequenceView從上面的簡單查詢填充,但我已經定義了一個自定義ViewCriteria,以檢索只有具有匹配名稱的行。我目前無法發佈圖片以向您展示,但VC只有:where name=:p_seq_name

下一步是定義檢索序列的方法。通過編寫實現類的序列的ViewObject內的自定義方法(SequencesViewImpl)做到這一點:

//returns a sequence view object row, always one 
public SequencesViewRowImpl getNextSequenceRow(String seqName) { 
    this.setp_seq_name(seqName); 
    this.setApplyViewCriteriaName("ByNameViewCriteria"); 
    this.executeQuery(); 
    final SequencesViewRowImpl seqRow = (SequencesViewRowImpl)this.first(); 
    return seqRow; 
} 

接下來,我們在服務中暴露我們的ViewObjectsAppModule(我知道,我沒有重命名它 - 這樣我們可以用自己的具體方法相關的實現類(AppModuleImpl)內。

我們必須公開序列視圖兩次,使我們面對的兩個不同行,不必設置和重置相同行(如果我們使用的話)到目前爲止,我們的AppModuleImpl類應該包含fo llowing getters我們views

/** 
* Container's getter for FolderView1. 
* @return FolderView1 
*/ 
public FolderViewImpl getFolderView1() { 
    return (FolderViewImpl)findViewObject("FolderView1"); 
} 

/** 
* Container's getter for PersonView1. 
* @return PersonView1 
*/ 
public PersonViewImpl getPersonView1() { 
    return (PersonViewImpl)findViewObject("PersonView1"); 
} 

/** 
* Container's getter for SequencesView1. 
* @return SequencesView1 
*/ 
public SequencesViewImpl getSequencesView1() { 
    return (SequencesViewImpl)findViewObject("SequencesView1"); 
} 

/** 
* Container's getter for SequencesView2. 
* @return SequencesView2 
*/ 
public SequencesViewImpl getSequencesView2() { 
    return (SequencesViewImpl)findViewObject("SequencesView2"); 
} 

現在,讓我們創建一個新的行Person,只是使用viewObjects方法。 (在AppModuleImpl類寫入代碼)

public void createPerson() { 
    final SequencesViewRowImpl personSeq = this.getSequencesView1().getNextSequenceRow("Person_Seq"); //a person sequence row, note that we are getting View1, we will use View2 for Folders. 
    final PersonViewRowImpl personRow = (PersonViewRowImpl)this.getPersonView1().createRow(); //create a new row in memory 

    //set attributes for the new person row 
    personRow.setId(personSeq.getNext()); 
    personRow.setUsername("TestUser"); 
    final Integer rootFolderId = this.createRootFolder(); //create a new folder row and retrieve the folderId 
    personRow.setRootFolderId(rootFolderId); 
    this.getPersonView1().insertRow(personRow); //actually insert the row, caches the result onto the entity, when commited it will be saved. 

    this.incrementSequences(); //increment the sequences 

    try { 
     this.getDBTransaction().commit(); //if all is ok save all the rows to the database 
    } catch (Exception e) { 
     this.getDBTransaction().rollback(); //if anything went wrong, rollback everything including the incrementing of the seq 
     //let the user know the data couldn't be saved, so he can try again; this should happen only when the primary key constraints have failed validation, since you should ensure data type and length validation eigther from page or other sources. 
     throw new JboException("Person with id = "+personSeq.getNext()+" or Folder with id = "+rootFolderId +" was already saved. Please try again."); 
    } 
} 

createRootFolder()的方法:

private Integer createRootFolder() { 
    final SequencesViewRowImpl folderSeq = this.getSequencesView2().getNextSequenceRow("Folder_Seq"); //View2 on the same Object for Folders 
    final FolderViewRowImpl folderRow = (FolderViewRowImpl)this.getFolderView1().createRow(); //create and set the attributes for the new folder 
    folderRow.setId(folderSeq.getNext()); 
    folderRow.setFolderName("ROOT"); 
    this.getFolderView1().insertRow(folderRow); 
    return folderSeq.getNext(); //return the new folder id 
} 

最後增量序列方法:

private void incrementSequences() { 
    final SequencesViewRowImpl personSeq = this.getSequencesView1().getNextSequenceRow("Person_Seq"); 
    final SequencesViewRowImpl folderSeq = this.getSequencesView2().getNextSequenceRow("Folder_Seq"); 

    personSeq.setNext(personSeq.getNext() + personSeq.getInc()); 
    folderSeq.setNext(folderSeq.getNext() + folderSeq.getInc()); 
} 

Finallu,我已經露出在所述createPerson()方法服務的客戶端界面(AppModule)並將其作爲頁面中的按鈕拖動。我已經從個人和文件夾的ID 1開始,然後第三次將其更改爲100。比賽結果如下:

人表:

 
ID | USERNAME | ROOT_FOLDER_ID 
1 | TestUser |1 
2 | TestUser |2 
3 | TestUser |100 

文件夾表:

 
ID | PARENT_ID | FOLDER_NAME 
1 |   |ROOT 
2 |   |ROOT 
100|   |ROOT 

序列表:

 
NAME  | NEXT | INC 
Folder_Seq| 101 |1 
Person_Seq| 4 |1 

這樣一來,你有什麼事情的完全控制權與你的行,對象和數據。如果從行中到達實體或數據庫時有任何驗證失敗,則所有事情都會回滾,因此如果沒有保存任何內容,則不會增加序列。

此外,這應該確保併發性不是問題,因爲所有內容都是按順序調用並保存在最後。因此,除非您有數百個用戶在同一納秒內擊中該按鈕,否則我認爲不可能獲取已保存的ID並獲得約束驗證。

如果在按下按鈕之間存在滯後,則可能出現一個問題,但對於這樣的小對象和查詢順序表,這不應發生。當然,如果你可以對此進行廣泛的測試,請繼續,我也對結果感興趣,但是我相信你會很難爲這個特定的測試獲得正確的場景。

在任何情況下,throw new JboException都會讓用戶知道是否有問題, 並且他可以重試保存新人。


編輯

我曾在同一時間與多個用戶點擊測試了一下,確實異常有時拋出,雖然是很少。該代碼確保沒有重複項能夠進入數據庫。

該錯誤會告訴用戶再次嘗試。但是,這種情況再次發生的頻率非常低,每兩次用戶發出100次點擊就會發生一次垃圾郵件。

當然我們需要更多的用戶,但應該沒問題。無論如何,錯誤的數據並沒有到達數據庫,用戶也不會丟失他的輸入數據。他只需要再試一次。

此錯誤還出現在你的服務器日誌中這樣的:

ADF:添加下面的JSF錯誤消息:人與ID = 61或ID = 158已經保存的文件夾中。請再試一次。

所以你可以監視它們。


我會刪除舊的答案,因爲它不是最優的。 :)有時間思考,我認爲這是最好的解決方案。另外,我很抱歉有很大的答案,可能很難遵循。如果您有任何問題,請告訴我。