refactor: 数据同步优化 & 标签树支持双击展开节点 & mysql支持with语句

This commit is contained in:
meilin.huang
2025-01-10 12:05:00 +08:00
parent 1be7a0ec79
commit 8d24c2a4fa
15 changed files with 231 additions and 187 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -1,6 +1,7 @@
import { VNode, h, render } from 'vue'; import { VNode, h, render } from 'vue';
import MonacoEditorDialog from './MonacoEditorDialog.vue'; import MonacoEditorDialog from './MonacoEditorDialog.vue';
import * as monaco from 'monaco-editor'; import * as monaco from 'monaco-editor';
import { ElMessage } from 'element-plus';
export type MonacoEditorDialogProps = { export type MonacoEditorDialogProps = {
content: string; content: string;
@@ -53,7 +54,23 @@ const MonacoEditorBox = (props: MonacoEditorDialogProps): void => {
console.log('close editor'); console.log('close editor');
}, },
onConfirm: () => { onConfirm: () => {
props.confirmFn && props.confirmFn(props.content); let value = props.content;
if (props.language === 'json') {
let val;
try {
val = JSON.parse(value);
if (typeof val !== 'object') {
ElMessage.error('Invalid json');
return;
}
} catch (e) {
ElMessage.error('Invalid json');
return;
}
// 压缩json字符串
value = JSON.stringify(val);
}
props.confirmFn && props.confirmFn(value);
}, },
}); });
// 将虚拟dom渲染到 container dom 上 // 将虚拟dom渲染到 container dom 上

View File

@@ -76,11 +76,11 @@ const confirm = async () => {
try { try {
val = JSON.parse(value); val = JSON.parse(value);
if (typeof val !== 'object') { if (typeof val !== 'object') {
ElMessage.error('请输入正确的json'); ElMessage.error('Invalid json');
return; return;
} }
} catch (e) { } catch (e) {
ElMessage.error('请输入正确的json'); ElMessage.error('Invalid json');
return; return;
} }

View File

@@ -18,7 +18,12 @@
:default-expanded-keys="props.defaultExpandedKeys" :default-expanded-keys="props.defaultExpandedKeys"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<span :id="node.key" @dblclick="treeNodeDblclick(data)" :class="data.type.nodeDblclickFunc ? 'none-select' : ''"> <span
:id="node.key"
@dblclick="treeNodeDblclick(data, node)"
class="node-container none-select"
:class="data.type.nodeDblclickFunc ? 'none-select' : ''"
>
<span v-if="data.type.value == TagTreeNode.TagPath"> <span v-if="data.type.value == TagTreeNode.TagPath">
<tag-info :tag-path="data.label" /> <tag-info :tag-path="data.label" />
</span> </span>
@@ -157,7 +162,13 @@ const treeNodeClick = async (data: any) => {
}; };
// 树节点双击事件 // 树节点双击事件
const treeNodeDblclick = (data: any) => { const treeNodeDblclick = (data: any, node: any) => {
if (node.expanded) {
node.collapse();
} else {
node.expand();
}
// emit('nodeDblick', data); // emit('nodeDblick', data);
if (!data.disabled && data.type.nodeDblclickFunc) { if (!data.disabled && data.type.nodeDblclickFunc) {
data.type.nodeDblclickFunc(data); data.type.nodeDblclickFunc(data);
@@ -245,5 +256,12 @@ defineExpose({
font-size: 10px; font-size: 10px;
margin-top: 2px; margin-top: 2px;
} }
.node-container {
display: flex;
align-items: center;
width: 100%; // 确保容器宽度占满整个节点区域
cursor: pointer; // 添加鼠标指针样式
}
} }
</style> </style>

View File

@@ -295,7 +295,7 @@ const TableIcon = {
}; };
const SqlIcon = { const SqlIcon = {
name: 'Files', name: 'icon db/sql',
color: '#f56c6c', color: '#f56c6c',
}; };
@@ -450,11 +450,11 @@ const NodeTypeTableMenu = new NodeType(SqlExecNodeType.TableMenu)
// 设置父节点参数的表大小 // 设置父节点参数的表大小
parentNode.params.dbTableSize = dbTableSize == 0 ? '' : formatByteSize(dbTableSize); parentNode.params.dbTableSize = dbTableSize == 0 ? '' : formatByteSize(dbTableSize);
return tablesNode; return tablesNode;
})
.withNodeDblclickFunc((node: TagTreeNode) => {
const params = node.params;
addTablesOpTab({ id: params.id, db: params.db, type: params.type, version: params.version, nodeKey: node.key });
}); });
// .withNodeDblclickFunc((node: TagTreeNode) => {
// const params = node.params;
// addTablesOpTab({ id: params.id, db: params.db, type: params.type, version: params.version, nodeKey: node.key });
// });
// 数据库sql模板菜单节点 // 数据库sql模板菜单节点
const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu) const NodeTypeSqlMenu = new NodeType(SqlExecNodeType.SqlMenu)

