Syncing DingTalk Contacts Incrementally into MySQL: A Practical Guide for One Integration Strategy
What This Strategy Solves (Scenario and Value)
At one manufacturer, DingTalk is the single source of identity for all employees, while HR, workflow, and reporting systems still read from a self-managed MySQL table. Staff transfers, resignations, and name changes happen every day, yet approval flows routinely break because "the initiator is no longer with the company." The core problem is straightforward: changes in the DingTalk directory must land in MySQL the same day. This guide focuses on a single concrete action—"Fetch DingTalk Contacts — Modify"—which writes updated user fields back into MySQL by userid.
Data Flow and Field Mapping (Source → Middle Layer → Target)
The source side is the DingTalk open platform endpoint topapi/v2/user/get, invoked via POST. The input only needs one dimension—the target userid—with language fixed at zh_CN. The target side is MySQL's execute endpoint, which is essentially a parameterized UPDATE with a main_sql placeholder. The mapping between the two sides is:
| Business Meaning | DingTalk Field (Response) | MySQL Column | Notes |
|---|---|---|---|
| DingTalk user primary key | userid | userid | UPDATE WHERE clause; de-duplication key |
| Job number | job_number | job_number | Frequently changing field |
| Name | name | name | Same as above |
| Title | title | title | Nullable |
| Unified identity | unionid | unionid | Critical for cross-system linkage |
| Department list | dept_id_list | dept_id_list | Array type, stored as JSON string |
| Department leader flag | leader_in_dept | leader_in_dept | Drives approval node logic |
| Deletion flag | del_flag | del_flag | Soft delete via source field |
This strategy only does "write-back by userid"—no split between insert and delete. The UPDATE's WHERE clause must be strictly bound to userid, otherwise the entire table can be wiped out in one run. That is the first pitfall we revisit later.
How to Configure It on Qeasy
On the Qeasy data integration platform, this strategy is a typical "source pull + target execute" WebAPI sync. We break the configuration into four parts based on field experience:
-
Source configuration (DingTalk): Set
metadata.apitotopapi/v2/user/getandeffecttoQUERY. Theuseridparameter is injected as a variable from an upstream strategy or scheduler;languageis fixed atzh_CN;dep_strategypoints to the ID of the "Fetch DingTalk Departments" upstream strategy, declaring the dependency. Useuseridas theidfield and enableautoFillResponseso the response schema is auto-populated and you do not have to rebuild the model every time a new field appears. -
Target configuration (MySQL): Set
effecttoEXECUTEandmetadata.apitoexecute. Therequestblock only carries a singlemain_paramsobject as the input placeholder; the actual SQL lives inotherRequestundermain_sql, written with named placeholders like:useridand:name. KeepidCheckenabled so the platform automatically verifies that the sourceidmatches theuseridin the SQL WHERE clause, preventing mismatches. -
Scheduling: Set
crontabto22,52 23 * * *, meaning two runs per day at 23:22 and 23:52. Late-night timing is chosen because DingTalk directory churn is concentrated during business hours; the night window gives the longest writeback runway with the least contention. -
Dependencies and sequence: This strategy's
depends_onpoints to the "Fetch Departments" strategy—departments first, then people under them. In Qeasy, this is expressed bysequence: Btogether with the explicit dependency declaration.
Implementation Steps
We split the rollout into three stages, each with a clear exit criterion:
Stage 1: Build the incremental starting point. In Qeasy, wire the source-side userid parameter to the upstream strategy's output and get a single-user pull + writeback working. Exit criterion: pick any 3 real users and confirm updated_at plus name/title actually change in the database.
Stage 2: Batch triggering. Once the upstream "Departments" strategy is in place, switch this strategy's userid input to "batch-passed from upstream output," so the flow becomes "get the user list per department, then write back per userid." On Qeasy this is typically handled with the "header / body phased" pattern: upstream is the header (department), this strategy is the body (every user under it). Exit criterion: a full run of ~5,000 users finishes in minutes with no primary-key conflicts.
Stage 3: Tune the schedule. Move from "once per day" toward 22,52 23 * * *, watching DingTalk API rate limits and MySQL slow logs. On 429s, add retry and backoff on the Qeasy strategy itself; on row locks, stagger the two executions by 30 minutes rather than simply removing one of them.
Pitfalls We Hit in the Field
These are the recurring failure modes we have seen on customer sites:
- Pitfall #1: UPDATE without a WHERE clause. During the first deployment, the
main_sqlwas written asupdate hzero_platform.dingtalk_user set ...withoutwhere userid=:userid. One batch run rewrote the whole table into a single user. The safe approach: keep the named-placeholder WHERE clause in SQL and enableidCheckon the Qeasy side so the platform has your back. - Pitfall #2: Receiving an array into a VARCHAR column. DingTalk returns
dept_id_listas an array; concatenating it directly as a string breaks JSON parsing downstream. The safe approach: serialize it to a JSON string in the middle layer, and deserialize on read; an expression transform in Qeasy's field mapping handles it. - Pitfall #3: Schedule collisions. Running the "Departments" strategy and the "Users" strategy at the same time causes "users being written before their departments exist," producing null pointers. The safe approach: declare the dependency explicitly with
depends_on, instead of relying on human convention. - Pitfall #4: Scattered code mappings. DingTalk's
dept_iddoes not match the self-managed department ID in MySQL, so every team hardcodes the conversion inside SQL—painful to maintain. The safe approach: centralize the mapping in Qeasy's "mapping table," and let both source and target reference the same one; one change, everywhere. - Pitfall #5: Mixing full and incremental runs. During early rollout, "full backfill" and "incremental schedule" were enabled together, causing the same row to be written multiple times and exploding the logs. The safe approach: keep full and incremental as two separate strategies; run full only during initial bootstrap and disable it immediately after.
When This Strategy Applies — and When It Does Not
Applies: DingTalk is the single source of org structure, and the business side relies on a self-managed MySQL user table that needs precise userid-based writeback. Typical examples include approval workflows and reporting users staying aligned with HR.
Does not apply: Cases that need full replacement (not userid-keyed writeback), or where changes must be distributed as an event stream to multiple downstream systems. Those call for a "pull-all + master-data fan-out" pattern, not the single-row UPDATE strategy described here.