马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PowerBuilder DataWindow 数据共享与搬运全解:ShareData、RowsCopy、RowsMove 的真实语义(PB10 基准 · PB12.5 实测)
阅读说明
① 适用版本:PB 10.0 基准编写,PB 12.5 实机逐项验证(两版所有读数逐字一致,零版本差异);ShareData / RowsCopy / RowsMove 自 PB 5 起就存在,低版本同样适用。
② 支持数据库:正文主体不依赖数据库(用语法串 + ImportString 在内存里造数据);「Update 归属」一节的验证在 SQL Server 上完成,思路适用于 PB 支持的任何数据库。
③ 操作系统与环境要求:Windows 7 及以上;无需任何第三方运行库,纯 PowerScript + DataStore 即可复现全部示例。
④ 难度系数:★★★☆☆(函数本身简单,难在共享后的缓冲区语义与「谁能 Update」这些反直觉细节)。
⑤ 其它阅读说明:示例全部用 DataStore(无窗口也能跑);把 DataStore 换成窗口里的 DataWindow 控件,行为完全一致。阅读前置:建议先读《DataWindow 三缓冲区详解》与《DataStore 后台批处理》。
一、这一篇讲什么
一个列表窗口和一个图表要同时看同一份数据;一个窗口改了数,另一个窗口要立刻跟着变。这类「多处看同一份数据」的需求,PowerBuilder 给了三个函数:ShareData 让两个 DataWindow/DataStore 共享同一份数据,RowsCopy 把行从一个缓冲区拷到另一个(可跨对象),RowsMove 则是「搬走」而不是「复印」。
这三个函数的文档写得很简短,但实测下来有好几处和流传说法相反:从属对象并不是只读的;从属上调用 Retrieve 会把共享数据整个清空;共享状态下 DeletedCount() 的读数会骗人。这一篇把 15 个探针的实测结果全部摊开。
二、ShareData 的基本语义:主从登记,缓冲区同一份
先看最小可运行示例。两个 DataStore 用同一份语法串创建,主对象灌数据,然后 ShareData:
- // 数据共享基本示例(可整段放进任意事件或函数直接跑)
- long ll_rc, ll_sh
- string ls_syn, ls_err, ls_msg
- datastore lds_main, lds_view
- // 语法串:三列 id/name/qty 的最小 DataWindow 定义,不依赖数据库
- ls_syn = 'release 10; datawindow(units=0 processing=1) ' + &
- 'table(column=(type=long name=id dbname="t.id") ' + &
- 'column=(type=char(10) name=name dbname="t.name") ' + &
- 'column=(type=long name=qty dbname="t.qty") ' + &
- 'retrieve="PBSELECT( VERSION(400) )")'
- lds_main = create datastore
- lds_view = create datastore
- ll_rc = lds_main.Create(ls_syn, ls_err)
- ll_rc = lds_view.Create(ls_syn, ls_err)
- // 主对象灌入三行数据(Tab 分列、换行分行)
- ll_rc = lds_main.ImportString('1~tA~t10~n2~tB~t20~n3~tC~t30')
- // 关键一行:主对象调用 ShareData,参数是「从属」
- ll_sh = lds_main.ShareData(lds_view)
- ls_msg = 'share=' + String(ll_sh) // 实测返回 1
- ls_msg = ls_msg + ',rows=' + String(lds_view.RowCount()) // 实测从属立刻有 3 行
- // 主改从见、从改主见:缓冲区是同一份
- lds_main.SetItem(3, 3, 99)
- ls_msg = ls_msg + ',主改=' + String(lds_view.GetItemNumber(3, 3)) // 实测 99
- lds_view.SetItem(1, 2, 'Z')
- ls_msg = ls_msg + ',从改=' + lds_main.GetItemString(1, 2) // 实测 Z
- MessageBox('ShareData 基本语义', ls_msg)
复制代码
实测读数:share=1,rows=3,主改=99,从改=Z。三个要点:
方向是「主.从属」。ShareData 是主对象的方法,参数是从属对象。写反了(从属调 ShareData 传主对象)在多数场景也能建立共享,但 Update 语义、ShareDataOff 的效果都会跟着「谁是调用者」走,别依赖写反的写法。
共享的是缓冲区不是副本。SetItem 改的是同一块内存里的数据,所以任何一侧改动、另一侧立刻可见——不需要刷新、不需要重新 Retrieve。实测行状态(GetItemStatus)也是同一份。
从属不需要先有数据。从属对象可以是一个刚 Create 的空 DataStore,ShareData 之后自动拥有主对象的全部数据(包括 Primary / Filter / Delete 三个缓冲区的划分)。
三、从属不是只读:改动同步、Update 也能落库
流传很广的说法是「共享数据时只有主对象能 Update」。实测推翻了这个说法。在 SQL Server 上的验证序列:主对象连库 Retrieve 出 3 行,共享给从属,然后从属自己 SetItem 改数、从属自己挂事务 Update:
- // 从属侧修改与提交(连库验证,事务对象请先连好并按环境填参数)
- long ll_rows, ll_u2, ll_db_qty
- datastore lds_main, lds_view
- lds_main = create datastore
- lds_view = create datastore
- lds_main.Create(is_syndb) // 带检索 SQL 与更新属性的语法串,见第八节
- lds_view.Create(is_syndb)
- lds_main.SetTransObject(sqlca)
- ll_rows = lds_main.Retrieve() // 实测返回 3
- lds_main.ShareData(lds_view)
- // 从属改第 2 行的 qty 为 44,然后从属自己 Update
- lds_view.SetItem(2, 3, 44)
- ll_u2 = lds_view.Update() // 实测返回 1
- COMMIT USING sqlca;
- // 回查数据库:qty 真的变成 44 了
- SELECT qty INTO :ll_db_qty FROM t_share WHERE id = 2 USING sqlca;
- MessageBox('从属 Update 实证', 'u2=' + String(ll_u2) + ',库中qty=' + String(ll_db_qty))
- // 实测读数:u2=1,库中qty=44
复制代码
结论:从属挂上自己的事务对象后,Update 返回 1 且真的落库。原因不难理解——共享的是缓冲区(连同行状态 DataModified),从属的 Update 拿的就是这份共享缓冲区里的脏行,生成 UPDATE 语句一气呵成。
继续深挖两步:主对象改一行、从属 DeleteRow 删一行,然后主对象 Update:
- 实测:Update 返回 1,数据库行数从 3 变成 2,被删的行真没了,主改的数也落库了。
也就是说:缓冲区是同一份、行状态是同一份,谁调用 Update 只决定「用哪个事务对象去提交」,不决定「哪些改动被提交」。任何一侧的改动(包括删除)都会随任意一侧的 Update 一起落库。想在共享数据上做「只读视图」,靠 ShareData 是做不到的,得靠别的方式(比如给从属挂一个只读事务、或干脆用 RowsCopy 复制一份)。
四、从属上的 Retrieve:一个 -1,清空整份共享数据
这是本篇最重的一个坑。共享之后,从属上调 Retrieve():
- 返回值:-1(不出所料,从属没有检索权);
- 副作用:主从两侧的数据同时清零(RowCount 都变 0)——出于意料;
- 共享关系:没有被破坏,之后主对象重新 ImportString,两侧又同步恢复。
实测读数序列:ret2=-1 → rows1=0, rows2=0 → 主重灌1行 → rows1=1, rows2=1 → 再次 ShareData 仍返回 1。
为什么?共享的是缓冲区,Retrieve 的第一步本来就是 Reset 清空缓冲区——从属的 Retrieve 在「清空」这半截生效了,「取数」那半截失败了。于是整份共享数据被无辜清空。
防护写法:从属一律不要调 Retrieve;封装层加一个断言,共享状态下谁调 Retrieve 就在开发期报错:
- // 共享安全 Retrieve 封装:只允许主对象取数
- // 参数:ads_want 要取数的对象;ab_is_primary 调用方是否为主对象
- // 返回:检索行数;从属调用一律返回 -99 不执行
- long ll_rc
- if not ab_is_primary then
- MessageBox('共享数据取数错误', '当前对象是共享从属,取数请通过主对象进行。')
- return -99
- end if
- ll_rc = ads_want.Retrieve()
- if ll_rc < 0 then
- MessageBox('检索失败', 'Retrieve 返回 ' + String(ll_rc))
- end if
- return ll_rc
复制代码
五、ImportString 在从属上竟能成功:写进去的是共享数据
第四节的坑还有一个镜像:既然从属 Retrieve 被禁止,那从属 ImportString 呢?实测成功,返回 1,而且写入的就是共享缓冲区——主对象立刻多出一行,主从行数同步变成 4。
实测读数:imp2=1, rows1=4, rows2=4,并且主对象 GetItemString 能读到从属灌进来的那行。之后主对象再 ImportString 一行,从属行数同步变 5。
把三、四、五节合起来,共享态的规则可以概括成一句:「读方向」的操作(Retrieve)被拒绝且带破坏性副作用;「写方向」的操作(SetItem / ImportString / Update)全部畅通,写的都是同一份数据。从属的「从」只体现在 Retrieve 权上,不体现在数据所有权上。
六、结构匹配:不同就 -1,相同才行
ShareData 要求两侧数据源完全相同(列数、列类型、列名)。用一份三列语法串和一份两列语法串实测:
- 不同结构 ShareData:返回 -1,从属保持 0 行,主对象数据不受影响;
- 相同结构 ShareData:返回 1。
这里「相同」指的是 DataWindow 对象的数据源定义一致。实践中最稳的做法是两边用同一份语法串(或同一个 .srd 对象)创建,避免手抄两份定义抄出不匹配。
七、一主多从与链式共享
一个主对象可以共享给多个从属(实测两次 ShareData 都返回 1,两个从属行数都是 3);从属还能继续往下共享(实测 lds2.ShareData(lds4) 返回 1,第四级行数 3、主对象 SetItem 后第四级立刻读到新值、第四级改数主和二程也立刻可见)。
链式共享里最有意思的是中游退出:中间那个从属调用 ShareDataOff 之后——
- 调用者自己:数据清空(RowCount 变 0),从此独立;
- 它的上游和下游:仍然互联——主对象改数,第四级照样同步收到(实测 666 传播到位);
- 中游自己想再干活:ImportString 重新造数即可,造出来的数据与链上其它成员无关(实测各改各的互不影响)。
也就是说共享关系不是一条链,而是全体成员挂同一份缓冲区的星型结构,ShareDataOff 只是让「调用者」一个人下车。
八、ShareDataOff:下车即清空
ShareDataOff 的实测语义比文档写的狠:谁调用,谁的数据清空,返回 1。
- 主对象调用:主对象保留 3 行,从属清空(实测 off1=1, rows2_after=0, rows1=3);
- 从属调用:从属自己清空,主保留(实测 off_by_sec=1, rows4=0, rows3=1);
- 中游调用:中游自己清空,上游下游不受影响。
一句话:ShareDataOff 之后的调用者,缓冲区空了、独立了,想继续用得自己重新造数或重新取数。写「下车」逻辑时,先判断调用者里还有没有要保留的数据,必要时先 RowsCopy 抢救一份再 off。
九、过滤与删除:同一份缓冲区的两面
过滤(Filter)在共享态是「全局」的。实测:主对象 SetFilter('id > 1') 再 Filter() 之后,从属的 RowCount 同步变成 2,从属的 FilteredCount 也能数到被过滤出去的行;反过来从属再 Filter('id > 2'),主对象的视图同样跟着变。两侧行数、过滤行数完全一致——因为过滤的本质是把行在 Primary 和 Filter 两个缓冲区之间移动,而这两个缓冲区是共享的。
删除(DeleteRow)有个计数陷阱。实测:主对象 DeleteRow(2) 之后,主从行数同步减 1(3 变 2),但 主从两侧的 DeletedCount() 都返回 0——被删的行既不在从属的 Delete 缓冲区计数里,也不在主对象的计数里。第一感觉是「这行丢了、Update 不会删库」,连库验证给出的答案恰恰相反:
- 主对象 DeleteRow 之后 Update,数据库里那行真的被 DELETE 了(行数 3 变 2);
- 从属 DeleteRow、主对象 Update,同样真删。
所以正确结论是:共享态下 DeletedCount() 的读数不可信(恒 0),但删除本身完全有效。判断「有没有待删除的行」不要数 DeletedCount,直接以行数变化或自己的业务标记为准。这个计数失真在非共享对象上不存在(实测非共享 DataStore 的 RowsMove 到 Delete 缓冲区后 DeletedCount 正确报 1)。
十、RowsCopy:复印一份,目标行状态是 NewModified
RowsCopy 与共享无关,它是纯搬运工具,两个对象不需要共享、甚至结构可以不同(按列位置拷)。实测三件事:
- // RowsCopy 三连:跨对象拷、拷进 Filter 缓冲区、拷进 Delete 缓冲区
- long ll_c1, ll_c2, ll_c3
- string ls_msg
- datastore lds_src, lds_dst
- lds_src = create datastore
- lds_dst = create datastore
- lds_src.Create(is_syn) // 与第二节相同的三列语法串
- lds_dst.Create(is_syn)
- lds_src.ImportString('1~tA~t10~n2~tB~t20~n3~tC~t30')
- // 1) 把源的 1~2 行拷到目标 Primary 缓冲区第 1 行之前
- ll_c1 = lds_src.RowsCopy(1, 2, Primary!, lds_dst, 1, Primary!)
- ls_msg = 'c1=' + String(ll_c1) + ',src=' + String(lds_src.RowCount()) + ',dst=' + String(lds_dst.RowCount())
- // 2) 目标内部:Primary 第 1 行拷进自己的 Filter 缓冲区
- ll_c2 = lds_dst.RowsCopy(1, 1, Primary!, lds_dst, 1, Filter!)
- ls_msg = ls_msg + ',filt=' + String(lds_dst.FilteredCount())
- // 3) 目标内部:Primary 第 2 行拷进自己的 Delete 缓冲区
- ll_c3 = lds_dst.RowsCopy(2, 2, Primary!, lds_dst, 1, Delete!)
- ls_msg = ls_msg + ',del=' + String(lds_dst.DeletedCount())
- MessageBox('RowsCopy 三连', ls_msg)
复制代码
实测读数:c1=1,src=3,dst=2,filt=1,del=1。要点:
- 源不动:拷贝后源还是 3 行(与 RowsMove 的本质区别);
- 目标行状态是 NewModified!(实测 GetItemStatus 返回 NewModified!)——拷过去的行带着「新改过」的状态,目标如果挂了事务,Update 会把这些行当新行 INSERT;
- 可以跨缓冲区拷:拷进 Filter! 只进过滤缓冲区(RowCount 不变、FilteredCount 加一),拷进 Delete! 则这些行会随 Update 生成 DELETE——拿它做「批量删除」的预备动作很好用。
十一、RowsMove:搬走就是真搬走
RowsMove 与 RowsCopy 参数完全同形,行为差在源侧。实测:
- 源 3 行 RowsMove(1, 2, Primary!, 目标, 1, Primary!) 之后:源只剩 1 行(原来第 3 行顶上来),目标 2 行,目标行状态同样是 NewModified!;
- 源对自身 RowsMove(1, 1, Primary!, 源, 1, Delete!)(Primary 搬进自己的 Delete 缓冲区):源 RowCount 变 0,DeletedCount 正确返回 1——对照第九节共享态 DeleteRow 的计数 0,可以看到这个计数失真是「共享态 DeleteRow」特有的。
「正序删漏行、倒序删干净」的规律在 RowsMove 上同样成立:按行号往上搬,搬一行行号全体前移,循环变量要倒着走。
十二、排序与清空也同步
共享态下主对象 SetSort + Sort,从属立刻按新顺序读数(实测降序后从属第一行就是最大 id);主对象 Reset() 清空,从属同步变 0 行;重灌数据,从属同步恢复。排序、清空、重灌,全部实时同步——再次印证「同一份缓冲区」这个模型。
十三、完整示例:一份封装函数串起全流程
把前面所有结论装进一个可复用的 NVO 方法,主流程五步:造数据、共享、双改、备份、分手:
- // nvo_art.of_run 的等价展开:共享全流程演示(无窗口可跑,实测读数见注释)
- long ll_rc, ll_sh, ll_cp
- string ls_syn, ls_err, ls_msg
- datastore lds_main, lds_view, lds_bak
- // 语法串与第二节相同(is_syn 提前准备好,此处从略其拼接)
- ls_syn = is_syn
- lds_main = create datastore
- lds_view = create datastore
- lds_bak = create datastore
- lds_main.Create(ls_syn, ls_err)
- lds_view.Create(ls_syn, ls_err)
- lds_bak.Create(ls_syn, ls_err)
- // 第 1 步:主对象造 3 行数据并共享
- ll_rc = lds_main.ImportString('1~tA~t10~n2~tB~t20~n3~tC~t30')
- ll_sh = lds_main.ShareData(lds_view)
- ls_msg = 'imp=' + String(ll_rc) + ',share=' + String(ll_sh) + ',rows=' + String(lds_view.RowCount())
- // 第 2 步:双向改动实时同步
- lds_main.SetItem(2, 3, 88)
- ls_msg = ls_msg + ',主改=' + String(lds_view.GetItemNumber(2, 3)) // 88
- lds_view.SetItem(3, 2, 'OK')
- ls_msg = ls_msg + ',从改=' + lds_main.GetItemString(3, 2) // OK
- // 第 3 步:RowsCopy 抢救一份独立备份(拷贝不在共享链上)
- ll_cp = lds_main.RowsCopy(1, 3, Primary!, lds_bak, 1, Primary!)
- ls_msg = ls_msg + ',copy=' + String(ll_cp) + ',bak=' + String(lds_bak.RowCount()) // bak=3
- // 第 4 步:主对象 ShareDataOff 分手(从属清空、备份不受影响)
- lds_main.ShareDataOff()
- ls_msg = ls_msg + ',off后view=' + String(lds_view.RowCount()) // 0
- ls_msg = ls_msg + ',bak=' + String(lds_bak.RowCount()) // 3
- // 第 5 步:主对象独立灌新数据,验证三边互不干扰
- lds_main.ImportString('9~tZ~t90')
- ls_msg = ls_msg + ',最终main=' + String(lds_main.RowCount()) // 4
- ls_msg = ls_msg + ',view=' + String(lds_view.RowCount()) // 0
- ls_msg = ls_msg + ',bak=' + String(lds_bak.RowCount()) // 3
- MessageBox('ShareData 全流程', ls_msg)
- destroy lds_bak
- destroy lds_view
- destroy lds_main
复制代码
实测读数:imp=3,share=1,rows=3,主改=88,从改=OK,copy=1,bak=3,off后view=0,bak=3,最终main=4,view=0,bak=3。注意第 4 步:off 前先用 RowsCopy 把备份抢救出来——off 会清空从属,备份对象因为从未参与共享所以安然无恙。
十四、坑清单(15 条,全部实测)
- ShareData 方向是「主.从属」,返回 1 成功、-1 结构不匹配;共享后从属立刻拥有主数据。
- 共享的是缓冲区:SetItem / 行状态 / 排序 / Reset / 重灌,全部实时同步。
- 从属不是只读:SetItem、ImportString 都写入共享数据,主侧立刻可见。
- 从属挂事务后 Update 真落库(实测从属改数从属提交,库中值变为新值);「只有主能 Update」是讹传。
- 谁 Update 都提交全部共享改动:从属删的行、主改的数,随任意一侧的 Update 一起落库。
- 从属调 Retrieve 返回 -1,且把主从两侧数据全部清空;共享关系不破,主重灌后恢复同步。
- 从属 ImportString 成功且写入共享缓冲区(Retrieve 被拒但 Import 畅通,别用「从属只读」的直觉推 API)。
- 结构不同(列数/类型/列名不一致)ShareData 返回 -1,从属数据不动;两侧用同一份语法串或同一 .srd 最稳。
- 一主多从没问题;从属还能继续往下共享,第四级与主实时同步。
- ShareDataOff 谁调谁清空(无论主/从/中游),返回 1;下车前先 RowsCopy 抢救数据。
- 链式共享中游 off 后,其余成员仍互联(共享是挂同一份缓冲区的星型结构,不是链)。
- 共享态 Filter 是全局的:任一侧 SetFilter+Filter,两侧视图与 FilteredCount 完全一致。
- 共享态 DeletedCount() 恒 0(两侧都是),但删除真实有效、Update 真生成 DELETE;判断待删行数别信它。
- RowsCopy 源不动、目标行状态 NewModified!;可拷进 Filter!/Delete! 缓冲区(Delete 缓冲区计数正常)。
- RowsMove 源真搬走、目标行状态 NewModified!;Primary 搬 Delete! 后 DeletedCount=1(非共享态计数正常)。
十五、与相近方案的选型
- 要「多处实时看同一份数据」:ShareData,改动零成本同步;代价是任何一方的 Update 都会提交所有改动,且 Retrieve / off 的副作用要防。
- 要「此刻的快照、之后各改各的」:RowsCopy 复制一份,两对象彻底独立,目标行是 NewModified! 状态可直接 Update 插库。
- 要「把一批行挪去另一个对象处理」:RowsMove,源行真消失,适合拆分、归档、批量删除前的搬运。
- 要「真正的只读视图」:共享做不到,用 RowsCopy 出副本后不给副本挂事务,或者从属干脆不提供 Update 入口。
(完) |