View File

@@ -8,21 +8,19 @@
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto"> <el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
<el-tabs v-model="tabActiveName"> <el-tabs v-model="tabActiveName">
<el-tab-pane :label="$t('common.basic')" :name="basicTab"> <el-tab-pane :label="$t('common.basic')" :name="basicTab">
<el-form-item> <el-row>
<el-row> <el-col :span="12">
<el-col :span="12"> <el-form-item prop="taskName" :label="$t('db.taskName')" required>
<el-form-item prop="taskName" :label="$t('db.taskName')" required> <el-input v-model.trim="form.taskName" auto-complete="off" />
<el-input v-model.trim="form.taskName" auto-complete="off" /> </el-form-item>
</el-form-item> </el-col>
</el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item prop="taskCron" label="cron" required> <el-form-item prop="taskCron" label="cron" required>
<CrontabInput v-model="form.taskCron" /> <CrontabInput v-model="form.taskCron" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
</el-form-item>
<el-form-item prop="status" :label="$t('common.status')" label-width="60" required> <el-form-item prop="status" :label="$t('common.status')" label-width="60" required>
<el-switch <el-switch
@@ -61,81 +59,70 @@
<monaco-editor height="200px" class="task-sql" language="sql" v-model="form.dataSql" /> <monaco-editor height="200px" class="task-sql" language="sql" v-model="form.dataSql" />
</el-form-item> </el-form-item>
<el-form-item> <el-row>
<el-row class="w100"> <el-col :span="12">
<el-col :span="12"> <el-form-item prop="targetTableName" :label="$t('db.targetDbTable')" required>
<el-form-item prop="targetTableName" :label="$t('db.targetDbTable')" required> <el-select v-model="form.targetTableName" filterable>
<el-select v-model="form.targetTableName" filterable> <el-option
<el-option v-for="item in state.targetTableList"
v-for="item in state.targetTableList" :key="item.tableName"
:key="item.tableName" :label="item.tableName + (item.tableComment && '-' + item.tableComment)"
:label="item.tableName + (item.tableComment && '-' + item.tableComment)" :value="item.tableName"
:value="item.tableName"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="pageSize" :label="$t('db.pageSize')" required>
<el-input
type="number"
v-model.number="form.pageSize"
:placeholder="$t('db.pageSizePlaceholder')"
auto-complete="off"
/> />
</el-form-item> </el-select>
</el-col> </el-form-item>
</el-row> </el-col>
</el-form-item>
<el-form-item> <el-col :span="12">
<el-row class="w100"> <el-form-item prop="pageSize" :label="$t('db.pageSize')" required>
<el-col :span="12"> <el-input type="number" v-model.number="form.pageSize" :placeholder="$t('db.pageSizePlaceholder')" auto-complete="off" />
<el-form-item class="w100" prop="updField"> </el-form-item>
<template #label> </el-col>
{{ $t('db.updateField') }} </el-row>
<el-tooltip :content="$t('db.updateFieldTips')" placement="top">
<SvgIcon name="question-filled" />
</el-tooltip>
</template>
<el-input v-model.trim="form.updField" :placeholder="$t('db.updateFiledPlaceholder')" auto-complete="off" />
</el-form-item>
</el-col>
<el-col :span="12"> <el-row>
<el-form-item class="w100" prop="updFieldVal"> <el-col :span="12">
<template #label> <el-form-item class="w100" prop="updField">
{{ $t('db.updateFieldValue') }} <template #label>
<el-tooltip :content="$t('db.updateFieldValueTips')" placement="top"> {{ $t('db.updateField') }}
<el-icon> <el-tooltip :content="$t('db.updateFieldTips')" placement="top">
<question-filled /> <SvgIcon name="question-filled" />
</el-icon> </el-tooltip>
</el-tooltip> </template>
</template> <el-input v-model.trim="form.updField" :placeholder="$t('db.updateFiledPlaceholder')" auto-complete="off" />
<el-input v-model.trim="form.updFieldVal" :placeholder="$t('db.updateFieldValuePlaceholder')" auto-complete="off" /> </el-form-item>
</el-form-item> </el-col>
</el-col>
</el-row>
</el-form-item>
<el-form-item> <el-col :span="12">
<el-row class="w100"> <el-form-item class="w100" prop="updFieldVal">
<el-col :span="12"> <template #label>
<el-form-item class="w100" prop="updFieldSrc"> {{ $t('db.updateFieldValue') }}
<template #label> <el-tooltip :content="$t('db.updateFieldValueTips')" placement="top">
{{ $t('db.fieldValueSrc') }} <el-icon>
<el-tooltip :content="$t('db.fieldValueSrcTips')" placement="top"> <question-filled />
<el-icon> </el-icon>
<question-filled /> </el-tooltip>
</el-icon> </template>
</el-tooltip> <el-input v-model.trim="form.updFieldVal" :placeholder="$t('db.updateFieldValuePlaceholder')" auto-complete="off" />
</template> </el-form-item>
<el-input v-model.trim="form.updFieldSrc" :placeholder="$t('db.fieldValueSrcPlaceholder')" auto-complete="off" /> </el-col>
</el-form-item> </el-row>
</el-col>
</el-row> <el-row>
</el-form-item> <el-col :span="12">
<el-form-item class="w100" prop="updFieldSrc">
<template #label>
{{ $t('db.fieldValueSrc') }}
<el-tooltip :content="$t('db.fieldValueSrcTips')" placement="top">
<el-icon>
<question-filled />
</el-icon>
</el-tooltip>
</template>
<el-input v-model.trim="form.updFieldSrc" :placeholder="$t('db.fieldValueSrcPlaceholder')" auto-complete="off" />
</el-form-item>
</el-col>
</el-row>
</el-tab-pane> </el-tab-pane>
<el-tab-pane :label="$t('db.fieldMap')" :name="fieldTab" :disabled="!baseFieldCompleted"> <el-tab-pane :label="$t('db.fieldMap')" :name="fieldTab" :disabled="!baseFieldCompleted">

