Oracle ADF - 在父实体创建上创建子实体

时间:2014-12-04 21:51:48

标签: mysql oracle-adf

我正在使用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实体/表是递归的,因为它看起来像这样:

文件夹

  • id (自动递增PK整数)
  • parentId (父文件夹对象的外键)
  • folderName
  • ...

而且,Person实体/表看起来像这样:

  • id (自动递增PK整数)
  • 的userName
  • rootFolderId (Folder表的外键)
  • ...

因此,一个人有一个分配给他/她的根文件夹。 (该文件夹可能具有子文件夹的层次结构。)

我遇到的问题是,当我创建一个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()调用,但它们都有问题。并且,我认为必须有一些我更容易忽视的事情。

如果我可以在实体本身以某种方式以声明方式执行此操作,而不使用代码,那将是理想的。但是,我认为编码解决方案非常简单。我只是没有看到它。

任何话?

2 个答案:

答案 0 :(得分:1)

好的,对@MihaiC有很多功劳,我已经解决了这个问题。

正如MihaiC所说,最好的解决方案是不使用MySQL的自动增量。但是,ADF不支持SQL92风格的序列(由于其他原因我必须使用它)。我研究了几种模拟序列的方法,并采用了一种效果很好的混合方法。

借用本文:http://www.sqlines.com/oracle-to-mysql/sequence,我创建了一个简单的表来存储序列来模拟这种行为。

表:_sequences

  • name:PK VARCHAR(存储序列名称)
  • next:INT(存储指定序列中的下一个可用值)
  • inc:INT(存储序列的增量值)

我使用以下值创建了一条记录:

  • name =“Folder_Seq”;
  • next = 100(因为我已经有一些ID值较低的测试记录)
  • inc = 1(不知道为什么你会使用其他任何东西,但我可以)

然后,只需查找给定命名序列的“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()中,我在提交数据库之前做了四件事:

  • 获取Folder_Seq序列的下一个可用序列号。
  • 增加相应Sequence实体中的“next”值。
  • 创建一个新的Folder实体,并使用该顺序值设置其PK。
  • 将此值设置为外键“RootFolderId”属性值。

然后,如果以上所有成功,则在调用super.doDML()时都会将所有内容提交到数据库。

也就是说,我相信这个逻辑是合理的。

我有点担心,在多用户环境中,两个用户可能会获得相同的next-in-sequence值,导致第二个人尝试提交时出现问题。如果ADF允许以下事件顺序,则可以这样做:

  1. 用户A获取......的下一个值...说... 1001。
  2. 用户B获得相同的值,因为用户A未提交其增加的值。
  3. 用户A提交所有更改。
  4. 用户B在尝试提交时看到错误,因为PK 1001现在存在于数据库中。
  5. 我不知道当另一个用户处于该过程的中间时,ADF是否允许一个用户启动doDML()。对此应用程序并不关心,但如果不进行进一步的研究,我不会相信大量的多用户应用程序。

答案 1 :(得分:1)

即使您已经找到了解决方案,我也想表明自己对此的看法:根本不使用实体中的代码,只需使用ViewObjects中的代码和方法。

我创建了3个表:PersonFolderSequences,正如您所描述的那样。然后,我为所有人创建了EntityObjectsViewObjects

PersonViewFolderView被调整为不返回任何行,即仅用于插入。

SequenceView被调整为始终返回最多一行。

PersonViewFolderView来自简单查询: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;
}

接下来,我们在服务中公开ViewObjectsAppModule(我知道,我没有重命名它 - 所以我们可以在关联的实现类中使用它们的特定方法({{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的文件夹。请再试一次。

所以你可以监控他们。


我会删除旧的答案,因为它不是最佳的。 :)有时间思考,我认为这是最好的解决方案。此外,我很抱歉答案很大,可能很难遵循。如果您有任何问题,请告诉我。