本文记录在 Sybase PowerDesigner 16.5.0.3982(D:\powerdesigner,COM 服务器
pdshell16.exe,CLSID{B6988F4F-A312-45D8-9BFF-29ABF70844E1})上实际验证过 的自动化接口事实。信息来源:
- 官方
Ole Automation\VBScriptConstants.vbs(元类 ID 常量)- 官方 C# 示例
Ole Automation\CSharp Samples\Sample1\Form1.csInterop.PdCommon.dll/Interop.PdPDM.dll元数据(成员名)- 本机真机 POC 运行(
pd_mcp probe/.probe/exp_round*.py)- Sybase 官方文档《Generating a Database (Scripting)》(infocenter.sybase.com)
- ProgID:
PowerDesigner.Application(16.5 变体:PowerDesigner.Application.16.5) - 本机注册在 32 位视图(WOW6432Node),但 64 位 Python
Dispatch()可直接 CoCreateInstance 成功(out-of-process LocalServer32)。 - 附加到运行实例:
pythoncom.GetActiveObject(progid)。 Application无Quit方法 —— 关闭用PostMessage(MainWindowHandle, WM_CLOSE)。app.InteractiveMode = False抑制模态对话框(真机验证有效; 设为 1 反而可能弹窗导致 COM 调用挂起)。app.Version→ "16.5.0.3982";app.HomeDirectory→ 安装目录;app.BeginTransaction/EndTransaction/CancelTransaction真机可用。
- 创建:
app.CreateModel(modelClass, template, 0)- PDM 模型类 ID:
-840675807(CDM509178224,LDM1598421368, 来源 VBScriptConstants.vbs) template = "|Diagram=PhysicalDiagram"(属性串格式,来自官方 C# 样例)template也可以是 .pdm 文件路径:新模型直接绑定该文件(SaveAs 替代方案的基石)
- PDM 模型类 ID:
model.ChangeDBMS("MySQL 5.0")→ DBMS 变为MYSQL50(真机验证)- 默认 DBMS:新 PDM 为
SYASA10(SQL Anywhere 10) model.Save():有文件时写盘;未保存模型静默返回(不弹窗、不落盘)model.FileName:未保存时为空串;FileName 属性只读(SetAttribute 被拒)model.CheckModel()返回None—— 检查结果进 PD 的 Result List 窗口model.GetPackageOptions()生成选项对象(见 §5 DDL)
tbl = model.CreateObject(940288576, "", -1, False) # PdPDM_Table
tbl.SetNameAndCode("ExpUser", "exp_user")
col = tbl.Columns.CreateNew(940288577) # PdPDM_Column
col.SetNameAndCode("UserId", "user_id")
col.DataType = "INT"; col.Length = 30
col.Mandatory = True
col.Primary = True # ← 官方样例的主键创建方式
diagram.AttachObject(tbl) # 图形符号- Key 对象(940288580)有
Primary可读写、Columns关联集合,可显式命名 PK。 - 列还有
DefaultValue / Comment / Description / Domain(let/set)。
References.CreateNew(1852601152);ref.ParentTable = t1; ref.ChildTable = t2ref.ParentKey = parentPK赋值后 PD 自动生成 Joins 并迁移 FK 列 (真机验证:1:N 自动建立 join)。- 手动 join:
ref.Joins.CreateNew(1852601153)→join.ParentTableColumn / join.ChildTableColumn(可 let/set,已验证) - Reference 另有
Mandatory / ParentRole / ChildRole / MinimumCardinality / MaximumCardinality / UpdateConstraint / DeleteConstraint / ForeignKeyConstraintName、 方法UpdateReferenceJoins / UpdateImplementation - 索引:
table.Indexes.CreateNew(940288578);idx.Unique = True; 列集合名为IndexColumns(不是 Columns),ic.Column = col(IndexColumn 940288579,可 let/set Column)。
官方文档流程(sybase infocenter 16.5):
opts = model.GetPackageOptions()
opts.GenerateODBC = False # 强制 SQL 脚本而非 ODBC
opts.GenerationPathName = r"C:\out" + "\\" # ⚠ 必须带尾部反斜杠!
opts.GenerationScriptName = "script.sql"
model.GenerateDatabase() # 无参调用坑 1:GenerationPathName 不带尾部 \ 时与脚本名直接拼接
(实测生成 logs + exp4.sql = logsexp4.sql)。
坑 2:pywin32 动态绑定无法为 GenerateDatabase 的可选对象参数构造
默认值(报 "The Python instance can not be converted to a COM object"),
必须用原始 IDispatch Invoke:
ole = model._oleobj_
dispid = ole.GetIDsOfNames(0, "GenerateDatabase")
ole.Invoke(dispid, 0, pythoncom.DISPATCH_METHOD, False) # 不取返回值真机输出验证(MySQL 5.0):drop table if exists exp_user; create table exp_user ( user_id INT not null, primary key (user_id) );
- 无
SaveAs(BaseModel/Model 均无;hasattr(model,'SaveAs') == False) - 另存为方案(真机验证):复制
<HomeDirectory>\ShellNew\pdmodel16.pdm|.cdm|.ldm→ 目标路径 →CreateModel(kind, 目标路径)(文件绑定)→ 复制内容 →Save()
- 集合:
.Count、.Item(i)(0 基)、.CreateNew(classId)、.Add(obj) - 对象:
GetAttribute(name)/SetAttribute(name,value)可访问任意元类属性; 失败属性名会出现在 com_error 文本中(如 "attribute Filename is read-only") SetNameAndCode(name, code)后 Code 可能被命名规范重推导,需回读校验- 模型种类识别:
model.ClassKind(数值=元类 ID);回退:探测Tables/Entities
- 请求路径可能只拿到模板壳:
Save()有时把真实内容写到"去掉扩展名"的 同级文件(50 KB 模型落在a_model,请求的a_model.pdm只剩 1.9 KB 模板壳;FileName在Save()后会被规范化成不含扩展名的形式)。另一些会话里 PD 会正常写回请求路径并额外生成.pdb/.cdb备份。行为不稳定,所以 保存后必须校验请求路径的内容,必要时把无扩展名文件搬回请求路径—— 移动前必须先Close()(PD 持句柄,否则 WinError 32),移动后再OpenModel(请求路径)让绑定保持一致。实现见_stray_saved_file()与save_model_as()。 - 内容复制不带图符号:复制后必须对每个表/实体与引用/关系重新
AttachObject/AttachLinkObject,否则新模型打开是空白图。附加完毕后调AutoLayout(原始 Invoke、无参)可避免符号重叠。Model.Diagram.Symbols.Count可用来断言"图不是空的"。
PDM 之外的对象容器与语义(真机验证):
| 概念 | PDM | CDM / LDM |
|---|---|---|
| 实体 | Tables |
Entities |
| 属性 | Columns |
Attributes |
| 主键 | Keys + Key.Primary=True |
Identifiers + Entity.PrimaryIdentifier |
| 关联 | References(ParentTable/ChildTable/Joins) |
Relationships(Entity1/Entity2) |
| 索引 | Indexes |
不存在(物理概念,应显式拒绝而非静默忽略) |
-
属性没有
Primary语义:给 CDM 属性设Primary=True不构成主键。必须Identifiers.CreateNew(509178229)→Identifier.Attributes.Add(attr)→Entity.PrimaryIdentifier = ident。成员可用Attributes.Add/Remove/Clear增删。 -
主标识符不能"就地替换"、也不能先置空:
Entity.PrimaryIdentifier = None抛类型不匹配(_try_set会静默吞掉);- 旧标识符仍存在时新建同名标识符 →
That name already exists!。 正确顺序:先prev.delete()(删除后PrimaryIdentifier自动变 None), 再创建新标识符并赋值;赋值后必须回读校验,失败要报错而不是静默通过。
-
属性数据类型是 PD 自有词汇,不是 DBMS 语法:
Characters(n)/Variable characters(n)/Integer/Decimal(p,s)/Timestamp。 直接传VARCHAR(20)会让 CheckModel 报未知类型。 -
关系基数:
Entity1ToEntity2RoleCardinality(E1→E2,即 E2 端重数)与Entity2ToEntity1RoleCardinality(E2→E1,即 E1 端重数),值为逗号格式"0,n"/"1,1"(不是 SQL 的0..*)。角色名Entity1ToEntity2RoleName/Entity2ToEntity1RoleName;依赖侧DependentRole("A"/"B")。 -
CDM 自身不做外键迁移:实测
campus_canteen.cdm中每个标识符属性只出现 一次;父实体的标识符是在生成 LDM 时才迁移进(x,n)端的子实体。 -
原生转换入口:PD 16.5 没有
GenerateLogicalDataModel/GeneratePhysicalDataModel方法,唯一入口是GenerateModel(ObjectSelection, Kind, Target, SaveDependencies)(dispid 33554819), 须原始InvokeTypes调用:ole.InvokeTypes(33554819, 0, pythoncom.DISPATCH_METHOD, (9, 2), ((9, 49), (3, 49), (8, 49), (11, 49)), None, 目标模型元类ID, "", True)
返回的模型未绑定文件,落盘要走第 6 节的 ShellNew 绑定流程。
-
链接对象的符号判定要覆盖关系:CDM 关系没有
Joins集合,只按hasattr(obj,'Joins')判断会把关系送进AttachObject(对链接对象是静默 no-op),图上就会丢连线。判定应同时检查Entity1/Entity2。
GetAttribute对不存在的属性也返回非 None,所以_safe()的 GetAttribute 兜底不能用来判断属性/集合是否存在。真机后果:一个 CDM 与 一个 LDM 都被判成 PDM(_safe(m,'Tables')返回了非 None)。凡"探测存在性 / 区分种类"的代码一律走纯getattr(实现:_plain_attr())。ClassKind可能是 int,也可能是数字字符串(真机:原生生成的 LDM 返回'1598421368'),比较前先int(str(ck))归一化;且必须在创建该对象的同一 COM 套间内读取——跨线程派发得到的代理会给出不同结果。ObjectID类型随元类变化:模型是整数,CDM 标识符是 GUID 字符串 ('{8CAF64C2-…}')。对象身份比较请统一用字符串形式;int('{...}')会抛异常 → 退化成is比较 → 恒 False(曾导致主标识符回读primary=False、CDM→LDM 迁移失效)。- COM 代理必须在创建它的套间里释放:主线程(从未
CoInitialize)在解释器 退出时回收代理会报CO_E_NOTINITIALIZED (0x800401f0)致命异常。断开连接时 应在派发线程内清空_models/_refs/_app(见ComAdapter._release_references()与ComDispatcher.shutdown())。 - 宿主 safe-delete 拦截:本机
unlink()被sitecustomize改为走回收站, 脚本批量删临时文件会抛OSError。清理逻辑要么用唯一目录,要么容忍失败。