View File

@@ -50,7 +50,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue'; import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { dbApi } from './api'; import { dbApi } from './api';
import PageTable from '@/components/pagetable/PageTable.vue'; import PageTable from '@/components/pagetable/PageTable.vue';
import { TableColumn } from '@/components/pagetable'; import { TableColumn } from '@/components/pagetable';
@@ -58,13 +57,10 @@ import { hasPerms } from '@/components/auth/auth';
import { SearchItem } from '@/components/SearchForm'; import { SearchItem } from '@/components/SearchForm';
import { DbDataSyncRecentStateEnum, DbDataSyncRunningStateEnum } from './enums'; import { DbDataSyncRecentStateEnum, DbDataSyncRunningStateEnum } from './enums';
import { useI18nConfirm, useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n'; import { useI18nConfirm, useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
import { useI18n } from 'vue-i18n';
const DataSyncTaskEdit = defineAsyncComponent(() => import('./SyncTaskEdit.vue')); const DataSyncTaskEdit = defineAsyncComponent(() => import('./SyncTaskEdit.vue'));
const DataSyncTaskLog = defineAsyncComponent(() => import('./SyncTaskLog.vue')); const DataSyncTaskLog = defineAsyncComponent(() => import('./SyncTaskLog.vue'));
const { t } = useI18n();
const perms = { const perms = {
save: 'db:sync:save', save: 'db:sync:save',
del: 'db:sync:del', del: 'db:sync:del',
@@ -77,6 +73,7 @@ const searchItems = [SearchItem.input('name', 'common.name')];
// 任务名、修改人、修改时间、最近一次任务执行状态、状态(停用启用)、操作 // 任务名、修改人、修改时间、最近一次任务执行状态、状态(停用启用)、操作
const columns = ref([ const columns = ref([
TableColumn.new('taskName', 'db.taskName'), TableColumn.new('taskName', 'db.taskName'),
TableColumn.new('cron', 'Cron'),
TableColumn.new('runningState', 'db.runState').typeTag(DbDataSyncRunningStateEnum), TableColumn.new('runningState', 'db.runState').typeTag(DbDataSyncRunningStateEnum),
TableColumn.new('recentState', 'db.recentState').typeTag(DbDataSyncRecentStateEnum), TableColumn.new('recentState', 'db.recentState').typeTag(DbDataSyncRecentStateEnum),
TableColumn.new('status', 'common.status').isSlot(), TableColumn.new('status', 'common.status').isSlot(),

View File

@@ -112,11 +112,7 @@ func (d *DataSyncTask) Run(rc *req.Ctx) {
func (d *DataSyncTask) Stop(rc *req.Ctx) { func (d *DataSyncTask) Stop(rc *req.Ctx) {
taskId := d.getTaskId(rc) taskId := d.getTaskId(rc)
rc.ReqParam = taskId rc.ReqParam = taskId
biz.ErrIsNil(d.dataSyncTaskApp.StopTask(rc.MetaCtx, taskId))
task := new(entity.DataSyncTask)
task.Id = taskId
task.RunningState = entity.DataSyncTaskRunStateStop
_ = d.dataSyncTaskApp.UpdateById(rc.MetaCtx, task)
} }
func (d *DataSyncTask) GetTask(rc *req.Ctx) { func (d *DataSyncTask) GetTask(rc *req.Ctx) {

View File

@@ -5,6 +5,7 @@ import "time"
type DataSyncTaskListVO struct { type DataSyncTaskListVO struct {
Id int64 `json:"id"` Id int64 `json:"id"`
TaskName string `json:"taskName"` TaskName string `json:"taskName"`
TaskCron string `json:"cron"`
CreateTime *time.Time `json:"createTime"` CreateTime *time.Time `json:"createTime"`
Creator string `json:"creator"` Creator string `json:"creator"`
UpdateTime *time.Time `json:"updateTime"` UpdateTime *time.Time `json:"updateTime"`

View File

@@ -3,13 +3,13 @@ package application
import ( import (
"cmp" "cmp"
"context" "context"
"database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"mayfly-go/internal/db/dbm/dbi" "mayfly-go/internal/db/dbm/dbi"
"mayfly-go/internal/db/domain/entity" "mayfly-go/internal/db/domain/entity"
"mayfly-go/internal/db/domain/repository" "mayfly-go/internal/db/domain/repository"
"mayfly-go/pkg/base" "mayfly-go/pkg/base"
"mayfly-go/pkg/cache"
"mayfly-go/pkg/contextx" "mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx" "mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
@@ -42,6 +42,8 @@ type DataSyncTask interface {
RunCronJob(ctx context.Context, id uint64) error RunCronJob(ctx context.Context, id uint64) error
StopTask(ctx context.Context, id uint64) error
GetTaskLogList(condition *entity.DataSyncLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) GetTaskLogList(condition *entity.DataSyncLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
} }
@@ -117,17 +119,10 @@ func (app *dataSyncAppImpl) RemoveCronJobById(taskId uint64) {
if err == nil { if err == nil {
scheduler.RemoveByKey(task.TaskKey) scheduler.RemoveByKey(task.TaskKey)
} }
} app.MarkStop(taskId)
func (app *dataSyncAppImpl) changeRunningState(id uint64, state int8) {
task := new(entity.DataSyncTask)
task.Id = id
task.RunningState = state
_ = app.UpdateById(context.Background(), task)
} }
func (app *dataSyncAppImpl) RunCronJob(ctx context.Context, id uint64) error { func (app *dataSyncAppImpl) RunCronJob(ctx context.Context, id uint64) error {
// 查询最新的任务信息
task, err := app.GetById(id) task, err := app.GetById(id)
if err != nil { if err != nil {
return errorx.NewBiz("task not found") return errorx.NewBiz("task not found")
@@ -135,8 +130,9 @@ func (app *dataSyncAppImpl) RunCronJob(ctx context.Context, id uint64) error {
if task.RunningState == entity.DataSyncTaskRunStateRunning { if task.RunningState == entity.DataSyncTaskRunStateRunning {
return errorx.NewBiz("the task is in progress") return errorx.NewBiz("the task is in progress")
} }
// 开始运行时,修改状态为运行中
app.changeRunningState(id, entity.DataSyncTaskRunStateRunning) // 标记该任务运行中
app.MarkRunning(id)
logx.InfofContext(ctx, "start the data synchronization task: %s => %s", task.TaskName, task.TaskKey) logx.InfofContext(ctx, "start the data synchronization task: %s => %s", task.TaskName, task.TaskKey)
@@ -183,8 +179,11 @@ func (app *dataSyncAppImpl) RunCronJob(ctx context.Context, id uint64) error {
log.ErrText = fmt.Sprintf("execution failure: %s", err.Error()) log.ErrText = fmt.Sprintf("execution failure: %s", err.Error())
logx.ErrorContext(ctx, log.ErrText) logx.ErrorContext(ctx, log.ErrText)
log.Status = entity.DataSyncTaskStateFail log.Status = entity.DataSyncTaskStateFail
app.endRunning(task, log) } else {
log.Status = entity.DataSyncTaskStateSuccess
} }
app.endRunning(task, log)
}() }()
return nil return nil
@@ -210,16 +209,6 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
if err != nil { if err != nil {
return syncLog, errorx.NewBiz("failed to connect to the target database: %s", err.Error()) return syncLog, errorx.NewBiz("failed to connect to the target database: %s", err.Error())
} }
targetDbTx, err := targetConn.Begin()
if err != nil {
return syncLog, errorx.NewBiz("failed to start the target database transaction: %s", err.Error())
}
defer func() {
if r := recover(); r != nil {
targetDbTx.Rollback()
err = fmt.Errorf("%v", r)
}
}()
// task.FieldMap为json数组字符串 [{"src":"id","target":"id"}]转为map // task.FieldMap为json数组字符串 [{"src":"id","target":"id"}]转为map
var fieldMap []map[string]string var fieldMap []map[string]string
@@ -227,13 +216,11 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
if err != nil { if err != nil {
return syncLog, errorx.NewBiz("there was an error parsing the field map json: %s", err.Error()) return syncLog, errorx.NewBiz("there was an error parsing the field map json: %s", err.Error())
} }
var updFieldType *dbi.DbDataType
// 记录本次同步数据总数 // 记录本次同步数据总数
total := 0 total := 0
batchSize := task.PageSize batchSize := task.PageSize
result := make([]map[string]any, 0) result := make([]map[string]any, 0)
var queryColumns []*dbi.QueryColumn
// 如果有数据库别名则从UpdField中去掉数据库别名, 如a.id => id用于获取字段具体名称 // 如果有数据库别名则从UpdField中去掉数据库别名, 如a.id => id用于获取字段具体名称
updFieldName := task.UpdField updFieldName := task.UpdField
@@ -255,23 +242,10 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
}) })
_, err = srcConn.WalkQueryRows(context.Background(), sql, func(row map[string]any, columns []*dbi.QueryColumn) error { _, err = srcConn.WalkQueryRows(context.Background(), sql, func(row map[string]any, columns []*dbi.QueryColumn) error {
if len(queryColumns) == 0 {
queryColumns = columns
// 遍历columns 取task.UpdField的字段类型
updFieldType = dbi.DefaultDbDataType
for _, column := range columns {
if strings.EqualFold(column.Name, updFieldName) {
updFieldType = column.DbDataType
break
}
}
}
total++ total++
result = append(result, row) result = append(result, row)
if total%batchSize == 0 { if total%batchSize == 0 {
if err := app.srcData2TargetDb(result, fieldMap, updFieldType, updFieldName, task, targetConn, targetDbTx, targetInsertColumns); err != nil { if err := app.srcData2TargetDb(result, fieldMap, updFieldName, task, targetConn, targetInsertColumns); err != nil {
return err return err
} }
@@ -282,43 +256,37 @@ func (app *dataSyncAppImpl) doDataSync(ctx context.Context, sql string, task *en
app.saveLog(syncLog) app.saveLog(syncLog)
result = result[:0] result = result[:0]
// 运行过程中,判断状态是否为已关闭,是则结束运行,否则继续运行
if !app.IsRunning(task.Id) {
return errorx.NewBiz("the task has been terminated manually")
}
} }
return nil return nil
}) })
if err != nil { if err != nil {
targetDbTx.Rollback()
return syncLog, err return syncLog, err
} }
// 处理剩余的数据 // 处理剩余的数据
if len(result) > 0 { if len(result) > 0 {
if err := app.srcData2TargetDb(result, fieldMap, updFieldType, updFieldName, task, targetConn, targetDbTx, targetInsertColumns); err != nil { if err := app.srcData2TargetDb(result, fieldMap, updFieldName, task, targetConn, targetInsertColumns); err != nil {
targetDbTx.Rollback()
return syncLog, err return syncLog, err
} }
} }
// 如果是mssql暂不手动提交事务否则报错 mssql: The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
if err := targetDbTx.Commit(); err != nil {
if targetConn.Info.Type != dbi.ToDbType("mssql") {
return syncLog, errorx.NewBiz("data synchronization - The target database transaction failed to commit: %s", err.Error())
}
}
logx.InfofContext(ctx, "synchronous task: [%s], finished execution, save records successfully: [%d]", task.TaskName, total) logx.InfofContext(ctx, "synchronous task: [%s], finished execution, save records successfully: [%d]", task.TaskName, total)
// 保存执行成功日志 // 执行成功日志
syncLog.ErrText = fmt.Sprintf("the synchronous task was executed successfully. New data: %d", total) syncLog.ErrText = fmt.Sprintf("the synchronous task was executed successfully. New data: %d", total)
syncLog.Status = entity.DataSyncTaskStateSuccess
syncLog.ResNum = total syncLog.ResNum = total
app.endRunning(task, syncLog)
return syncLog, nil return syncLog, nil
} }
func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap []map[string]string, updFieldType *dbi.DbDataType, updFieldName string, task *entity.DataSyncTask, targetDbConn *dbi.DbConn, targetDbTx *sql.Tx, targetInsertColumns []dbi.Column) error { func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap []map[string]string, updFieldName string, task *entity.DataSyncTask, targetDbConn *dbi.DbConn, targetInsertColumns []dbi.Column) (err error) {
// 遍历res组装数据 // 遍历res组装数据
var targetData = make([]map[string]any, 0) var targetData = make([]map[string]any, 0)
for _, srcData := range srcRes { for _, srcData := range srcRes {
@@ -341,6 +309,37 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
} }
// 执行插入 // 执行插入
targetDialect := targetDbConn.GetDialect()
// 生成目标数据库批量插入sql并执行
sqls := targetDialect.GetSQLGenerator().GenInsert(task.TargetTableName, targetInsertColumns, tragetValues, cmp.Or(task.DuplicateStrategy, dbi.DuplicateStrategyNone))
// 开启本批次执行事务
targetDbTx, err := targetDbConn.Begin()
if err != nil {
return errorx.NewBiz("failed to start the target database transaction: %s", err.Error())
}
defer func() {
if r := recover(); r != nil {
targetDbTx.Rollback()
err = fmt.Errorf("%v", r)
}
}()
for _, sql := range sqls {
_, err := targetDbTx.Exec(sql)
if err != nil {
targetDbTx.Rollback()
return err
}
}
// 如果是mssql暂不手动提交事务否则报错 mssql: The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
if err := targetDbTx.Commit(); err != nil {
if targetDbConn.Info.Type != dbi.ToDbType("mssql") {
return errorx.NewBiz("data synchronization - The target database transaction failed to commit: %s", err.Error())
}
}
setUpdateFieldVal := func(field string) { setUpdateFieldVal := func(field string) {
// 解决字段大小写问题 // 解决字段大小写问题
@@ -351,27 +350,20 @@ func (app *dataSyncAppImpl) srcData2TargetDb(srcRes []map[string]any, fieldMap [
task.UpdFieldVal = cast.ToString(updFieldVal) task.UpdFieldVal = cast.ToString(updFieldVal)
} }
// 如果指定了更新字段,则以更新字段取值 // 如果指定了更新字段,则以更新字段取值
setUpdateFieldVal(cmp.Or(task.UpdFieldSrc, updFieldName)) setUpdateFieldVal(cmp.Or(task.UpdFieldSrc, updFieldName))
targetDialect := targetDbConn.GetDialect() return nil
}
// 生成目标数据库批量插入sql并执行 func (app *dataSyncAppImpl) StopTask(ctx context.Context, taskId uint64) error {
sqls := targetDialect.GetSQLGenerator().GenInsert(task.TargetTableName, targetInsertColumns, tragetValues, cmp.Or(task.DuplicateStrategy, dbi.DuplicateStrategyNone)) task := new(entity.DataSyncTask)
for _, sql := range sqls { task.Id = taskId
_, err := targetDbTx.Exec(sql) task.RunningState = entity.DataSyncTaskRunStateStop
if err != nil { if err := app.UpdateById(ctx, task); err != nil {
return err return err
}
} }
app.MarkStop(taskId)
// 运行过程中,判断状态是否为已关闭,是则结束运行,否则继续运行
taskParam, _ := app.GetById(task.Id)
if taskParam.RunningState == entity.DataSyncTaskRunStateStop {
return errorx.NewBiz("the task has been terminated manually")
}
return nil return nil
} }
@@ -382,9 +374,7 @@ func (app *dataSyncAppImpl) endRunning(taskEntity *entity.DataSyncTask, log *ent
task := new(entity.DataSyncTask) task := new(entity.DataSyncTask)
task.Id = taskEntity.Id task.Id = taskEntity.Id
task.RecentState = state task.RecentState = state
if state == entity.DataSyncTaskStateSuccess { task.UpdFieldVal = taskEntity.UpdFieldVal
task.UpdFieldVal = taskEntity.UpdFieldVal
}
task.RunningState = entity.DataSyncTaskRunStateReady task.RunningState = entity.DataSyncTaskRunStateReady
// 运行失败之后设置任务状态为禁用 // 运行失败之后设置任务状态为禁用
//if state == entity.DataSyncTaskStateFail { //if state == entity.DataSyncTaskStateFail {
@@ -394,6 +384,7 @@ func (app *dataSyncAppImpl) endRunning(taskEntity *entity.DataSyncTask, log *ent
_ = app.UpdateById(context.Background(), task) _ = app.UpdateById(context.Background(), task)
// 保存执行日志 // 保存执行日志
app.saveLog(log) app.saveLog(log)
app.MarkStop(task.Id)
} }
func (app *dataSyncAppImpl) saveLog(log *entity.DataSyncLog) { func (app *dataSyncAppImpl) saveLog(log *entity.DataSyncLog) {
@@ -440,3 +431,23 @@ func (app *dataSyncAppImpl) InitCronJob() {
func (app *dataSyncAppImpl) GetTaskLogList(condition *entity.DataSyncLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) { func (app *dataSyncAppImpl) GetTaskLogList(condition *entity.DataSyncLogQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
return app.dbDataSyncLogRepo.GetTaskLogList(condition, pageParam, toEntity, orderBy...) return app.dbDataSyncLogRepo.GetTaskLogList(condition, pageParam, toEntity, orderBy...)
} }
// MarkRunning 标记任务执行中
func (app *dataSyncAppImpl) MarkRunning(taskId uint64) {
task := new(entity.DataSyncTask)
task.Id = taskId
task.RunningState = entity.DataSyncTaskRunStateRunning
_ = app.UpdateById(context.Background(), task)
cache.Set(fmt.Sprintf("mayfly:db:syncdata:%d", taskId), 1, -1)
}
// MarkStop 标记任务结束
func (app *dataSyncAppImpl) MarkStop(taskId uint64) {
cache.Del(fmt.Sprintf("mayfly:db:syncdata:%d", taskId))
}
// IsRunning 判断任务是否执行中
func (app *dataSyncAppImpl) IsRunning(taskId uint64) bool {
return cache.GetStr(fmt.Sprintf("mayfly:db:syncdata:%d", taskId)) != ""
}

View File

@@ -136,6 +136,8 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *dto.DbSqlExecRe
return allExecRes, nil return allExecRes, nil
} }
// mysql parser with语句会分解析为两条故需要特殊处理
currentWithSql := ""
for _, stmt := range stmts { for _, stmt := range stmts {
var execRes *dto.DbSqlExecRes var execRes *dto.DbSqlExecRes
var err error var err error
@@ -143,7 +145,8 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *dto.DbSqlExecRe
sql := stmt.GetText() sql := stmt.GetText()
dbSqlExecRecord := createSqlExecRecord(ctx, execSqlReq, sql) dbSqlExecRecord := createSqlExecRecord(ctx, execSqlReq, sql)
dbSqlExecRecord.Type = entity.DbSqlExecTypeOther dbSqlExecRecord.Type = entity.DbSqlExecTypeOther
sqlExec := &sqlExecParam{DbConn: dbConn, Sql: sql, Procdef: flowProcdef, Stmt: stmt, SqlExecRecord: dbSqlExecRecord} sqlExec := &sqlExecParam{DbConn: dbConn, Sql: currentWithSql + sql, Procdef: flowProcdef, Stmt: stmt, SqlExecRecord: dbSqlExecRecord}
currentWithSql = ""
switch stmt.(type) { switch stmt.(type) {
case *sqlstmt.SimpleSelectStmt: case *sqlstmt.SimpleSelectStmt:
@@ -152,6 +155,8 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *dto.DbSqlExecRe
execRes, err = d.doSelect(ctx, sqlExec) execRes, err = d.doSelect(ctx, sqlExec)
case *sqlstmt.OtherReadStmt: case *sqlstmt.OtherReadStmt:
execRes, err = d.doOtherRead(ctx, sqlExec) execRes, err = d.doOtherRead(ctx, sqlExec)
case *sqlstmt.WithStmt:
currentWithSql = sql
case *sqlstmt.UpdateStmt: case *sqlstmt.UpdateStmt:
execRes, err = d.doUpdate(ctx, sqlExec) execRes, err = d.doUpdate(ctx, sqlExec)
case *sqlstmt.DeleteStmt: case *sqlstmt.DeleteStmt:
@@ -174,9 +179,13 @@ func (d *dbSqlExecAppImpl) Exec(ctx context.Context, execSqlReq *dto.DbSqlExecRe
execRes, err = d.doExec(ctx, dbConn, sql) execRes, err = d.doExec(ctx, dbConn, sql)
} }
if currentWithSql != "" {
continue
}
if err != nil { if err != nil {
if execRes == nil { if execRes == nil {
execRes = &dto.DbSqlExecRes{Sql: sql} execRes = &dto.DbSqlExecRes{Sql: sqlExec.Sql}
} }
execRes.ErrorMsg = err.Error() execRes.ErrorMsg = err.Error()
} else { } else {
@@ -624,7 +633,7 @@ func isInsert(sql string) bool {
func isOtherQuery(sql string) bool { func isOtherQuery(sql string) bool {
sqlPrefix := strings.ToLower(sql[:10]) sqlPrefix := strings.ToLower(sql[:10])
return strings.Contains(sqlPrefix, "explain") || strings.Contains(sqlPrefix, "show") return strings.Contains(sqlPrefix, "explain") || strings.Contains(sqlPrefix, "show") || strings.Contains(sqlPrefix, "with")
} }
func isDDL(sql string) bool { func isDDL(sql string) bool {

View File

@@ -62,6 +62,9 @@ func (v *MysqlVisitor) VisitDmlStatement(ctx *mysqlparser.DmlStatementContext) i
if ssc := ctx.SelectStatement(); ssc != nil { if ssc := ctx.SelectStatement(); ssc != nil {
return ssc.Accept(v) return ssc.Accept(v)
} }
if withStmt := ctx.WithStatement(); withStmt != nil {
return withStmt.Accept(v)
}
if usc := ctx.UpdateStatement(); usc != nil { if usc := ctx.UpdateStatement(); usc != nil {
return usc.Accept(v) return usc.Accept(v)
} }
@@ -94,6 +97,12 @@ func (v *MysqlVisitor) VisitUtilityStatement(ctx *mysqlparser.UtilityStatementCo
return sqlstmt.NewNode(ctx.GetParser(), ctx) return sqlstmt.NewNode(ctx.GetParser(), ctx)
} }
func (v *MysqlVisitor) VisitWithStatement(ctx *mysqlparser.WithStatementContext) interface{} {
ort := new(sqlstmt.WithStmt)
ort.Node = sqlstmt.NewNode(ctx.GetParser(), ctx)
return ort
}
func (v *MysqlVisitor) VisitSimpleSelect(ctx *mysqlparser.SimpleSelectContext) interface{} { func (v *MysqlVisitor) VisitSimpleSelect(ctx *mysqlparser.SimpleSelectContext) interface{} {
sss := new(sqlstmt.SimpleSelectStmt) sss := new(sqlstmt.SimpleSelectStmt)
sss.Node = sqlstmt.NewNode(ctx.GetParser(), ctx) sss.Node = sqlstmt.NewNode(ctx.GetParser(), ctx)

View File

@@ -70,6 +70,10 @@ type OtherReadStmt struct {
*Node *Node
} }
type WithStmt struct {
*Node
}
func IsSelectStmt(stmt Stmt) bool { func IsSelectStmt(stmt Stmt) bool {
return reflect.TypeOf(stmt).AssignableTo(reflect.TypeOf(&SelectStmt{})) return reflect.TypeOf(stmt).AssignableTo(reflect.TypeOf(&SelectStmt{}))
} }

View File

@@ -58,7 +58,7 @@ const (
DataSyncTaskStatusDisable int8 = -1 // 禁用状态 DataSyncTaskStatusDisable int8 = -1 // 禁用状态
DataSyncTaskStateSuccess int8 = 1 // 执行成功状态 DataSyncTaskStateSuccess int8 = 1 // 执行成功状态
DataSyncTaskStateRunning int8 = 2 // 执行成功状态 DataSyncTaskStateRunning int8 = 2 // 执行状态
DataSyncTaskStateFail int8 = -1 // 执行失败状态 DataSyncTaskStateFail int8 = -1 // 执行失败状态
DataSyncTaskRunStateRunning int8 = 1 // 运行中状态 DataSyncTaskRunStateRunning int8 = 1 // 运行中状态

View File

@@ -5,7 +5,6 @@ import (
"mayfly-go/pkg/logx" "mayfly-go/pkg/logx"
"mayfly-go/pkg/rediscli" "mayfly-go/pkg/rediscli"
"mayfly-go/pkg/utils/anyx" "mayfly-go/pkg/utils/anyx"
"strconv"
"strings" "strings"
"time" "time"
@@ -22,7 +21,7 @@ func GetStr(key string) string {
if val == nil { if val == nil {
return "" return ""
} }
return val.(string) return cast.ToString(val)
} }
if res, err := rediscli.Get(key); err == nil { if res, err := rediscli.Get(key); err == nil {
@@ -36,12 +35,7 @@ func GetInt(key string) int {
if val == "" { if val == "" {
return 0 return 0
} }
if intV, err := strconv.Atoi(val); err != nil { return cast.ToInt(key)
logx.Error("获取缓存中的int值转换失败", err)
return 0
} else {
return intV
}
} }
// Get 获取缓存值并使用json反序列化。返回是否获取成功。若不存在或者解析失败则返回false // Get 获取缓存值并使用json反序列化。返回是否获取成功。若不存在或者解析失败则返回false