工作流和并发冲突
普通用户可以忽略本节内容。
提示:
本节内容在编写的时候,Foxtable还没有内置OpenQQ,无法实现消息的主动推送和接收。所以本节的内容其实是有些过时的。
从Foxtable 2016开始,我们可以OpenQQ来实现工作流,并解决并发冲突的问题,且更加方便可靠,请参考:OpenQQ、工作流与并发冲突
尽管过时,但是本节内容依然有参考价值,有兴趣的用户可以学习一下。
本节的例子可以参考CaseStudy目录下的文件:工作流与并发冲突.Table
在浏览本节内容的时候,建议将帮助窗口最大化。
之前介绍的工作流,都是一人负责个流程,有时工作流会更复杂一些,例如一项工作分为多个流程,每一个流程会有多人负责,此时就需要考虑“抢先”的问题。例如A、B、C三人都有权限处理某一流程,但同一时候只能有一人处理,假定A用户已经“抢先”处理此流程,当B用户和C用户尝试处理同一流程的时候,系统必须能够通知两位后来者:已经有人在处理此流程了 ,这就是所谓的并发冲突。
本节我们用一个简单例子来说明如何将工作流和并发冲突相结合,其中并发冲突的处理技巧,和数据源无关,正因为不需要借助特定数据源的功能,所以通用性很强。
假定一个表有九列,分成三个工作流程来处理:
流程1处理一、二、三列
流程2处理四、五、六列
流程3处理七、八、九列
只有在上一流程已经处理完毕的情况下,才可以处理下一流程,例如只有输入一、二、三列的值后,才能输入四、五、六列的值。
本次设计要完成的任务为:
1、在针对某行处理一个流程前,首先自动检索该行的最新数据和流程处理进度,以决定能否处理此流程。
2、如果能够处理此流程,那么在开始处理之前先锁定此行,使得其他用户不能再处理此行,避免出现多个用户同时处理某一行的情况。
3、用户在处理完一个流程后,在保存处理结果的同时,还应该解锁行,使得其他用户可以处理此行。
4、提供强制解锁和流程回退功能,当某行出现死锁的时候,可以将某行解锁并强行回退到任意流程。
提示:这里的锁定行,和我们之间介绍的锁定行是不同的,这里是在后台锁定某个行,使得其他用户不能编辑此行。
设计步骤:
1、首先我们需要加一个整数型的标记列,
标记列的值等于1、2、3的时候,分别表示已经处理完成流程1、流程2、流程3,当标志列的值等于-1的时候,表示有人正在处理此行,也就是被锁定了。
2、将表锁定,避免用户直接在表中输入数据
3、增加一个选择流程的窗口,窗口的设计如下:
按钮名称 | 代码 |
流程一 |
Dim
r As
Row =
Tables("表A").Current r.DataRow.Load() '重新加载此行的数据 If r.IsNull("标记") Then Dim cmd as New SQLCommand cmd.CommandText = "Update {表A} Set 标记 = -1 Where 标记 Is Null And [_Identify] = " & r("_Identify") If cmd.ExecuteNonQuery = 1 Then '防止并发冲突,上面的条件是很关键的 Forms("流程1").Open() Else MessageBox.Show("其他用户已经抢先处理此行!") End If ElseIf r("标记") = -1 Then Messagebox.Show("其他用户正在处理此行!") Else MessageBox.Show("流程1已经完成!") End If |
流程二 |
Dim
r As
Row =
Tables("表A").Current r.DataRow.Load() '重新加载此行的数据 If r.IsNull("标记") Then MessageBox.Show("流程1没有完成!") ElseIf r("标记") = 1 Then Dim cmd as New SQLCommand cmd.CommandText = "Update {表A} Set 标记 = -1 Where 标记 = 1 And [_Identify] = " & r("_Identify") If cmd.ExecuteNonQuery = 1 Then '防止并发冲突,上面的条件是很关键的 Forms("流程2").Open() Else MessageBox.Show("其他用户已经抢先处理此行!") End If ElseIf r("标记") = -1 Then Messagebox.Show("其他用户正在处理此行!") Else MessageBox.Show("流程2已经完成!") End If |
流程三 |
Dim
r As
Row =
Tables("表A").Current r.DataRow.Load() '重新加载此行的数据 If r.IsNull("标记") OrElse r("标记") = 1 Then MessageBox.Show("流程2没有完成!") ElseIf r("标记") = 2 Then Dim cmd as New SQLCommand cmd.CommandText = "Update {表A} Set 标记 = -1 Where 标记 = 2 And [_Identify] = " & r("_Identify") If cmd.ExecuteNonQuery = 1 Then '防止并发冲突,上面的条件是很关键的 Forms("流程3").Open() Else MessageBox.Show("其他用户已经抢先处理此行!") End If ElseIf r("标记") = -1 Then Messagebox.Show("其他用户正在处理此行!") Else MessageBox.Show("流程3已经完成!") End If |
强制解锁与流程回退(1) |
'强制解锁,并回退到流程1 Dim dr As DataRow = Tables("表A").Current.DataRow Dim cmd as New SQLCommand cmd.CommandText = "Update {表A} Set 标记 = 1 Where [_Identify] = " & dr("_Identify") cmd.ExecuteNonQuery dr.Load() |
强制解锁与流程回退(2) |
'强制解锁,并回退到流程2 Dim dr As DataRow = Tables("表A").Current.DataRow Dim cmd as New SQLCommand cmd.CommandText = "Update {表A} Set 标记 = 2 Where [_Identify] = " & dr("_Identify") cmd.ExecuteNonQuery dr.Load() |
强制解锁与流程回退(3) |
'强制解锁,并回退到流程3 Dim dr As DataRow = Tables("表A").Current.DataRow Dim cmd as New SQLCommand cmd.CommandText = "Update {表A} Set 标记 = 3 Where [_Identify] = " & dr("_Identify") cmd.ExecuteNonQuery dr.Load() |
强制解锁与流程回退(Null) |
'强制解锁,并回退到初始状态 Dim dr As DataRow = Tables("表A").Current.DataRow Dim cmd as New SQLCommand cmd.CommandText = "Update {表A} Set 标记 = Null Where [_Identify] = " & dr("_Identify") cmd.ExecuteNonQuery dr.Load() |
提示:SQLCommand的ExecuteNonQuery命令会返回一个整数值,表示受此命令影响的行数。
4、增加三个模式窗口,分别为流程1、流程2、流程3,用于处理各个流程,每个流程窗口的设计都是类似的,用于输入该流程 应该处理的数据,例如流程2窗口的设计为:
由于表是锁定的,所以流程窗口中文本框的只读属性应该设置为False,否则不能输入数据:
给流程窗口的按钮设置代码,以流程2窗口为例:
按钮名称 | 代码 |
确定 |
With
Tables("表A").Current If .Isnull("第四列") OrElse .IsNull("第五列") OrElse .IsNull("第六列") Then MessageBox.Show("流程二没有处理完!") Return End If End With Tables("表A").Current("标记") = 2 DataTables("表A").Save() e.Form.Close() |
取消 |
Tables("表A").Current.Reject() |
提示:为了防止用户跳过确定和取消按钮,直接关闭窗口,流程1、流程2、流程3三个窗口的“显示控制框”属性设置为False,这样窗口标题栏就不会出现关闭按钮:
防止并发冲突的原理
至此我们这个简单的工作流系统已经设计完成,本系统不仅能够实现工作流,而且可以防止并发冲突,从某用户开始编辑某行数据开始,任何人都不能编辑此行数据,直到该用户结束或取消编辑。
其实现的原理很简单,以某个用户准备对某行数据进行流程2处理为例,用户单击流程选择窗口的流程2按钮后,执行的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Dim
r As
Row =
Tables("表A").Current r.DataRow.Load() '重新加载此行的数据 If r.IsNull("标记") Then MessageBox.Show("流程1没有完成!") ElseIf r("标记") = 1 Then Dim cmd as New SQLCommand cmd.CommandText = "Update {表A} Set 标记 = -1 Where 标记 = 1 And [_Identify] = " & r("_Identify") If cmd.ExecuteNonQuery = 1 Then '防止并发冲突,上面的条件是很关键的 Forms("流程2").Open() Else MessageBox.Show("其他用户已经抢先处理此行!") End If ElseIf r("标记") = -1 Then Messagebox.Show("其他用户正在处理此行!") Else MessageBox.Show("流程2已经完成!") End If |
为便于讲解,我们对上面的代码进行了编号:
1、第2行代码重新加载此行,获得最新的数据和流程处理进度。
2、第3行代码判断流程进度是否为空,如果为空表示流程1没有处理完成,所以不能处理流程2;第5行代码判断当前进度是否等于1,如果等于1,表示可以处理流程2。
3、第6、7、8行是处理并发冲突的关键代码,用Update语句将当前行的标记列设置为-1,等于通知其他用户,我开始处理此行了;Update语句的条件设置是至关重要的,主键值条件的加入是当然的,也是必须的,大家都能理解,最关键的是“标记
=
1”的条件;假定A和B用户几乎在同一时间对同一行进行流程2处理,两个人在重新加载此行后,都得到了此行目前流程处理进度为1的信息,可以进行流程2的处理,于是两个人都试图用Update语句将当前行的标记设置为-1,但是只有一个人的ExecuteNonQuery方法(第8行)返回值会等于-1;例如假定A用户抢先执行了第8行的代码,ExecuteNonQuery的返回值等于1,于是正常开始流程2的处理,B用户随后同样执行第8行代码,但由于此时标记列的内容
已经等于-1,不符合第7行设置的条件之一“标记 = 1”,所以不会有任何符合条件的行,于是第8行ExecuteNonQuery方法的返回值不等于1,不能正常进入流程2的处理。
上面是进入流程的处理, 下面讲讲退出流程的处理,以流程2窗口为例,该窗口的确定按钮代码为:
1 2 3 4 5 6 7 8 9 |
With
Tables("表A").Current If .Isnull("第四列") OrElse .IsNull("第五列") OrElse .IsNull("第六列") Then MessageBox.Show("流程二没有处理完!") Return End If End With Tables("表A").Current("标记") = 2 DataTables("表A").Save() e.Form.Close() |
上面的代码首先判断流程2是否正常处理完毕,然后在第7行代码将当前行标记列的值设置为2,并保存数据,等于告诉其他用户:我已经处理完流程2了,你们可以进行后续处理工作了。
流程2窗口取消按钮的代码为:
1 2 3 4 |
Tables("表A").Current.Reject() Tables("表A").Current("标记") = 1 DataTables("表A").Save() e.Form.Close() |
第1行代码撤销对当前行的修改,第2行代码将标记列的值设置为1,并保存数据,等于告诉其他用户:我放弃处理流程2了,流程进度重新回到1,你们可以继续处理此行了。
强制解锁
如果某个用户正在针对某行处理一个流程,此时因为断电或者其它原因导致非正常退出,那么此行的标记列内容将一直等于-1,任何人将不能再处理此行了,所以我们还应该提供“解锁”的功能,强行将标志列的内容设置为某个值。
例如下面的代码,将标志列的内容强行设置为2:
'强制解锁,并将流程设置为2
Dim dr
As
DataRow =
Tables("表A").Current.DataRow
Dim cmd
as
New SQLCommand
cmd.CommandText = "Update {表A} Set
标记 = 2 Where [_Identify] = " & dr("_Identify")
cmd.ExecuteNonQuery
dr.Load()
提示:实际应用的时候,应该设置为只有特定用户才能执行强制解锁功能。