通過 OpenAPI 批量接入自訂實體與血緣
本文介紹如何使用 DataWorks OpenAPI 將企業自有系統中的中繼資料對象和血緣關係大量註冊到 DataWorks 資料地圖,實現統一檢索、統一展示、統一血緣分析和影響分析。
概述
在企業資料治理過程中,很多關鍵資料對象並不完全產生於 DataWorks 內部,例如:
BI 報表、看板、指標、資料產品。
內部服務 API、資料服務介面、應用系統對象。
第三方資料庫、外部表格、外部欄位。
企業自研平台中的任務、流程、模型或主題域對象。
如果這些對象沒有進入資料地圖,您在做資料資產檢索、上下遊分析、故障排查和變更影響評估時,只能看到 DataWorks 已採集的表和欄位,無法形成完整的資料鏈路。通過自訂實體和血緣註冊相關 OpenAPI,您可以:
將外部業務對象註冊到資料地圖,作為可查詢、可展示、可治理的中繼資料實體。
將 DataWorks 已採集表、欄位、資料集與外部對象建立血緣關係。
註冊表與表、欄位與欄位、資料集與表之間的血緣關係。
為自訂實體、表、欄位補充業務屬性,例如業務域、來源系統、負責人、資料分層。
將外部系統的資產關係沉澱到資料地圖,支撐資料溯源、影響分析和統一資產盤點。
批量接入時,可以把 DataWorks 資料地圖視為企業自有中繼資料和血緣關係的統一消費入口。典型鏈路如下:
企業資料來源:業務系統、第三方資料庫、BI 報表、API 服務等。
批量接入工程:從源系統抽取實體清單、血緣關係清單、屬性定義清單,統一作為 OpenAPI 入參。
DataWorks OpenAPI:調用
CreateMetaEntityDef、CreateCustomAttribute、BatchCreateMetaEntities、CreateLineageRelationship等介面寫入實體定義、實體物件、自訂屬性和血緣關係。DataWorks 資料地圖:統一展示已採集對象與自訂對象,提供檢索、詳情、血緣圖、影響分析等能力。
下遊消費:調用
ListLineages等查詢介面,支撐資料溯源、影響分析、斷鏈檢測、血緣健康巡檢等營運情境。
其中,實體清單描述"有哪些對象要納管",血緣關係清單描述"對象之間如何流轉",屬性定義清單描述"哪些業務欄位需要統一展示、篩選和治理"。
物件類型選擇
接入前建議先判斷對象是否已經被 DataWorks 納管,再決定是否需要註冊實體。
典型接入情境
情境 | 樣本 | 推薦方式 |
將業務對象納入資料地圖 | 報表、API、指標、資料產品 | 註冊為自訂實體物件 |
將外部庫表納入資料地圖 | 第三方資料庫、外部表格、外部欄位 | 註冊為擴充表實體 |
給實體補充商務資訊 | 業務域、負責人、來源系統、安全等級 | 建立自訂屬性並寫入實體 |
註冊外部血緣 | 表到報表、表到 API、表到外部表格 | 調用 |
註冊納管實體之間血緣 | 表與表、欄位與欄位、表與欄位、資料集與表 | 使用 DataWorks 已採集實體 ID 註冊血緣 |
CreateLineageRelationship 支援在 DataWorks 已納管實體之間註冊血緣關係,不要求血緣兩端必須包含自訂對象。
非納管自訂對象(custom-{Type}:{Identifier})主要用於相容歷史用法。新接入情境建議優先註冊為 DataWorks 自訂實體物件,再建立血緣關係。
按對象是否納管選擇註冊方式
物件類型 | 是否需要註冊實體 | 推薦方式 | 後續 ID 擷取方式 |
DataWorks 已採集表、欄位、資料集、資料集版本 | 否 | 直接使用查詢 API 返回的實體 ID 註冊血緣 |
|
報表、API、指標、資料產品等業務對象 | 是 | 使用 |
|
DataWorks 未自動採集的外部庫表欄位 | 是 | 使用擴充表實體建模,先寫入 Database,再寫入 Table 和內聯 Column |
|
非納管自訂對象 | 否 | 僅用於相容歷史血緣註冊情境,新接入不推薦 | 調用方自我維護 |
前置要求
使用本文涉及的 OpenAPI 前,請先確認帳號和 DataWorks 版本滿足以下條件。
DataWorks 版本
本文樣本會建立實體定義、建立實體物件、建立自訂屬性並註冊血緣關係,因此需要準備 DataWorks 專業版及以上版本。其中:
CreateMetaEntityDef用於建立自訂實體定義或擴充表實體定義,介面要求專業版及以上版本。BatchCreateMetaEntities用於大量建立自訂實體物件、擴充 Database 和擴充 Table,介面要求專業版及以上版本。CreateLineageRelationship用於註冊血緣關係,介面要求專業版及以上版本。
如果只調用查詢類 API,請以對應 API 詳情頁中的版本要求為準。
帳號和角色許可權
範例程式碼使用 AccessKey 調用 OpenAPI。AccessKey 對應的主帳號、RAM 使用者或 STS 身份,需要屬於目標 DataWorks 租戶,並具備調用 DataWorks OpenAPI 的許可權。
建立自訂實體定義、自訂屬性、純自訂實體和擴充表實體時,建議使用具備以下任一許可權的帳號:
租戶 Owner。
租用戶系統管理員。
租戶資料治理管理員。
具備 DataWorks FullAccess 許可權的帳號。
如果後續要更新 DataWorks 已採集 MaxCompute 表或欄位上的業務屬性,調用方還需要是 MaxCompute 表 Owner,或對應 DataWorks 專案的專案 Owner、專案系統管理員。本文 Demo 唯讀取已採集表和欄位的 ID 並註冊血緣,不會修改這些已採集表和欄位。
運行本文 Demo 前需要準備的資料
如果需要運行本文 Java Demo,類中的 UPSTREAM_TABLE_ID 需要替換成您帳號下真實存在的 DataWorks 已採集表 ID。建議通過以下方式擷取:
表 ID:調用
ListTables或GetTable,使用返回結果中的Id。欄位 ID:調用
ListColumns或GetColumn,使用返回結果中的Id。
對於 DataWorks 已採集到的表、欄位、資料集、資料集版本,不建議手動拼接實體 ID,應優先使用對應查詢 API 返回的 ID,避免因為 ID 拼接有誤或對象尚未採集導致血緣註冊失敗。通過 BatchCreateMetaEntities 註冊的擴充表和擴充欄位,也可以在寫入成功後通過 ListTables、GetTable、ListColumns、GetColumn 查詢並擷取返回結果中的 Id。如果後續要把擴充表或擴充欄位作為血緣端點,也建議優先使用查詢 API 返回的 ID。
如果只按照"大量註冊工程方案"接入自己的對象清單和血緣清單,不一定需要使用本文 Demo 中的上遊表示例;可以直接使用您清單中真實存在的上遊和下遊實體 ID。
資料準備模板
批量接入前,推薦至少準備三類結構化清單。可以使用 CSV、Excel 或資料庫表儲存,欄位名稱可按企業內部規範調整,但語義建議保持一致。
實體定義清單樣本
欄位 | 說明 | 樣本 |
| 實體定義名稱,用於建立實體類型 |
|
| 頁面展示名稱 | BI 報表 |
| 對象建模方式:自訂實體或擴充表實體 |
|
| 是否擴充 DataWorks 表模型 |
|
| 普通屬性定義,使用 JSON 數組儲存 |
|
| 業務說明 | 企業 BI 報表資產 |
實體物件清單樣本
欄位 | 說明 | 樣本 |
| 實體類型 |
|
| 實體名稱 |
|
| 實體說明 | 每日銷售報告 |
| 普通屬性值,使用 JSON 對象儲存 |
|
| 自訂屬性值,使用 JSON 對象儲存 |
|
| 擴充 database/table 層級關係 |
|
| 擴充表欄位列表,隨表寫入 |
|
血緣關係清單樣本
欄位 | 說明 | 樣本 |
| 上遊實體 ID |
|
| 上遊實體名稱 |
|
| 下遊實體 ID |
|
| 下遊實體名稱 |
|
| 產生血緣的任務或過程 ID |
|
| 任務類型 |
|
| 任務屬性,使用 JSON 對象儲存 |
|
接入流程
推薦按照以下流程接入。具體 API 參數、欄位約束、錯誤碼和更多 SDK 程式碼範例,請參見文末"參考文檔"中的 API 詳情頁。
步驟一:梳理需要納管的對象和關係
先整理企業系統中的對象清單和關係清單。對象清單通常包括:
對象名稱,例如訂單查詢 API、銷售報告、外部訂單表。
物件類型,例如 API、報表、指標、資料庫表。
業務屬性,例如負責人、業務域、來源系統。
如果是表或欄位,整理庫名、表名、欄位名和欄位類型。
關係清單通常包括:
上遊對象。
下遊對象。
關聯類型,例如表產出報表、欄位對應欄位、API 寫入表。
產生關係的任務或過程,例如同步任務、加工任務、介面調用鏈路。
如果需要批量接入,建議先按照"資料準備模板"整理實體清單和血緣關係清單,再進入後續 API 呼叫流程。
步驟二:判斷對象是否已經被 DataWorks 納管
如果對象已經由 DataWorks 採集或平台管理,不需要重複註冊實體,直接使用對應查詢 API 返回的實體 ID。常見擷取方式:
表:調用
ListTables或GetTable。欄位:調用
ListColumns或GetColumn。資料集:調用
ListDatasets或GetDataset。資料集版本:調用
ListDatasetVersions或GetDatasetVersion。
如果對象還沒有被 DataWorks 納管,再根據物件類型選擇註冊方式:
報表、API、指標、系統等業務對象:註冊為自訂實體。
具備資料庫、表、欄位結構的外部資料資產:註冊為擴充表實體。
步驟三:建立實體定義
使用 CreateMetaEntityDef 建立實體類型定義。
自訂實體適合描述業務概念,例如 API、報表、指標。樣本:
{
"Name": "customer_api",
"DisplayName": "業務API",
"Description": "企業系統中的 API 對象",
"AttributeDefs": [
{
"Name": "apiCode",
"DisplayName": "API編碼",
"Type": "STRING",
"IsOptional": false
},
{
"Name": "serviceName",
"DisplayName": "服務名稱",
"Type": "STRING",
"IsOptional": true
}
]
}擴充表實體適合描述 DataWorks 未自動採集的外部庫表欄位。建議在建立擴充表時同步建立對應的擴充 Database 對象,並通過 parentMetaEntityId 建立 Database 到 Table 的層級關係,便於後續在資料地圖中按完整庫表層級查看。樣本:
{
"Name": "bi-table",
"DisplayName": "BI報表",
"Description": "外部 BI 系統中的庫表欄位",
"Extend": "TABLE"
}具體欄位含義和命名約束請參見 CreateMetaEntityDef API 詳情頁。
步驟四:建立可複用的自訂屬性
如果希望在資料地圖中對實體展示或篩選業務屬性,可以使用 CreateCustomAttribute 建立自訂屬性定義。例如:
來源系統。
業務域。
負責人。
資料分層。
安全等級。
樣本:
{
"Id": "custom-attribute:source_system",
"DisplayName": "來源系統",
"Type": "ENUM",
"ValueEnums": ["CRM", "ERP", "BI", "CUSTOM_APP"],
"EntityTypes": ["custom_entity-customer_api"],
"SearchFilterEnabled": true,
"DisplayEnabled": true
}自訂屬性建立後,可在寫入自訂實體、擴充表實體,或更新已採集表欄位商務資訊時使用。EntityTypes 需要填寫可掛載該屬性的具體實體類型;如果需要掛載到擴充表或擴充欄位,請填寫對應擴充實體定義產生的具體類型。
步驟五:建立實體物件
使用 BatchCreateMetaEntities 寫入具體實體物件。
自訂實體物件樣本:
{
"Entities": [
{
"EntityType": "custom_entity-customer_api",
"Name": "api_001",
"Comment": "業務訂單查詢 API",
"Attributes": {
"apiCode": "api_001",
"serviceName": "order-service"
},
"CustomAttributes": {
"source_system": ["CRM"]
}
}
]
}BatchCreateMetaEntities 是批量介面,需要同時關注兩層成功狀態:返回體最外層的 Success 表示本次批量請求是否被服務端成功處理;Results 中每條記錄的 Success 表示對應實體物件是否建立成功。實際接入時,不能只判斷最外層 Success,還需要逐條檢查 Results[].Success,並記錄失敗項的 ErrorMessage。
擴充表實體物件樣本
擴充表寫入前,建議先寫入同一擴充類型下的資料庫實體,再寫入表實體並將表的 parentMetaEntityId 指向該資料庫實體。這樣註冊後可以形成完整的 Database -> Table -> Column 層級關係,便於在資料地圖中查看和管理。由於 BatchCreateMetaEntities 同一批次的實體必須屬於同一種 EntityType,database 和 table 建議拆成兩次請求。
先寫入外部 database:
{
"Entities": [
{
"EntityType": "custom_bi-database",
"Name": "report_db",
"Comment": "外部 BI 報表庫",
"Attributes": {
"parentMetaEntityId": "custom_bi:custom_bi_src",
"technicalMetadata.location": "external://bi/report_db"
}
}
]
}再寫入外部 table,並通過 columns 一起註冊欄位:
{
"Entities": [
{
"EntityType": "custom_bi-table",
"Name": "sales_report",
"Comment": "銷售報告結果表",
"Attributes": {
"parentMetaEntityId": "custom_bi-database:custom_bi_src::report_db",
"columns": "[{\"name\":\"order_id\",\"type\":\"STRING\"},{\"name\":\"amount\",\"type\":\"DECIMAL\"}]"
}
}
]
}欄位不需要單獨調用 BatchCreateMetaEntities 建立。上例寫入成功後,外部欄位可使用擴充欄位 ID 參與欄位血緣。擴充表和欄位的 ID 可以通過 ListTables、GetTable、ListColumns、GetColumn 查詢獲得;如果已經明確擴充實體 ID 規則,也可以按規則產生,例如 custom_bi-column:custom_bi_src::report_db::sales_report:order_id。
寫入成功後,響應中會返回實體 ID。後續註冊血緣、查詢詳情、更新實體時都使用該 ID。
步驟六:註冊血緣關係
使用 CreateLineageRelationship 註冊實體之間的上下遊關係。血緣方向始終是:上遊 SrcEntity -> 下遊 DstEntity。
樣本一:DataWorks 已採集表到自訂 API。
{
"SrcEntity": {
"Id": "mysql-table:rm-xxx::sales_db::orders",
"Name": "orders"
},
"DstEntity": {
"Id": "custom_entity-customer_api:api_001",
"Name": "api_001"
},
"Task": {
"Id": "table_to_api_001",
"Type": "custom-lineage-task",
"Attributes": {
"scene": "table_to_api"
}
}
}樣本二:DataWorks 已採集表到外部擴充表。
{
"SrcEntity": {
"Id": "maxcompute-table:::prod_project:default:dwd_order",
"Name": "dwd_order"
},
"DstEntity": {
"Id": "custom_bi-table:custom_bi_src::report_db::sales_report",
"Name": "sales_report"
},
"Task": {
"Id": "dwd_order_to_sales_report",
"Type": "custom-table-lineage",
"Attributes": {
"scene": "external_bi_table"
}
}
}樣本三:DataWorks 已採集欄位到外部擴充欄位。
{
"SrcEntity": {
"Id": "maxcompute-column:::prod_project:default:dwd_order:order_id",
"Name": "order_id"
},
"DstEntity": {
"Id": "custom_bi-column:custom_bi_src::report_db::sales_report:order_id",
"Name": "order_id"
},
"Task": {
"Id": "dwd_order_id_to_report_order_id",
"Type": "custom-column-lineage",
"Attributes": {
"scene": "external_bi_column"
}
}
}對於欄位級血緣,SrcEntity 和 DstEntity 使用欄位實體 ID。欄位 ID 可以來自 ListColumns 或 GetColumn,包括 DataWorks 已採集欄位和登入擴充表欄位。具體請求參數和返回欄位請參見 CreateLineageRelationship API 詳情頁。
步驟七:驗證註冊結果並消費血緣
註冊完成後,建議做兩類驗證。
驗證實體是否寫入成功:
自訂實體:調用
GetMetaEntity。擴充表:調用
GetTable。擴充欄位:調用
GetColumn。DataWorks 已採集對象:調用對應的 Get API。
實體建立成功後,查詢側可能存在約 1 秒的短暫可見度延遲。如果剛寫入後沒有立即查詢到,可短暫等待後重試。
驗證血緣是否寫入成功:
查詢下遊:調用
ListLineages並傳入SrcEntityId。查詢上遊:調用
ListLineages並傳入DstEntityId。需要關係詳情時,設定
NeedAttachRelationship=true。
血緣註冊後可能存在短暫索引延遲。如果剛寫入後沒有立即查詢到,可等待數秒後重試。
完成基礎驗證後,可以繼續把血緣資料用於日常分析:
上遊溯源:從目標實體出發,調用
ListLineages查詢上遊,並按上遊節點繼續遞迴查詢,用於回答"這個報表或表的資料來自哪裡"。下遊影響分析:從變更實體出發,調用
ListLineages查詢下遊,並統計下遊實體類型和數量,用於回答"這張表或這個欄位變更會影響哪些報表、API 或外部表格"。斷鏈檢測:將大量註冊的實體清單與血緣端點清單做比對,找出沒有任何上遊或下遊的孤立實體,作為待補錄或待清理對象。
血緣健康巡檢:定期檢查實體是否存在、血緣關係是否重複、血緣端點是否可查詢、批量寫入是否有失敗項,形成巡檢報表。
多層級影響分析的實現思路如下:
queue = [changedEntityId]
visited = set()
while queue is not empty:
current = queue.pop()
downstream = ListLineages(SrcEntityId=current)
for entity in downstream:
if entity.id not in visited:
visited.add(entity.id)
queue.push(entity.id)
/* 輸出 visited 中的實體類型、實體名稱和影響路徑。 */建議在批量接入工程中儲存每次註冊成功的實體 ID 和血緣關係 ID。後續做影響分析、斷鏈檢測或累加式更新時,可以直接基於這份結果表和查詢 API 做比對,不需要重新解析原始業務系統資料。
大量註冊工程方案
當接入規模從幾個對象擴大到幾百個報表、上千張外部表格或上萬條血緣關係時,建議把註冊過程設計成可重複運行、可斷點續跑、可審計的批處理任務。
推薦寫入順序
批量接入時,建議按照以下順序執行:
建立或確認實體定義:調用
CreateMetaEntityDef。建立或確認自訂屬性:調用
CreateCustomAttribute。寫入擴充 Database:調用
BatchCreateMetaEntities。寫入擴充 Table,並隨 Table 寫入 Column。
寫入純自訂實體,例如報表、API、指標。
註冊血緣關係:調用
CreateLineageRelationship。查詢驗證並產生接入結果報告。
批次、重試和斷點續跑
BatchCreateMetaEntities 是實體批量寫入介面,單次請求最多寫入 5 個實體,並且同一批次必須是同一種 EntityType。實際工程中建議先按 EntityType 分組,再按 5 條一批切分。
CreateLineageRelationship 按單條血緣關係寫入。血緣量較大時,可以控制並發度分批調用,並對限流或臨時服務異常做退避重試。
建議採用以下錯誤處理模式:
for batch in splitByEntityType(rows, size=5):
resp = BatchCreateMetaEntities(batch)
if resp.Success is not true:
write_batch_failed(batch, resp.RequestId, resp.Message)
continue
for result in resp.Results:
if result.Success is true:
mark_done(result.Id)
else:
write_row_failed(result.Id, result.ErrorMessage, resp.RequestId)這裡需要同時判斷兩層成功狀態:最外層 Success 表示請求是否被服務端成功處理,Results[].Success 表示每個實體物件是否寫入成功。批量工程不要因為最外層 Success=true 就認為所有實體都已成功。
斷點續跑建議:
為實體和血緣使用穩定的業務 ID,避免每次運行產生不同標識。
每批寫入後記錄成功項、失敗項、
RequestId、失敗原因和原始行號。重跑時跳過已成功項,只重試失敗項或變更項。
運行前可通過
GetMetaEntity、GetCustomAttribute、ListLineageRelationships等查詢介面確認目標對象是否已經存在。4xx 類資料錯誤通常需要修正清單後再重試;限流、網路抖動、臨時服務異常可以做有限次數退避重試。
對於客戶側每日同步任務,推薦保留"期望狀態表"和"上次成功寫入結果表"。增量同步處理時只處理新增和變更對象;定期全量比對時,用期望狀態表與 DataWorks 查詢結果做差異檢查,發現缺失、重複或廢棄關係後再補錄或清理。
頁面效果
Demo 註冊完成後,您可以在 DataWorks 資料地圖中看到以下效果。實際頁面展示會受帳號許可權、地區、資料地圖頁面版本和註冊資料內容影響。
自訂實體可被搜尋和篩選
註冊自訂實體定義和實體物件後,可以在資料地圖中搜尋到對應實體。若實體定義中的屬性開啟了搜尋篩選能力,頁面左側會展示相應篩選項(例如按業務域、來源系統、負責人過濾報表實體)。
自訂實體詳情展示屬性資訊
進入自訂實體詳情頁後,可以查看實體基礎資訊、普通屬性和自訂屬性。例如 BI 報表實體詳情頁會展示報表編碼、業務域、負責人以及來源系統等業務欄位。
自訂屬性可統一管理
通過 CreateCustomAttribute 建立的自訂屬性會進入資料地圖的自訂屬性管理頁,可查看屬性名稱、類型、適用實體類型等資訊,便於統一維護和複用。
擴充表實體進入表資產搜尋
註冊擴充 Database、Table 和 Column 後,外部庫表可以進入資料地圖的表資產視圖,並按資料來源類型、資料庫等條件檢索,與 DataWorks 已採集表統一呈現。
表級和欄位級血緣在血緣圖中展示
註冊"表到自訂實體"、"表到表"、"欄位到欄位"血緣後,可以在資料地圖的血緣頁面查看上下遊關係。例如同一張銷售匯總表可以同時關聯到自訂報表實體和擴充表實體,並向下展示欄位級血緣。
Java SDK 範例
下面提供一個可直接編譯啟動並執行 Java Demo,同時覆蓋:
註冊 BI 報表自訂實體,並建立 DataWorks 已採集表到報表的血緣。
註冊外部擴充表,並建立表到表、欄位到欄位血緣。
Demo 中的業務標識集中放在類頂部常量裡。實際接入時,請替換為您帳號下真實的上遊表 ID、欄位 ID、報表名稱、外部庫表名稱等。首次運行前,也請確認樣本中的實體定義、自訂屬性和實體物件在帳號下尚未存在,或改成企業內部唯一的業務標識。
建議先通過 ListTables 或 GetTable 擷取真實上遊表 ID,通過 ListColumns 或 GetColumn 擷取真實上遊欄位 ID,再運行 Demo。
1. 準備工程
工程目錄:
custom-metadata-lineage-demo
├── pom.xml
└── src
└── main
└── java
└── CustomMetadataLineageDemo.javapom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>custom-metadata-lineage-demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dataworks_public20240518</artifactId>
<version>${dataworks.sdk.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mainClass>CustomMetadataLineageDemo</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>2. 編寫 Demo
將以下代碼儲存為 src/main/java/CustomMetadataLineageDemo.java。
import com.aliyun.dataworks_public20240518.Client;
import com.aliyun.dataworks_public20240518.models.BatchCreateMetaEntitiesRequest;
import com.aliyun.dataworks_public20240518.models.BatchCreateMetaEntitiesResponse;
import com.aliyun.dataworks_public20240518.models.CreateCustomAttributeRequest;
import com.aliyun.dataworks_public20240518.models.CreateLineageRelationshipRequest;
import com.aliyun.dataworks_public20240518.models.CreateMetaEntityDefRequest;
import com.aliyun.dataworks_public20240518.models.GetCustomAttributeRequest;
import com.aliyun.dataworks_public20240518.models.GetCustomAttributeResponse;
import com.aliyun.dataworks_public20240518.models.GetMetaEntityDefRequest;
import com.aliyun.dataworks_public20240518.models.GetMetaEntityDefResponse;
import com.aliyun.dataworks_public20240518.models.LineageEntity;
import com.aliyun.dataworks_public20240518.models.LineageTask;
import com.aliyun.dataworks_public20240518.models.MetaEntityAttributeDef;
import com.aliyun.dataworks_public20240518.models.MetaEntityWriteResult;
import com.aliyun.teaopenapi.models.Config;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CustomMetadataLineageDemo {
private static final String REGION_ID = "cn-shanghai";
// DataWorks 已採集表 ID。請通過 ListTables/GetTable 查詢後替換。
private static final String UPSTREAM_TABLE_ID = "maxcompute-table:::prod_project:default:dwd_sales_order";
private static final String UPSTREAM_TABLE_NAME = "dwd_sales_order";
// BI 報表示例。
private static final String REPORT_ENTITY_TYPE = "custom_entity-bi_report";
private static final String REPORT_ENTITY_NAME = "daily_sales_report";
// 外部擴充表示例。
private static final String EXT_PREFIX = "warehouse";
private static final String EXT_SOURCE = "custom_external_bi_src";
private static final String EXT_DATABASE = "analytics_db";
private static final String EXT_TABLE = "ads_sales_summary";
private static final String EXT_TABLE_ID =
"custom_" + EXT_PREFIX + "-table:" + EXT_SOURCE + "::" + EXT_DATABASE + "::" + EXT_TABLE;
private static final String EXT_ORDER_ID_COLUMN_ID =
"custom_" + EXT_PREFIX + "-column:" + EXT_SOURCE + "::" + EXT_DATABASE + "::" + EXT_TABLE + ":order_id";
private static final String EXT_AMOUNT_COLUMN_ID =
"custom_" + EXT_PREFIX + "-column:" + EXT_SOURCE + "::" + EXT_DATABASE + "::" + EXT_TABLE + ":amount";
public static void main(String[] args) throws Exception {
Client client = new Client(new Config()
.setAccessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"))
.setAccessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"))
.setRegionId(REGION_ID));
if (System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID") == null
|| System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET") == null) {
throw new IllegalArgumentException(
"請先設定 ALIBABA_CLOUD_ACCESS_KEY_ID 和 ALIBABA_CLOUD_ACCESS_KEY_SECRET");
}
String reportId = registerBiReport(client);
registerExternalTable(client);
createLineage(client, UPSTREAM_TABLE_ID, UPSTREAM_TABLE_NAME,
reportId, REPORT_ENTITY_NAME, "dw_table_to_bi_report", "custom-table_to_report");
createLineage(client, UPSTREAM_TABLE_ID, UPSTREAM_TABLE_NAME,
EXT_TABLE_ID, EXT_TABLE, "dw_table_to_external_table", "custom-table_to_table");
createLineage(client, EXT_ORDER_ID_COLUMN_ID, "order_id",
EXT_AMOUNT_COLUMN_ID, "amount", "ext_col_to_ext_col", "custom-column_to_column");
System.out.println("自訂實體、擴充表和血緣註冊完成。");
}
private static String registerBiReport(Client client) throws Exception {
ensureMetaEntityDef(client, REPORT_ENTITY_TYPE, new CreateMetaEntityDefRequest()
.setName("bi_report")
.setDisplayName("BI 報表")
.setDescription("企業 BI 報表資產")
.setExtend("NONE")
.setAttributeDefs(Arrays.asList(
new MetaEntityAttributeDef()
.setName("reportCode").setDisplayName("報表編碼").setType("STRING").setIsOptional(false),
new MetaEntityAttributeDef()
.setName("reportUrl").setDisplayName("報表地址").setType("STRING").setIsOptional(true)
)));
ensureCustomAttribute(client, "custom-attribute:sourceSystem", new CreateCustomAttributeRequest()
.setId("custom-attribute:sourceSystem")
.setDisplayName("來源系統")
.setType("ENUM")
.setValueEnums(Arrays.asList("BI", "CRM", "ERP"))
.setEntityTypes(Collections.singletonList(REPORT_ENTITY_TYPE))
.setDisplayEnabled(true)
.setSearchFilterEnabled(true));
Map<String, String> attrs = new HashMap<String, String>();
attrs.put("reportCode", REPORT_ENTITY_NAME);
attrs.put("reportUrl", "https://bi.example.com/reports/daily_sales");
Map<String, List<String>> customAttrs = new HashMap<String, List<String>>();
customAttrs.put("sourceSystem", Collections.singletonList("BI"));
String reportId = createOneEntity(client,
new BatchCreateMetaEntitiesRequest.BatchCreateMetaEntitiesRequestEntities()
.setEntityType(REPORT_ENTITY_TYPE)
.setName(REPORT_ENTITY_NAME)
.setComment("每日銷售報告")
.setAttributes(attrs)
.setCustomAttributes(customAttrs));
Thread.sleep(1000L); // 實體建立成功後,查詢側可能存在約 1 秒可見度延遲。
return reportId;
}
private static void registerExternalTable(Client client) throws Exception {
ensureMetaEntityDef(client, "custom_" + EXT_PREFIX + "-table", new CreateMetaEntityDefRequest()
.setName(EXT_PREFIX + "-table")
.setDisplayName("外部數倉表")
.setDescription("DataWorks 未自動採集的外部庫表")
.setExtend("TABLE"));
Map<String, String> dbAttrs = new HashMap<String, String>();
dbAttrs.put("parentMetaEntityId", "custom_" + EXT_PREFIX + ":" + EXT_SOURCE);
dbAttrs.put("technicalMetadata.location", "external://" + EXT_DATABASE);
createOneEntity(client, new BatchCreateMetaEntitiesRequest.BatchCreateMetaEntitiesRequestEntities()
.setEntityType("custom_" + EXT_PREFIX + "-database")
.setName(EXT_DATABASE)
.setComment("外部分析庫")
.setAttributes(dbAttrs));
Map<String, String> tableAttrs = new HashMap<String, String>();
tableAttrs.put("parentMetaEntityId", "custom_" + EXT_PREFIX + "-database:" + EXT_SOURCE + "::" + EXT_DATABASE);
tableAttrs.put("tableType", "TABLE");
tableAttrs.put("columns", "["
+ "{\"name\":\"order_id\",\"type\":\"STRING\",\"comment\":\"訂單 ID\",\"position\":1},"
+ "{\"name\":\"amount\",\"type\":\"DECIMAL\",\"comment\":\"銷售金額\",\"position\":2}"
+ "]");
createOneEntity(client, new BatchCreateMetaEntitiesRequest.BatchCreateMetaEntitiesRequestEntities()
.setEntityType("custom_" + EXT_PREFIX + "-table")
.setName(EXT_TABLE)
.setComment("外部銷售匯總表")
.setAttributes(tableAttrs));
Thread.sleep(1000L);
}
private static String createOneEntity(
Client client,
BatchCreateMetaEntitiesRequest.BatchCreateMetaEntitiesRequestEntities entity) throws Exception {
BatchCreateMetaEntitiesResponse response = client.batchCreateMetaEntities(
new BatchCreateMetaEntitiesRequest().setEntities(Collections.singletonList(entity)));
if (response.getBody() == null || !Boolean.TRUE.equals(response.getBody().getSuccess())) {
throw new RuntimeException("BatchCreateMetaEntities 請求失敗");
}
MetaEntityWriteResult result = response.getBody().getResults().get(0);
if (!Boolean.TRUE.equals(result.getSuccess())) {
if (isAlreadyExists(result.getErrorMessage())) {
String existingId = buildExpectedEntityId(entity);
System.out.println("實體已存在,複用: " + existingId);
return existingId;
}
throw new RuntimeException("實體建立失敗: " + result.getErrorMessage());
}
System.out.println("實體建立成功: " + result.getId());
return result.getId();
}
private static String buildExpectedEntityId(
BatchCreateMetaEntitiesRequest.BatchCreateMetaEntitiesRequestEntities entity) {
String entityType = entity.getEntityType();
String name = entity.getName();
if (entityType.startsWith("custom_entity-")) {
return entityType + ":" + name;
}
Map<String, String> attributes = entity.getAttributes();
String parentMetaEntityId = attributes == null ? null : attributes.get("parentMetaEntityId");
if (entityType.endsWith("-database") && parentMetaEntityId != null) {
String metaSource = parentMetaEntityId.substring(parentMetaEntityId.indexOf(':') + 1);
return entityType + ":" + metaSource + "::" + name;
}
if (entityType.endsWith("-table") && parentMetaEntityId != null) {
String parent = parentMetaEntityId.substring(parentMetaEntityId.indexOf(':') + 1);
return entityType + ":" + parent + "::" + name;
}
throw new IllegalArgumentException("無法根據實體類型和屬性推導已存在實體 ID: " + entityType + "/" + name);
}
private static void createLineage(
Client client,
String srcId,
String srcName,
String dstId,
String dstName,
String taskId,
String taskType) throws Exception {
client.createLineageRelationship(new CreateLineageRelationshipRequest()
.setSrcEntity(new LineageEntity().setId(srcId).setName(srcName))
.setDstEntity(new LineageEntity().setId(dstId).setName(dstName))
.setTask(new LineageTask().setId(taskId).setType(taskType)));
System.out.println("血緣建立成功: " + srcId + " -> " + dstId);
}
private static void ensureMetaEntityDef(
Client client,
String entityType,
CreateMetaEntityDefRequest request) throws Exception {
if (metaEntityDefExists(client, entityType)) {
System.out.println("實體定義已存在,跳過建立: " + entityType);
return;
}
client.createMetaEntityDef(request);
}
private static boolean metaEntityDefExists(Client client, String entityType) throws Exception {
try {
GetMetaEntityDefResponse response = client.getMetaEntityDef(
new GetMetaEntityDefRequest().setEntityType(entityType));
return response.getBody() != null
&& Boolean.TRUE.equals(response.getBody().getSuccess())
&& response.getBody().getMetaEntityDef() != null;
} catch (Exception e) {
if (isNotFound(e)) {
return false;
}
throw e;
}
}
private static void ensureCustomAttribute(
Client client,
String id,
CreateCustomAttributeRequest request) throws Exception {
if (customAttributeExists(client, id)) {
System.out.println("自訂屬性已存在,跳過建立: " + id);
return;
}
client.createCustomAttribute(request);
}
private static boolean customAttributeExists(Client client, String id) throws Exception {
try {
GetCustomAttributeResponse response = client.getCustomAttribute(
new GetCustomAttributeRequest().setId(id));
return response.getBody() != null
&& Boolean.TRUE.equals(response.getBody().getSuccess())
&& response.getBody().getCustomAttribute() != null;
} catch (Exception e) {
if (isNotFound(e)) {
return false;
}
throw e;
}
}
private static boolean isNotFound(Exception e) {
String message = e.getMessage();
if (message == null) {
return false;
}
String lowerMessage = message.toLowerCase();
return lowerMessage.contains("not found")
|| lowerMessage.contains("not exist")
|| lowerMessage.contains("not_exists")
|| message.contains("不存在");
}
private static boolean isAlreadyExists(String message) {
if (message == null) {
return false;
}
String lowerMessage = message.toLowerCase();
return lowerMessage.contains("already exists") || lowerMessage.contains("already exist");
}
}3. 運行 Demo
運行前配置訪問憑證。範例程式碼從環境變數讀取 AccessKey,避免在代碼中明文寫入密鑰。
export ALIBABA_CLOUD_ACCESS_KEY_ID="your-access-key-id"
export ALIBABA_CLOUD_ACCESS_KEY_SECRET="your-access-key-secret"執行:
export DATAWORKS_SDK_VERSION="latest-version"
export JAVA_VERSION="8"
mvn -Ddataworks.sdk.version="${DATAWORKS_SDK_VERSION}" \
-Djava.version="${JAVA_VERSION}" \
compile exec:java說明:
Demo 按建立流程編寫。運行前,請將類頂部的實體類型、實體名稱、外部庫表名稱等業務標識改成您帳號下尚未使用的唯一值。
BatchCreateMetaEntities需要同時檢查最外層Success和每條Results[].Success,樣本中的createOneEntity已包含這部分檢查。實體建立成功後,查詢側可能有約 1 秒可見度延遲。樣本在建立後加入了短暫等待,實際生產接入可改成有限重試。
4. 批量接入核心程式碼片段
上面的 Demo 用於快速跑通端到端流程。生產環境批量接入時,建議參考"資料準備模板"準備實體物件清單和血緣關係清單,再分別轉換成 List<EntityRow> 和 List<LineageRow> 作為批量方法入參。
其中,EntityRow 對應實體物件清單,LineageRow 對應血緣關係清單。如果清單來自 CSV、Excel 或資料庫,可以先把 attributes_json、custom_attributes_json、task_attributes_json 解析成 Map 後賦值給對應 Row 對象。下面樣本是可嵌入前面 Demo 工程的核心程式碼片段,不是完整 CSV/Excel 讀取器;檔案讀取和 JSON 解析可以按企業內部工程規範實現。
如果將以下代碼放入上面的 CustomMetadataLineageDemo 類中,需要額外引入:
import java.util.ArrayList;
import java.util.LinkedHashMap;大量建立實體:
private static BatchWriteResult batchCreateEntities(Client client, List<EntityRow> entityRows) throws Exception {
BatchWriteResult report = new BatchWriteResult();
// BatchCreateMetaEntities 要求同一批次為同一種 EntityType,因此先按 EntityType 分組。
Map<String, List<EntityRow>> grouped = new LinkedHashMap<String, List<EntityRow>>();
for (EntityRow row : entityRows) {
if (!grouped.containsKey(row.entityType)) {
grouped.put(row.entityType, new ArrayList<EntityRow>());
}
grouped.get(row.entityType).add(row);
}
for (Map.Entry<String, List<EntityRow>> entry : grouped.entrySet()) {
List<EntityRow> sameTypeRows = entry.getValue();
for (int from = 0; from < sameTypeRows.size(); from += 5) {
int to = Math.min(from + 5, sameTypeRows.size());
List<EntityRow> rowBatch = sameTypeRows.subList(from, to);
List<BatchCreateMetaEntitiesRequest.BatchCreateMetaEntitiesRequestEntities> requestEntities =
new ArrayList<BatchCreateMetaEntitiesRequest.BatchCreateMetaEntitiesRequestEntities>();
for (EntityRow row : rowBatch) {
requestEntities.add(row.toRequestEntity());
}
BatchCreateMetaEntitiesResponse response = client.batchCreateMetaEntities(
new BatchCreateMetaEntitiesRequest().setEntities(requestEntities));
if (response.getBody() == null || !Boolean.TRUE.equals(response.getBody().getSuccess())) {
for (EntityRow row : rowBatch) {
report.failed.add("entity=" + row.name + ", reason=batch request failed");
}
continue;
}
List<MetaEntityWriteResult> results = response.getBody().getResults();
if (results == null || results.size() != rowBatch.size()) {
for (EntityRow row : rowBatch) {
report.failed.add("entity=" + row.name + ", reason=unexpected batch result");
}
continue;
}
for (int i = 0; i < results.size(); i++) {
MetaEntityWriteResult result = results.get(i);
if (Boolean.TRUE.equals(result.getSuccess())) {
report.successIds.add(result.getId());
} else {
EntityRow row = rowBatch.get(i);
report.failed.add("entity=" + row.name + ", reason=" + result.getErrorMessage());
}
}
}
}
return report;
}大量註冊血緣關係:
private static BatchWriteResult batchCreateLineages(Client client, List<LineageRow> lineages) {
BatchWriteResult report = new BatchWriteResult();
for (LineageRow row : lineages) {
boolean success = false;
String lastError = null;
for (int attempt = 1; attempt <= 3; attempt++) {
try {
LineageTask task = new LineageTask().setId(row.taskId).setType(row.taskType);
if (row.taskAttributes != null && !row.taskAttributes.isEmpty()) {
task.setAttributes(row.taskAttributes);
}
client.createLineageRelationship(new CreateLineageRelationshipRequest()
.setSrcEntity(new LineageEntity().setId(row.srcId).setName(row.srcName))
.setDstEntity(new LineageEntity().setId(row.dstId).setName(row.dstName))
.setTask(task));
report.successIds.add(row.srcId + " -> " + row.dstId);
success = true;
break;
} catch (Exception e) {
lastError = e.getMessage();
try {
Thread.sleep(1000L * attempt);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
lastError = interrupted.getMessage();
break;
}
}
}
if (!success) {
report.failed.add("lineage=" + row.srcId + " -> " + row.dstId + ", reason=" + lastError);
}
}
return report;
}輔助結構:
private static class EntityRow {
// 對應實體物件清單中的 entity_type、name、comment。
private String entityType;
private String name;
private String comment;
// 對應 attributes_json、custom_attributes_json。
private Map<String, String> attributes = new HashMap<String, String>();
private Map<String, List<String>> customAttributes = new HashMap<String, List<String>>();
// 對應 parent_entity_id、columns_json,主要用於擴充 Database/Table。
private String parentEntityId;
private String columnsJson;
private BatchCreateMetaEntitiesRequest.BatchCreateMetaEntitiesRequestEntities toRequestEntity() {
Map<String, String> mergedAttributes = new HashMap<String, String>();
if (attributes != null) {
mergedAttributes.putAll(attributes);
}
if (parentEntityId != null && parentEntityId.length() > 0) {
mergedAttributes.put("parentMetaEntityId", parentEntityId);
}
if (columnsJson != null && columnsJson.length() > 0) {
mergedAttributes.put("columns", columnsJson);
}
return new BatchCreateMetaEntitiesRequest.BatchCreateMetaEntitiesRequestEntities()
.setEntityType(entityType)
.setName(name)
.setComment(comment)
.setAttributes(mergedAttributes)
.setCustomAttributes(customAttributes);
}
}
private static class BatchWriteResult {
private final List<String> successIds = new ArrayList<String>();
private final List<String> failed = new ArrayList<String>();
}
private static class LineageRow {
private String srcId;
private String srcName;
private String dstId;
private String dstName;
private String taskId;
private String taskType;
private Map<String, String> taskAttributes = new HashMap<String, String>();
}實際工程中,可以把 BatchWriteResult.failed 寫入檔案、資料庫或訊息佇列,作為斷點續跑和人工修正的資料來源。對於失敗項,不建議無差別無限重試;參數錯誤、實體類型不存在、血緣端點不存在等問題需要先修正資料清單,再重新執行。
推薦實踐
優先註冊為 DataWorks 自訂實體物件
非納管 custom-{Type}:{Identifier} 對象可以直接參与血緣註冊,但這種方式不利於後續管理。新接入情境建議:
使用
CreateMetaEntityDef建立實體定義。使用
BatchCreateMetaEntities建立實體物件。使用返回的實體 ID 註冊血緣。
這樣可以在資料地圖中查看實體詳情、維護屬性、複用自訂屬性,並支援後續治理。
區分自訂實體和擴充表實體
如果對象是報表、API、指標、系統、資料產品等業務概念,建議註冊為自訂實體。如果對象具備明確的資料庫、表、欄位結構,且 DataWorks 未自動採集,建議註冊為擴充表實體。
註冊擴充表實體時,建議同時建立對應的擴充 Database 對象,再建立 Table 和內聯 Column。這樣可以保留外部資料源的庫表欄位層級,後續在資料地圖中查看資產時更接近真實資料結構。
請勿通過 BatchCreateMetaEntities 建立 MaxCompute、DLF、EMR 等 DataWorks 採集器管理的預置實體。
對於已支援的類型:
若未採集,請通過採集器將其納入 DataWorks 管理。
若已採集,請通過查詢 API 擷取其 ID 直接使用。
對於不支援的類型:
請使用擴充表實體進行建立。
通過 API 詳情頁確認參數
本文樣本用於說明 API 組合方式和接入流程。欄位必填性、命名規則、枚舉值、錯誤碼、SDK 代碼等詳細資料,請進入對應 API 詳情頁查看。
營運與更新
自訂實體和血緣關係接入後,建議把它作為一條持續同步鏈路維護,而不是一次性匯入任務。常見營運動作如下:
營運動作 | 推薦 API | 說明 |
更新實體定義展示名、說明或屬性定義 |
| 適合實體模型演化;修改前建議評估已有實體資料相容性 |
更新自訂屬性定義 |
| 適合調整展示、篩選、枚舉值等屬性配置 |
更新自訂實體或擴充表實體物件 |
| 適合更新實體說明、普通屬性、自訂屬性等資訊 |
更新表或欄位業務屬性 |
| 適合維護表、欄位上的 README 和自訂屬性 |
刪除廢棄血緣 |
| 可先通過 |
刪除廢棄實體定義 |
| 刪除前需要確認沒有仍在使用的實體和血緣關係,謹慎使用強制移除能力 |
血緣關係變化時,推薦先從客戶系統產生一份"期望血緣清單",再用 ListLineageRelationships 查詢 DataWorks 當前已有關係,對比後執行:
期望清單有、DataWorks 沒有:調用
CreateLineageRelationship補充。期望清單沒有、DataWorks 仍存在:調用
DeleteLineageRelationship清理。兩邊都存在但任務資訊變化:先刪除舊關係,再建立新關係。
如果客戶系統每天都有新增對象,建議使用增量同步處理;如果客戶系統存在大量歷史修訂或人工維護資料,建議定期執行全量比對,避免 DataWorks 中出現漂移資料。
常見問題
現象 | 可能原因 | 處理建議 |
實體類型不存在 | 還沒有建立實體定義,或建立後查詢側尚未可見 | 先調用 |
實體寫入請求成功但部分實體失敗 | 只判斷了最外層 | 逐條檢查 |
血緣端點不存在 | 上遊或下遊實體 ID 不正確,或剛建立的實體尚未可查 | 使用 |
欄位血緣註冊失敗 | 欄位 ID 手動拼接錯誤,或擴充表欄位未隨表註冊 | 已採集欄位優先使用 |
批量調用被限流 | 並發過高或短時間請求過多 | 降低並發度,對限流錯誤做指數退避重試 |
重跑後出現重複或舊血緣 | 沒有用穩定 ID 和期望狀態做比對 | 儲存成功結果表;重跑前查詢已有關係,跳過已存在關係或先刪除舊關係 |
自訂屬性在頁面不可篩選 | 屬性定義未開啟搜尋篩選,或適用實體類型不包含目標類型 | 檢查 |
需要排查批量寫入失敗 | 失敗資訊缺少可定位上下文 | 記錄失敗請求的 |
頁面或查詢介面短時間查不到剛寫入對象 | 查詢索引存在短暫延遲 | 等待 1 秒到數秒後重試;批量工程中使用有限重試而不是立即判定失敗 |
常見接入方案
方案一:把 BI 報表接入資料地圖
適合希望在資料地圖中查看報表依賴哪些表、某張表變更會影響哪些報表的情境。推薦流程:
使用
CreateMetaEntityDef建立報表實體定義。使用
CreateCustomAttribute建立業務域、負責人、來源系統等屬性。使用
BatchCreateMetaEntities批量寫入報表實體。使用
ListTables、ListColumns擷取 DataWorks 已採集表欄位 ID。使用
CreateLineageRelationship註冊表到報表、欄位到欄位血緣。使用
ListLineages驗證上下遊鏈路。
方案二:把外部資料庫表接入資料地圖
適合企業已有第三方資料庫或外部系統表,DataWorks 未自動採集,但希望納入統一資產視圖的情境。推薦流程:
使用
CreateMetaEntityDef建立擴充表定義。使用
BatchCreateMetaEntities先寫入外部資料庫,再寫入外部表格,欄位隨表內聯寫入。使用
CreateCustomAttribute補充來源系統、資料域等業務屬性。使用
CreateLineageRelationship建立 DataWorks 表與外部表格的血緣關係。使用
GetTable、GetColumn、ListLineages驗證結果。
方案三:補充 DataWorks 已採集資產之間的血緣
適合企業存在外部加工鏈路或歷史鏈路,希望補充到資料地圖中的情境。推薦流程:
使用
ListTables、ListColumns、ListDatasets等 API 擷取實體 ID。根據實際上下遊方向調用
CreateLineageRelationship。使用
ListLineages查詢驗證。
這種情境不需要建立自訂實體定義,也不需要寫入實體物件。