一、引入
接上一篇,已经解决了单向的数据传输问题,即:将 MySQL 的数据写入到多维表。
但是如果需求具有持续性,换句话说,每隔一段时间需要同步一次数据,今天需要同步数据,明天还需要同步数据,又该怎么办?
数据的同步中一般会有两种同步策略:
- 增量同步:只同步新增的数据;
- 全量同步:同步所有数据。
本文针对这两种同步策略展开探讨下如何持续同步数据。
二、策略之增量同步
增量同步主要解决两个问题,一个是怎么做增量,一个是补数据。
怎么做增量?
其实逻辑很简单,就是通过时间划分区间,下一个区间开始或者上一个去接结束后,同步上一个区间的数据,比如说今天同步昨天产生的数据。只要控制好时间范围就可以了。这是针对只插入不更新的表单而言的策略。
如果是事务表会根据时间的推移发生数据的变更,比如说订单状态由下单成功变成了退款成功,那么就会涉及到数据更新的逻辑,相应的,为了保证数据的一致性,如果使用到订单状态,则需要进行数据更新操作。
什么是补数据?
其实就是数据缺了需要补充,比如说你周末放假了,没有跑任务,到了周一再数据,这就是补数据。如果是通过调度工具跑任务,则更多的是任务的数据未来得及更新或者跑失败了,需要重新跑数,补全数据。通常也是修改同步的时间范围即可。
2.1 增量同步配置
在 SQL 代码中加一个“where”条件限制即可,假设 T+1 更新,参考设置如下,通过datetime
模块获取今天和昨天的日期,再通过自定义函数date_to_timestamp()
将日期转为时间戳,然后传递到 SQL 中,限制paid_time
的范围。
注意:user_orders
表的paid_time
是秒级时间戳,所以转化的时间戳也是秒级,下同。
import datetime
def date_to_timestamp(date_):
timestamp = int(datetime.datetime(date_.year, date_.month, date_.day).timestamp())
return timestamp
yesterday = datetime.datetime.now().date() - datetime.timedelta(days=1) # 昨天
today = datetime.datetime.now().date() # 今天
yesterday_timestamp = date_to_timestamp(yesterday)
today_timestamp = date_to_timestamp(today)
sql = f'''
select uo.user_id as "用户ID"
,u.nickname as "昵称"
,(case u.sex when 0 then '女' when 1 then '男' else '未知' end) as "性别"
,u.mobile as "手机号"
,u.city as "城市"
,uo.id as "订单号"
,uo.paid_time*1000 as "下单时间"
,uo.amount/100 as "下单金额"
from my_datas.user_orders uo
join my_datas.users u on u.id=uo.user_id
where uo.production_id=10
and uo.paid_time>={yesterday_timestamp}
and uo.paid_time< {today_timestamp};
'''
如果涉及数据更新该怎么办呢?
可以根据更新时间updated_at
进行判断,选取updated_at
时间在昨天的数据,然后和现有的 SQL 进行 union(去重合并),然后需要拉飞书多维表的数据进行匹配,将已存在的数据视为需要更新的数据,其他的则是新增的数据。
具体操作可参考下文全量同步的数据分类逻辑。
2.2 补数据
如果需要补数据,则传递日期的标准格式字符串,如:2024年1月1日,使用“2024-01-01”。
特别注意的是,存在一个日期边界的问题,结束日期需要加 1 天,即 86400 秒,否则会少记录结束日期当天的数据。
import datetime
def date_to_timestamp(date_str):
y_m_d = date_str.split('-')
timestamp = int(datetime.datetime(y_m_d[0], y_m_d[1], y_m_d[2]).timestamp())
return timestamp
start_date = '2024-01-01' # 开始日期
end_date = '2024-01-03' # 结束日期
start_timestamp = date_to_timestamp(start_date)
end_timestamp = date_to_timestamp(end_date) + 86400 # 需要加 1 天
sql = f'''
select uo.user_id as "用户ID"
,u.nickname as "昵称"
,(case u.sex when 0 then '女' when 1 then '男' else '未知' end) as "性别"
,u.mobile as "手机号"
,u.city as "城市"
,uo.id as "订单号"
,uo.paid_time*1000 as "下单时间"
,uo.amount/100 as "下单金额"
from my_datas.user_orders uo
join my_datas.users u on u.id=uo.user_id
where uo.production_id=10
and uo.paid_time>={start_timestamp}
and uo.paid_time< {end_timestamp};
'''
三、策略之全量同步
全量同步一般有两种方式,一种是删除旧数据重新插入所有数据,一种是更新旧数据+插入新数据+删除异常数据。
- 前者会清除所有历史的数据,如果业务人员在多维表上仅读数据,不编辑数据,比如说新增列记录是否已经回访用户,用户的反馈是什么等,可以采用,但是如果有做了一些数据标记,则行不通;
- 后者则支持业务人员共创协作,针对历史数据做更新,对新增的数据直接插入,当然对于异常的数据,比如说业务人员自行新增的行,需要视具体情况进行处理,或标记或删除。
3.1 先删除后全量新增
这种方式比较简单粗暴,主要是在上一篇的基础上加上一个删除历史数据的逻辑。先删除历史数据,删完之后直接跑下上一篇的代码即可完成全量数据新增。
删除历史数据的逻辑也相对简单,直接读取多维表全部数据,然后获取所有的“record_id”,按每份 500 个进行切分,然后循环传递给飞书多维表删除多条记录的 API 接口进行删除即可。
读取多维表全部数据,这个在《飞书API(3):Python 自动读取多维表所有分页数据的三种方法》中已经介绍过,可以拿来即用;调接口删除数据在《飞书 API 2-4:如何使用 API 将数据写入数据表》中也有涉及到。所以该方式最终只需要补充一个函数,用于处理“record_id”,把“record_id”提取出来并切分即可。
注意:以下是一个伪代码,不能直接执行。
# 读取代码:函数为xxx
def 读取多维表函数():
"""《飞书API(3):Python 自动读取多维表所有分页数据的三种方法》代码"""
feishu_datas = [{"fields":{},"record_id":"1"},{"fields":{},"record_id":"2"}]
return feishu_datas
def 读数据库写入多维表函数():
"""飞书API 2-5:如何将 MySQL 数据库的查询结果写入多维表(上)"""
pass
def delete_records(access_token,app_token,table_id,request_body):
"""《飞书 API 2-4:如何使用 API 将数据写入数据表》代码"""
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_delete"
payload = json.dumps(request_body)
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {access_token}'
}
response = requests.request("POST", url, headers=headers, data=payload)
code = response.json()['code']
if code == 0:
len_record = len(request_body["records"])
print(f"成功删除 {len_record} 数据。关联函数:delete_records。")
else:
msg = response.json().get("msg")
raise f"更新数据失败,失败信息:{msg}。关联函数:delete_records。"
# 新增的代码
def format_record_id(feishu_datas,change_size=500):
"""格式化 record_id:提取并切分"""
datas_field = [record['record_id'] for record in feishu_datas]
body_ls = []
for i in range(0,len(datas_field), change_size):
data_records = datas_field[i:i+change_size]
body_ls.append(data_records)
return body_ls
def main():
feishu_datas = 读取多维表函数()
record_id_ls = get_record_id(feishu_datas)
for request_body in body_ls:
delete_records(access_token,app_token,table_id,request_body)
读数据库写入多维表函数()
if __name__ == '__main__':
main()
3.2 更新旧数据+插入新数据
更新数据,需要获取每一行记录的“record_id”,而“record_id”和数据库查询到的数据,没有直接的关系,需要通过一个“桥梁”来把二者串联起来,这就需要使用到每一行记录的唯一值,上一篇的案例中,可以使用用户ID 或者订单 ID 来做这个桥梁。将数据关联起来之后,需要针对不同的数据进行处理,所以需要分类。如果数据超过了接口的最高限制 500 条记录,还需要考虑对分类数据进行分组。
所以整个流程可分为
- 先对数据进行格式化,主要是提取“外键”;
- 对格式化数据进行分类
- 对分类数据进行分组
- 遍历分组,调接口实现数据的变更
3.2.1 格式化:提取“外键”
分类之前还需要格式化。格式化分两步,一步是处理数据库数据,另外一步是处理飞书多维表的数据。
- 处理数据库数据:将数据库的数据处理为接口数据的格式,并提取出外键
- 处理飞书数据:提取外键和“record_id”。
前面在《飞书API 2-5》中也有介绍到将数据库读取到的数据格式为 API 所需要的请求头,如下图,这里把格式化和分组拆开,因为中间涉及一个分类的操作。
格式化数据库数据,将数据库数据以键值对的形式包括起来作为“fields”的值,并把外键“订单号”提取出来,最终转化为 DataFrame 返回,该 DataFrame 共有 2 列:“fields”和“foreign_key”。
# 新增格式化
def format_db_datas(df_db, foreign_key):
"""格式化数据库数据:整理为 API 所需格式,并将 外键 提取出来"""
# 处理为结构 [{foreign_key: <外键值>,'fields' : <所有明细数据>]
datas_ls = df_db.to_dict(orient='records')
datas_field = [{foreign_key: data[foreign_key],'fields' : data} for data in datas_ls]
# 转为DataFrame,以便下 Python SQL 处理
df_db = pd.DataFrame(datas_field)
print(f'成功提取【数据库】的关键字段:fields, {foreign_key}。方法:format_db_datas')
return df_db
格式化飞书多维表数据,由于飞书多维表不同的数据类型的数据格式不同,需要多传递一个数据类型的编号,以便对“foreign_key”进行格式化,此代码中,“foreign_key”仅支持较为常用的几种数据类型:文本、数字、单选、手机号、日期。另外,为了避免多维表是一个空表,加了一个判断,当为空表是生成一个空的 DataFrame。最终返回一个 DataFrame,包含 2 列:“record_id”和“foreign_key”。
def format_feishu_datas(feishu_datas, foreign_key, data_type=1):
"""格式化飞书数据:提取关键字段:外键 和 record_id"""
if feishu_datas == []:
#如果飞书多维表没有数据,返回无值的DataFrame
df_tb = pd.DataFrame(columns=['record_id', foreign_key])
else:
df_tb = pd.DataFrame(feishu_datas)
if data_type == 1: # 文本
df_tb[foreign_key] = df_tb['fields'].map(lambda x: x.get(foreign_key)[0]['text'] if x.get(foreign_key) else None)
elif data_type in (2, 3, 13, 5): # 数字、单选、手机号、日期
df_tb[foreign_key] = df_tb['fields'].map(lambda x: x.get(foreign_key) if x.get(foreign_key) else None)
else:
raise '暂不支持改类型!'
# 实际生产过程,可能新增行行为。
df_tb_ok = df_tb[~df_tb[foreign_key].isna()]
df_tb_ok = df_tb_ok[['record_id', foreign_key]]
invalid_num = df_tb[df_tb[foreign_key].isna()].shape[0]
print(f'成功提取【飞书表单】的关键字段:record_id, {foreign_key}。发现无效数据 {invalid_num} 条。方法:format_feishu_datas')
return df_tb_ok
3.2.2 分类与分组
由于涉及更新、插入或删除操作,需要将数据进行分类。基本逻辑是将从数据库读取到的数据进行分类,如果多维表已经有记录则进行更新,多维表没有的进行新增,多维表多出的进行删除或标记。
可以类比数据库的联结操作,分类逻辑伪代码参考如下:
-- 更新集合:取多维表的 record_id + 数据库的数据
select 多维表数据.record_id,数据库数据.*
from 数据库数据
join 多维表数据 on 多维表数据.订单ID=数据库数据.订单ID
-- 插入集合:取数据库多出部分
select 数据库数据.*
from 数据库数据
left join 多维表数据 on 多维表数据.订单ID=数据库数据.订单ID
where 多维表数据.订单ID is null
-- 删除或标记集合
select 多维表数据.record_id
from 数据库数据
right join 多维表数据 on 多维表数据.订单ID=数据库数据.订单ID
where 数据库数据.订单ID is null
特别注意:先分类再分组,即每个集合分完之后才做数据分组,按接口上限 500 条记录切分。
在 Python 中,可以通过merge()
方法来实现,当涉及“where”条件时,可以通过query()
方法来实现,由于 NaN 不等于 NaN,所以可以使用“record_id != record_id”或“fields != fields”筛选出该字段值为空的行。参考代码如下:
def classify_datas(df_from, df_join, on=None, left_on=None, right_on=None):
"""将两个[{},{}]或 DF 结构的数据分类。ls_from 为数据库数据,ls_join 为多维表数据"""
if on is not None:
df_cre = df_from.merge(df_join, how='left' , on=on).query('record_id != record_id')[['fields']] # 等同于'record_id.isnull()',因为 NaN != NaN
df_ups = df_from.merge(df_join, how='inner', on=on)[['record_id', 'fields']]
df_del = df_from.merge(df_join, how='right', on=on).query('fields != fields')[['record_id']] # 等同于'fields.isnull()',因为 NaN != NaN
else:
df_cre = df_from.merge(df_join, how='left' , left_on=left_on, right_on=right_on).query('record_id != record_id')[['fields']] # 等同于'record_id.isnull()',因为 NaN != NaN
df_ups = df_from.merge(df_join, how='inner', left_on=left_on, right_on=right_on)
df_del = df_from.merge(df_join, how='right', left_on=left_on, right_on=right_on).query('fields != fields')[['record_id']] # 等同于'fields.isnull()',因为 NaN != NaN
print('数据分类:新增数据集(df_cre)的数据量为 %s, 更新数据集(df_ups)的数据量为 %s, 删除数据集(df_ups)的数据量为 %s。方法:classify_datas' % (df_cre.shape[0], df_ups.shape[0], df_del.shape[0]))
return df_cre, df_ups, df_del
分好类之后,需要对数据进行分组,新增和更新接口的请求体外层结构的“records”的值是一个字典结构,而删除接口是一个列表结构,所以数据分组的时候需要分 2 种情况进行处理,分别是“dict”和“list”。
def cut_datas(df,mode='dict',page_size=500):
"""
按指定尺寸切分数据,并转为API要求格式
切割数据:接口数据上限500
mode: 模式,默认是字典格式 dict(插入/更新结构),即 DF.to_dict(orient='records')。还支持 list(删除结构),即 Series.to_list()
"""
df_ls = []
for i in range(0,df.shape[0], page_size):
if mode=='dict':
data_records = df.iloc[i:i+page_size].to_dict(orient='records')
elif mode=='list':
data_records = df.iloc[i:i+page_size].record_id.to_list()
data_to_table = {"records": data_records}
df_ls.append(data_to_table)
print('数据切割:切割数据集为 %s 份。方法:datas_processing.cut_dataset' % (len(df_ls)))
return df_ls
3.2.2 调接口处理数据
直接拿前面《飞书API 2-6》的代码复用即可。不同点在于此处需要遍历分组的列表,然后调用接口进行数据操作。
3.3.4 整合代码
整个流程包含获取飞书 token,再读取飞书多维表数据和读取数据库数据,然后对二者进行格式化,然后联结起来做分类和分组,然后调用飞书的 API 进行数据新增、更新或删除操作。
工作流程图参考如下:
读取飞书多维表数据可以参考《飞书API(7)》,读取数据库数据,参考《飞书API 2-5》,调接口进行增删改记录,参考《飞书 API 2-4》,新增部分主要是格式化、联结数据之后进行分类和分组。
具体参考代码如下:
import requests
import json
import datetime
import pandas as pd
from sqlalchemy import create_engine, text
from urllib.parse import urlparse, parse_qs
# 读取飞书多维表数据:参考《飞书API(7):MySQL 入库通用版本》
def get_table_params(bitable_url):
# bitable_url = "https://feishu.cn/base/aaaaaaaa?table=tblccc&view=vewddd"
parsed_url = urlparse(bitable_url) #解析url:(ParseResult(scheme='https', netloc='feishu.cn', path='/base/aaaaaaaa', params='', query='table=tblccc&view=vewddd', fragment='')
query_params = parse_qs(parsed_url.query) #解析url参数:{'table': ['tblccc'], 'view': ['vewddd']}
app_token = parsed_url.path.split('/')[-1]
table_id = query_params.get('table', [None])[0]
view_id = query_params.get('view', [None])[0]
print(f'成功解析链接,app_token:{app_token},table_id:{table_id},view_id:{view_id}。关联方法:get_table_params。')
return app_token, table_id, view_id
def get_tenant_access_token(app_id, app_secret):
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
payload = json.dumps({
"app_id": app_id,
"app_secret": app_secret
})
headers = {'Content-Type': 'application/json'}
response = requests.request("POST", url, headers=headers, data=payload)
tenant_access_token = response.json()['tenant_access_token']
print(f'成功获取tenant_access_token:{tenant_access_token}。关联函数:get_table_params。')
return tenant_access_token
def get_bitable_datas(tenant_access_token, app_token, table_id, view_id, page_token='', page_size=20):
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/search?page_size={page_size}&page_token={page_token}&user_id_type=user_id"
payload = json.dumps({"view_id": view_id})
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {tenant_access_token}'
}
response = requests.request("POST", url, headers=headers, data=payload)
print(f'成功获取page_token为【{page_token}】的数据。关联函数:get_bitable_datas。')
return response.json()
def get_all_bitable_datas(tenant_access_token, app_token, table_id, view_id, page_token='', page_size=20):
has_more = True
feishu_datas = []
while has_more:
response = get_bitable_datas(tenant_access_token, app_token, table_id, view_id, page_token, page_size)
if response['code'] == 0:
page_token = response['data'].get('page_token')
has_more = response['data'].get('has_more')
# print(response['data'].get('items'))
# print('\n--------------------------------------------------------------------\n')
feishu_datas.extend(response['data'].get('items'))
else:
raise Exception(response['msg'])
print(f'成功获取飞书多维表所有数据,返回 feishu_datas。关联函数:get_all_bitable_datas。')
return feishu_datas
# ------------------------------------------------------------------------------------------------------------------------
# 读取数据库数据,参考《飞书API 2-5:如何将 MySQL 数据库的查询结果写入多维表(上)》
def get_datas(sql, connect_info, fields_type):
# connect_info = 'mysql+pymysql://{}:{}@{}:{}/{}?charset=utf8'\
# .format("your_user_name", "your_password", "127.0.0.1", "3306","my_datas")
engine = create_engine(connect_info)
result = pd.read_sql(sql, engine)
result = result.astype(fields_type,errors='ignore')
# result.info()
return result
# def format_to_reqbody(df,change_size=500):
# df_dict = df.to_dict(orient='records')
# datas_field = [{'fields' : data} for data in df_dict]
# df_ls = []
# for i in range(0,len(datas_field), change_size):
# data_records = {"records": datas_field[i:i+change_size]}
# # data_to_table = json.dumps(data_records).replace(': NaN',': null')
# df_ls.append(data_records)
# print('数据格式化并切割:切割数据集为 %s 份。关联函数:format_to_reqbody' % (len(df_ls)))
# return df_ls
# ------------------------------------------------------------------------------------------------------------------------------
# 新增格式化、分类与分组
def format_db_datas(df_db, foreign_key):
"""格式化数据库数据:整理为 API 所需格式,并将 外键 提取出来"""
# 处理为结构 [{foreign_key: <外键值>,'fields' : <所有明细数据>]
datas_ls = df_db.to_dict(orient='records')
datas_field = [{foreign_key: data[foreign_key],'fields' : data} for data in datas_ls]
# 转为DataFrame,以便下 Python SQL 处理
df_db = pd.DataFrame(datas_field)
print(f'成功提取【数据库】的关键字段:fields, {foreign_key}。方法:format_db_datas')
return df_db
def format_feishu_datas(feishu_datas, foreign_key, data_type=1):
"""格式化飞书数据:提取关键字段:外键 和 record_id"""
if feishu_datas == []:
#如果飞书多维表没有数据,返回无值的DataFrame
df_tb = pd.DataFrame(columns=['record_id', foreign_key])
else:
df_tb = pd.DataFrame(feishu_datas)
if data_type == 1:
df_tb[foreign_key] = df_tb['fields'].map(lambda x: x.get(foreign_key)[0]['text'] if x.get(foreign_key) else None)
elif data_type in (2, 3, 13, 5): #数字、单选、手机号、日期
df_tb[foreign_key] = df_tb['fields'].map(lambda x: x.get(foreign_key) if x.get(foreign_key) else None)
else:
raise '暂不支持改类型!'
# 实际生产过程,可能新增行行为。
df_tb_ok = df_tb[~df_tb[foreign_key].isna()]
df_tb_ok = df_tb_ok[['record_id', foreign_key]]
invalid_num = df_tb[df_tb[foreign_key].isna()].shape[0]
print(f'成功提取【飞书表单】的关键字段:record_id, {foreign_key}。发现无效数据 {invalid_num} 条。方法:format_feishu_datas')
return df_tb_ok
def classify_datas(df_from, df_join, on=None, left_on=None, right_on=None):
"""将两个[{},{}]或 DF 结构的数据分类。ls_from 为数据库数据,ls_join 为多维表数据"""
if on is not None:
df_cre = df_from.merge(df_join, how='left' , on=on).query('record_id != record_id')[['fields']] # 等同于'record_id.isnull()',因为 NaN != NaN
df_ups = df_from.merge(df_join, how='inner', on=on)[['record_id', 'fields']]
df_del = df_from.merge(df_join, how='right', on=on).query('fields != fields')[['record_id']] # 等同于'fields.isnull()',因为 NaN != NaN
else:
df_cre = df_from.merge(df_join, how='left' , left_on=left_on, right_on=right_on).query('record_id != record_id')[['fields']] # 等同于'record_id.isnull()',因为 NaN != NaN
df_ups = df_from.merge(df_join, how='inner', left_on=left_on, right_on=right_on)
df_del = df_from.merge(df_join, how='right', left_on=left_on, right_on=right_on).query('fields != fields')[['record_id']] # 等同于'fields.isnull()',因为 NaN != NaN
print('数据分类:新增数据集(df_cre)的数据量为 %s, 更新数据集(df_ups)的数据量为 %s, 删除数据集(df_ups)的数据量为 %s。方法:classify_datas' % (df_cre.shape[0], df_ups.shape[0], df_del.shape[0]))
return df_cre, df_ups, df_del
def cut_datas(df,mode='dict',page_size=500):
"""
按指定尺寸切分数据,并转为API要求格式
切割数据:接口数据上限500
mode: 模式,默认是字典格式 dict(插入/更新结构),即 DF.to_dict(orient='records')。还支持 list(删除结构),即 Series.to_list()
"""
df_ls = []
for i in range(0,df.shape[0], page_size):
if mode=='dict':
data_records = df.iloc[i:i+page_size].to_dict(orient='records')
elif mode=='list':
data_records = df.iloc[i:i+page_size].record_id.to_list()
#插入和更新都使用该结构
# data_to_table = json.dumps({"records": data_records}).replace('NaN','null')
# data_to_table = json.dumps({"records": data_records}).replace(': NaN',': null') # 使用re也可以,不过大材小用了。
data_to_table = {"records": data_records}
# _data_to_table = json.dumps({"records": data_records})
# data_to_table = re.sub(r': NaN', ': null', _data_to_table)
df_ls.append(data_to_table)
print('数据切割:切割数据集为 %s 份。方法:datas_processing.cut_dataset' % (len(df_ls)))
return df_ls
# -----------------------------------------------------------------------------------------------------------------------
# 调接口进行增删改记录,参考《飞书 API 2-4:如何使用 API 将数据写入数据表》
def insert_records(access_token,app_token,table_id,request_body):
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_create"
payload = json.dumps(request_body).replace(': NaN',': null')
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {access_token}'
}
response = requests.request("POST", url, headers=headers, data=payload)
code = response.json()['code']
if code == 0:
len_record = len(request_body["records"])
print(f"成功插入 {len_record} 数据。关联函数:insert_records。")
else:
msg = response.json().get("msg")
raise f"插入数据失败,失败信息:{msg}。关联函数:insert_records。"
def update_records(access_token,app_token,table_id,request_body):
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_update"
payload = json.dumps(request_body).replace(': NaN',': null')
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {access_token}'
}
response = requests.request("POST", url, headers=headers, data=payload)
code = response.json()['code']
if code == 0:
len_record = len(request_body["records"])
print(f"成功更新 {len_record} 条数据。关联函数:update_records。")
else:
msg = response.json().get("msg")
raise f"更新数据失败,失败信息:{msg}。关联函数:update_records。"
def delete_records(access_token,app_token,table_id,request_body):
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records/batch_delete"
payload = json.dumps(request_body)
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {access_token}'
}
response = requests.request("POST", url, headers=headers, data=payload)
code = response.json()['code']
if code == 0:
len_record = len(request_body["records"])
print(f"成功删除 {len_record} 数据。关联函数:delete_records。")
else:
msg = response.json().get("msg")
raise f"更新数据失败,失败信息:{msg}。关联函数:delete_records。"
def get_tenant_access_token(app_id, app_secret):
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
payload = json.dumps({
"app_id": app_id,
"app_secret": app_secret
})
headers = {'Content-Type': 'application/json'}
response = requests.request("POST", url, headers=headers, data=payload)
tenant_access_token = response.json()['tenant_access_token']
print(f'成功获取tenant_access_token:{tenant_access_token}。关联函数:get_table_params。')
return tenant_access_token
def main(bitable_url, connect_info, sql, fields_type, foreign_key, data_type=1, page_size=500):
# 基本配置
app_token, table_id, view_id = get_table_params(bitable_url)
app_id = 'your_app_id'
app_secret = 'your_app_secret'
access_token = get_tenant_access_token(app_id, app_secret)
# 读取数据库数据并格式化
df_db = get_datas(sql, connect_info, fields_type)
df_from = format_db_datas(df_db, foreign_key)
# 读取多维表数据并格式化
feishu_datas = get_all_bitable_datas(access_token, app_token, table_id, view_id, page_size=page_size)
df_join = format_feishu_datas(feishu_datas, foreign_key, data_type)
# 数据分类
df_cre, df_ups, df_del = classify_datas(df_from, df_join, on=foreign_key)
ls_cre = cut_datas(df_cre,mode='dict',page_size=page_size)
ls_ups = cut_datas(df_ups,mode='dict',page_size=page_size)
ls_del = cut_datas(df_del,mode='list',page_size=page_size)
# body 已经做了格式化
if ls_cre:[insert_records(access_token,app_token,table_id,cre_body) for cre_body in ls_cre]
if ls_ups:[update_records(access_token,app_token,table_id,ups_body) for ups_body in ls_ups]
if ls_del:[delete_records(access_token,app_token,table_id,del_body) for del_body in ls_del]
print('更新完成。关联函数:main')
if __name__ == '__main__':
bitable_url = 'https://vl933ry4wy.feishu.cn/base/EPYFbi4ThahvLUsJ9nUchaXQnLh?table=tblnE4CHrysoKYNO&view=vewH9qJSRL'
connect_info = 'mysql+pymysql://{}:{}@{}:{}/{}?charset=utf8'\
.format("root", "123456", "127.0.0.1", "3306","my_datas")
sql = '''
select uo.user_id as "用户ID"
,u.nickname as "昵称"
,(case u.sex when 0 then '女' when 1 then '男' else '未知' end) as "性别"
,u.mobile as "手机号"
,u.city as "城市"
,uo.id as "订单号"
,uo.paid_time*1000 as "下单时间"
,uo.amount/100 as "下单金额"
from my_datas.user_orders uo
join my_datas.users u on u.id=uo.user_id
where uo.production_id=10;
'''
fields_type = {"用户ID": int, "昵称": str, "性别": str, "手机号": str, "城市": str, "订单号": int, "下单时间": 'int64', "下单金额": float}
foreign_key ='订单号'
data_type = 2
page_size = 2
main(bitable_url, connect_info, sql, fields_type, foreign_key, data_type, page_size)
为了测试上面代码功能,在数据库中,新增两条记录,参考如下:
insert into user_orders(user_id, production_id, paid_time,amount) values
(4,10,1704398000,99900),
(3,11,1704695836,39900);
在多维表上也新增 2 条记录,一条空记录,一条在订单号调试一个数字。
执行完整代码,结果如下:
多维表结果如下:
至此,完成了新增数据的同步。
四、小结
本文介绍了针对需要持续从 MySQL 数据库读取数据并更新到多维表的工作流,介绍了几种可供参考的同步策略:增量同步和全量同步。
增量同步包含定时更新和补数据两种方式,全量同步包含先删除再全量新增和件数据分类,更新旧数据并插入新数据和删除多余数据。
可根据实际的业务场景进行选择同步策略。
补充一点,如果多维表多出的数据不做删除动作,而是新增一个列(比如:异常备注)进行备注,直接走更新逻辑,然后“fields”中只需要给对应列的值标记接口,比如说:{“fields”:{“异常备注”:“多维表数据异常”}}。