我正在使用MySQL数据库构建ADF Fusion Web应用程序(12c)。
要在插入时获得自动递增的PK值,我使用分配“AutoIncrementProperty”属性集(属性“AI”,值“true”)的方法来自动增加PK字段。所有实体都从覆盖doDML()的类扩展而来。这一切都很好,但仅供参考,这是我用于此的代码:
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表的实体。我们称他们为“人”和“文件夹”。 Folder实体/表是递归的,因为它看起来像这样:
文件夹
而且,Person实体/表看起来像这样:
人
因此,一个人有一个分配给他/她的根文件夹。 (该文件夹可能具有子文件夹的层次结构。)
我遇到的问题是,当我创建一个Person实体时,我想创建一个新的Folder实体,获取其PK值,然后将该Integer分配给新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的Create方法中调用它,如下所示:
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()调用,但它们都有问题。并且,我认为必须有一些我更容易忽视的事情。
如果我可以在实体本身以某种方式以声明方式执行此操作,而不使用代码,那将是理想的。但是,我认为编码解决方案非常简单。我只是没有看到它。
任何话?
答案 0 :(得分:1)
好的,对@MihaiC有很多功劳,我已经解决了这个问题。
正如MihaiC所说,最好的解决方案是不使用MySQL的自动增量。但是,ADF不支持SQL92风格的序列(由于其他原因我必须使用它)。我研究了几种模拟序列的方法,并采用了一种效果很好的混合方法。
借用本文:http://www.sqlines.com/oracle-to-mysql/sequence,我创建了一个简单的表来存储序列来模拟这种行为。
表:_sequences
我使用以下值创建了一条记录:
然后,只需查找给定命名序列的“next”值,并为下一次调用递增该值。我为此目的创建了一个Sequence实体对象,基于_sequences表。
这是我在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()中,我在提交数据库之前做了四件事:
然后,如果以上所有成功,则在调用super.doDML()时都会将所有内容提交到数据库。
也就是说,我相信这个逻辑是合理的。
我有点担心,在多用户环境中,两个用户可能会获得相同的next-in-sequence值,导致第二个人尝试提交时出现问题。如果ADF允许以下事件顺序,则可以这样做:
我不知道当另一个用户处于该过程的中间时,ADF是否允许一个用户启动doDML()。对此应用程序并不关心,但如果不进行进一步的研究,我不会相信大量的多用户应用程序。
答案 1 :(得分:1)
即使您已经找到了解决方案,我也想表明自己对此的看法:根本不使用实体中的代码,只需使用ViewObjects
中的代码和方法。
我创建了3个表:Person
,Folder
和Sequences
,正如您所描述的那样。然后,我为所有人创建了EntityObjects
和ViewObjects
。
PersonView
和FolderView
被调整为不返回任何行,即仅用于插入。
SequenceView
被调整为始终返回最多一行。
PersonView
和FolderView
来自简单查询:select * from table
。
SequenceView
,但我在其上定义了一个自定义ViewCriteria
,仅检索具有匹配名称的行。我暂时无法发布图片给您,但VC
只会:where name=:p_seq_name
。
下一步是定义检索序列的方法。您可以通过在Sequence 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;
}
接下来,我们在服务中公开ViewObjects
,AppModule
(我知道,我没有重命名它 - 所以我们可以在关联的实现类中使用它们的特定方法({{1} })。
我们必须公开序列视图两次,这样我们才能处理两个不同的行,而不必设置和重置同一行(如果我们使用的话)。到目前为止,我们的AppModuleImpl
类应该包含以下AppModuleImpl
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
}
Finallu,我在服务的客户端界面(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());
}
)中公开了createPerson()
方法,并将其作为页面中的按钮拖动。我已经从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并获得约束验证。
如果按下按钮之间存在延迟,则可能会出现一个问题,但对于此类小对象和查询序列表不应该发生这种情况。当然,如果您可以对此进行广泛测试,请继续,我也对结果感兴趣,但我相信您将很难为此特定测试找到正确的方案。
在任何情况下,AppModule
都会让用户知道是否有任何问题,
他可以重新拯救新人。
编辑
我已经同时通过多次用户点击对其进行了测试,实际上有时会抛出异常,尽管很少。该代码确保没有重复项可以进入数据库。
该错误将告诉用户再试一次。但同样,这种情况很少发生,一次每100次点击一次,来自2个垃圾按钮的用户。
当然我们需要更多用户,但应该没问题。无论如何,错误的数据无法访问数据库,并且用户不会丢失其输入数据。他只需要再试一次。
错误也会出现在您的服务器日志中:
ADF:添加以下JSF错误消息:已保存id = 61的人或ID = 158的文件夹。请再试一次。
所以你可以监控他们。
我会删除旧的答案,因为它不是最佳的。 :)有时间思考,我认为这是最好的解决方案。此外,我很抱歉答案很大,可能很难遵循。如果您有任何问题,请告诉我。