美文网首页高性能MySQL
Excel 也可以玩 REST (3)

Excel 也可以玩 REST (3)

作者: Stone0823 | 来源:发表于2018-09-02 22:25 被阅读10次

    系列文章索引


    Excel 也可以玩 REST
    Excel 也可以玩 REST (2)
    Excel 也可以玩 REST (3)


    接下来,设计一个以 Excel 作为用户界面,通过 HTTP Request 对数据库进行 CRUD 操作的实现。我们在日常工作中,经常需要用 Excel 来记录事件和数据,比如,在项目实施的过程中,记录和跟进实施过程中的问题、任务分派等等。但如果不是专门的软件,如 Redmine ,基于 Excel 文件记录数据还是有很多不便之处的。比如版本冲突,多个人员不能同时编辑数据等等。

    这个时候,用 Excel 作为前端界面,实现在线的数据输入和数据同步,不失为一个好的方式。但常规的方法中,Excel 与数据库交互,需要借助诸如 ADO 这样的数据访问模型。一般来说,每一台 PC 都需要安装相关驱动。比如,如果在 Linux 操作系统上部署 MySQL 数据库,那么通过 ADO 的数据访问数据库的话,可能采用 ODBC,需要为每一台 PC 安装 MySQL for ODBC 驱动。

    但 Excel 基于 HTTP Request 的话,从理论上来说,只要有网络,就可以实现 CRUD ,达到在线输入的要求。所以在本篇中,我将介绍如何用 WinHttp COM 对象 ,借助 Http Request,实现对 MySQL 数据库的增删改查。

    当然,前提是有服务器端提供的 Restful API。我在前面相关文章中,使用不同的方法实现过 Restful API,比如 Pthon Flask、 SAP Web Service 和 Node.js 等等,都提供了如何实现 Restful API 的说明,感兴趣的读者可以参考我的文章,或者网络上其他文章。如果是非开发人员,使用其他语言实现 Restful API 可能有一定难度。

    我的相关文章链接:

    Json 数据转换

    Json 数据转换使用 Github 上的 VBA-Json 模块。前面的文章也介绍了使用方法。

    封装 HTTP Request 方法

    为了使用方便,对 Http Request 进行封装,封装为四个方法:

    • doGet: 处理 GET 请求
    • doPost: 处理 POST 请求
    • doPut: 处理 PUT 请求
    • doDelete:处理 DELETE 请求

    代码放在 HttpRequests 模块,完整的代码如下:

    Option Explicit
    
    Public Type HttpResponse
        Status As Long
        ResponseText As String
        StatusText As String
    End Type
    
    Public Function doGet(url As String) As HttpResponse
        On Error GoTo errHandler
        
        Dim httpReq As WinHttp.WinHttpRequest
        Dim httpResp As HttpResponse
        
        Set httpReq = CreateObject("WinHttp.WinHttpRequest.5.1")
    
        httpReq.SetTimeouts 60000, 60000, 60000, 60000
        httpReq.Open "GET", url, False
        httpReq.Send
        
        httpResp.Status = httpReq.Status
        httpResp.ResponseText = httpReq.ResponseText
        httpResp.StatusText = httpReq.StatusText
        
        doGet = httpResp
        Exit Function
        
    errHandler:
        MsgBox Err.Description
        Exit Function
    End Function
    
    
    Public Function doPost(url As String, payload As String) As HttpResponse
    On Error GoTo errHandler
        Dim httpReq As WinHttp.WinHttpRequest
        Dim httpResp As HttpResponse
        
        Set httpReq = CreateObject("WinHttp.WinHttpRequest.5.1")
        
        httpReq.Open "POST", url, False
        httpReq.SetRequestHeader "Content-Type", "application/json"
    
        httpReq.Send payload
        
        httpResp.Status = httpReq.Status
        httpResp.ResponseText = httpReq.ResponseText
        httpResp.StatusText = httpReq.StatusText
        
        doPost = httpResp
        Exit Function
        
    errHandler:
        MsgBox Err.Description
        Exit Function
    End Function
    
    
    Public Function doPut(url As String, payload As String) As HttpResponse
    On Error GoTo errHandler
        Dim httpReq As WinHttp.WinHttpRequest
        Dim httpResp As HttpResponse
        
        Set httpReq = CreateObject("WinHttp.WinHttpRequest.5.1")
        
        httpReq.Open "PUT", url, False
        httpReq.SetRequestHeader "Content-Type", "application/json"
    
        httpReq.Send payload
        
        httpResp.Status = httpReq.Status
        httpResp.ResponseText = httpReq.ResponseText
        httpResp.StatusText = httpReq.StatusText
        
        doPut = httpResp
        Exit Function
        
    errHandler:
        MsgBox Err.Description
        Exit Function
    End Function
    
    
    Public Function doDelete(url As String) As HttpResponse
    On Error GoTo errHandler
        Dim httpReq As WinHttp.WinHttpRequest
        Dim httpResp As HttpResponse
        
        Set httpReq = CreateObject("WinHttp.WinHttpRequest.5.1")
        
        httpReq.Open "DELETE", url, False
        httpReq.Send
        
        httpResp.Status = httpReq.Status
        httpResp.ResponseText = httpReq.ResponseText
        httpResp.StatusText = httpReq.StatusText
        
        doDelete = httpResp
        Exit Function
        
    errHandler:
        MsgBox Err.Description
        Exit Function
    End Function
    

    CRUD 的请求

    后台使用 MySQL 数据库,表名为 emp_master。表的创建脚本和示例数据请参考:Flask 实现 Rest API。代码放在 Employee_CRUD 模块,主要是进一步封装,简化前端的调用。

    Option Explicit
    
    Public Const BASE_URL As String = "http://localhost:5000"
    
    Public Function get_employees() As HttpResponse
        Dim resp As HttpResponse
        resp = doGet(BASE_URL & "/employees")
        get_employees = resp
    End Function
    
    Public Function create_employee(payload As String) As HttpResponse
        Dim resp As HttpResponse
        resp = doPost(BASE_URL & "/employees/create", payload)
        
        create_employee = resp
    End Function
    
    Public Function modify_employee(empId As Integer, payload As String) As HttpResponse
        Dim resp As HttpResponse
        resp = doPut(BASE_URL & "/employees/" & empId, payload)
        
        modify_employee = resp
    End Function
    
    Public Function delete_employee_by_id(empId As Integer) As HttpResponse
        Dim resp As HttpResponse
        resp = doDelete(BASE_URL & "/employees/" & empId)
        
        delete_employee_by_id = resp
    End Function
    

    至此,后台功能全部完毕。

    界面实现逻辑

    下面说明前端的实现方式。首先我们看一看前端的界面,以及我的思路:

    数据刷新、提交修改等功能,都通过 “超链接” 的方法实现,没有使用按钮控件,这样界面更加清爽。当用户在数据区域操作时,自动对用户所在行的状态进行记录。当用户修改了数据,所在行的 A 列自动标记 M。如果点击插入新行,在现有数据下面插入一行,并且所在行的 A 列自动标记为 N。如果需要删除某行,则在 A 列的所在行输入 D。点击提交修改按钮,新增、修改和删除的记录被提交到后台。

    数据区域使用 Table 表格来实现

    Excel 提供了一个叫做 Table 的对象,与一般的数据区域 Range 不同,Table 对象在数据操作、界面自动化等多个方面都更加强大。Table 对象创建的方法,就是选定一个区域,然后 CTRL + T。Table 在 VBA 中被称作 ListObject,比操作 Range 要方便很多。因为篇幅原因,不对 ListObject做过多解释。

    工作表保护与取消保护

    在线编辑涉及多个用户,Excel 界面不能是没有任何制约,否则可能导致服务器端数据的冲突和数据毁损。所以我通过 VBA 代码,对 Excel 工作表进行保护,需要的时候通过代码取消保护。

    Public Sub setWorksheetProtection(sht As Worksheet)
        Dim editRange As AllowEditRange
        For Each editRange In sht.Protection.AllowEditRanges
            editRange.Delete
        Next
        sht.Protection.AllowEditRanges.Add Title:="EditArea", Range:=sht.ListObjects("EmpTable").DataBodyRange
        sht.Protection.AllowEditRanges.Add Title:="ActionFlag", Range:=sht.Range("A:A")
        
        sht.Protect DrawingObjects:=True, Contents:=True, Scenarios:=True _
            , AllowFormattingCells:=True, AllowFormattingColumns:=True, _
            AllowFormattingRows:=True, AllowFiltering:=True, Password:="123456"
    End Sub
    
    
    Public Sub removeWorkSheetProtection(sht As Worksheet)
        sht.Unprotect Password:="123456"
    End Sub
    

    行项目状态的自动标记

    自动标记通过 Workbook_SheetChange 事件来实现。当然,我们不能始终都触发这些事件,所以,我用一个全局变量 isRecordingChange 来记录是否要自动记录修改。

    Public isRecordingChange As Boolean
    
    Public Sub setRecordingFlag(flag As Boolean)
        isRecordingChange = flag
    End Sub
    

    工作簿打开的时候,isRecordingChange 为 True:

    Private Sub Workbook_Open()
        setRecordingFlag True
    End Sub
    

    如果用户在数据区域 (用户可编辑的数据区域为 ListObject EmpTable )修改了记录,自动将 A 列标记为 M:

    Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range)
        If isRecordingChange = False Then Exit Sub
        
        Dim cell As Range
        Dim actionMarkCell As Range
        
        For Each cell In Target.Cells
            If isCellInRange(cell, SheetCRUD.ListObjects("EmpTable").DataBodyRange) Then
                Set actionMarkCell = SheetCRUD.Cells(cell.row, 1)
                If Len(actionMarkCell.Value) = 0 Then
                
                    Call removeWorkSheetProtection(SheetCRUD)
                    actionMarkCell.Value = "M"
                    Call setWorksheetProtection(SheetCRUD)
                    
                End If
            End If
        Next
    End Sub
    

    注意 isCellInRange(cell, SheetCRUD.ListObjects("EmpTable").DataBodyRange) 用于判断数据修改过的单元格是否在 EmpTableDataBodyRange 范围内。isCellInRange 是一个自定义函数, 判断单元格 (cell) 是否在某一个范围 (rng) 内。代码如下:

    Public Function isCellInRange(cell As Range, rng As Range) As Boolean    
        If rng Is Nothing Then
            isCellInRange = False
            Exit Function
        End If
        
        If cell Is Nothing Then
            isCellInRange = False
            Exit Function
        End If
        
        Dim isect As Object
        Set isect = Application.Intersect(cell, rng)
        
        If isect Is Nothing Then
            isCellInRange = False
        Else
            isCellInRange = True
        End If
    End Function
    

    如果用户点击了插入新行超链接,则自动在 A 列标记 N:

    Public Sub insert_new_row()
        Call setRecordingFlag(False)
        Call removeWorkSheetProtection(SheetCRUD)
        
        Dim tbl As ListObject
        Set tbl = SheetCRUD.ListObjects("EmpTable")
        tbl.ListRows.Add alwaysinsert:=True
        tbl.Range(tbl.ListRows.Count, 1).Offset(1, -1).Value = "N"
        
        Call setRecordingFlag(True)
        Call setWorksheetProtection(SheetCRUD)
    End Sub
    

    超链接与宏代码绑定

    如何用超链接来完成操作呢?我以 “刷新” 为例,介绍相关步骤。首先,在 B1 单元中输入刷新 ,然后右键,选择 超链接。在下面的界面中,“链接到” 选择本文档中的位置,单元格引用输入本身所在的单元格,“屏幕提示” 可以输入一个更加清晰的提示,否则,当光标在这个单元格,Excel 显示链接的目标地址。

    接下来,进入 VBE 代码编写环境,Excel 对于工作薄和工作表,都有相应的 FollowHyperLink 事件。本示例只有一个工作表,所以我就在 Worksheet_FollowHyperLink 事件中编写代码:

    Private Sub Worksheet_FollowHyperlink(ByVal Target As Hyperlink)
        Dim rng As Range
        Set rng = Target.Range
        
        Select Case rng.Value
            Case "刷新"
                Call refresh_data
                
            Case "插入新行"
                Call insert_new_row
                
            Case "提交修改"
                Call submit_change_requests
        End Select
    End Sub
    

    根据 Target.Value,执行不同的操作。

    前台功能的实现

    刷新数据

    当用户点击刷新数据 按按钮,触发 refresh_data 过程。refresh_data 过程调用 get_employees() 函数:

    Public Sub refresh_data()
        Call setRecordingFlag(False)
        Call removeWorkSheetProtection(SheetCRUD)
        
        Dim resp As HttpResponse
        resp = get_employees()
        If resp.Status = 200 Then
            Call writeJson(resp.ResponseText, SheetCRUD)
        End If
        
        setRecordingFlag True
        
        Call setWorksheetProtection(SheetCRUD)
    End Sub
    

    如果 Http 请求的状态码为 200,将获取的 json 数据写到工作表中 (writeJson):

    Public Sub writeJson(jsonText As String, sht As Worksheet)
        Dim parsedDict As Object
        Set parsedDict = JsonConverter.parseJson(jsonText)("rows")
    
        Dim tbl As ListObject
        Set tbl = sht.ListObjects("EmpTable")
        If Not tbl.DataBodyRange Is Nothing Then
            tbl.DataBodyRange.Rows.Delete
        End If
        
        ' Print headers
        Dim startCell As Range
        Set startCell = sht.Range("B2")
        
        startCell.Offset(0, 0) = "雇员ID"
        startCell.Offset(0, 1) = "性别"
        startCell.Offset(0, 2) = "年龄"
        startCell.Offset(0, 3) = "Email"
        startCell.Offset(0, 4) = "电话号码"
        startCell.Offset(0, 5) = "教育程度"
        startCell.Offset(0, 6) = "婚姻状况"
        startCell.Offset(0, 7) = "子女数"
       
        ' Print items
        Dim item As Dictionary
        Dim valArray() As Variant
        ReDim valArray(1 To parsedDict.Count, 1 To 8)
        
        Dim rowIdx As Long
        rowIdx = 1
        For Each item In parsedDict
            valArray(rowIdx, 1) = item("EMP_ID")
            valArray(rowIdx, 2) = item("GENDER")
            valArray(rowIdx, 3) = item("AGE")
            valArray(rowIdx, 4) = item("EMAIL")
            valArray(rowIdx, 5) = item("PHONE_NR")
            valArray(rowIdx, 6) = item("EDUCATION")
            valArray(rowIdx, 7) = item("MARITAL_STAT")
            valArray(rowIdx, 8) = item("NR_OF_CHILDREN")
            
            rowIdx = rowIdx + 1
        Next
        
        startCell.Offset(1, 0).Resize(parsedDict.Count, 8).Value = valArray
    End Sub
    

    插入新行

    用户点击插入新行超链接,插入一个新行,并且标记为 N。insert_new_row 的代码刚刚已经介绍了,请自行参考。

    提交修改

    如果用户点击了提交修改超链接,自动将修改的数据提交到后台:

    Public Sub submit_change_requests()
        Dim empId As Integer
        Dim tbl As ListObject
        
        Set tbl = SheetCRUD.ListObjects("EmpTable")
        
        ' 取消工作表保护
        Call removeWorkSheetProtection(SheetCRUD)
        
        ' 根据 A 列确定相应的操作
        ' N: 新增, M: 修改, D: 删除
        Dim idx As Long
        Dim action As String
       
        For idx = 1 To tbl.ListRows.Count
            action = tbl.ListRows(idx).Range(1, 1).Offset(0, -1).Value
            
            If UCase(action) = "N" Then
                If str(tbl.ListRows(idx).Range(1, 1).Value) = "" Then
                    tbl.ListRows(idx).Range(1, 1).Offset(0, -1).Value = ""
                Else
                    Dim newEmp As Employee
                    Dim payload As String
                    
                    newEmp = build_employee_from_range(idx)
                    payload = convert_emp_to_json_text(newEmp)
                    
                    Dim resp As HttpResponse
                    resp = create_employee(payload)
                    
                    If resp.Status = 201 Then
                        tbl.ListRows(idx).Range(1, 1).Offset(0, -1).Value = ""
                    End If
                End If
            End If
            
            If UCase(action) = "M" Then
                Application.ScreenUpdating = False
                
                Dim modifiedEmp As Employee
                modifiedEmp = build_employee_from_range(idx)
                empId = tbl.ListRows(idx).Range(1, 1).Value
                
                payload = convert_emp_to_json_text(modifiedEmp)
                Call modify_employee(empId, payload)
                
                tbl.ListRows(idx).Range(1, 1).Offset(0, -1).Value = ""
                Application.ScreenUpdating = True
            End If
            
            If UCase(action) = "D" Then
                empId = tbl.ListRows(idx).Range(1, 1).Value
                
                Call delete_employee_by_id(empId)
                
                tbl.ListRows(idx).Range(1, 1).Offset(0, -1).Value = ""
                tbl.ListRows(idx).Delete
            End If
        Next
        
        Call setWorksheetProtection(SheetCRUD)
    End Sub
    

    除了前面已经说明过的 create_employee() 函数和 modify_employee() 函数,submit_change_requests 过程还调用了 build_employee_from_range 函数,该函数将 Excel 某一行的数据转换成 Dictionary 对象,convert_emp_to_json_text 函数,将 Dictionary 转换成 json 格式。

    因为我们处理的数据都是针对 employee master,为了方便,先定义一个结构:

    Public Type Employee
        Emp_ID As Integer
        Gender As String
        Age As Integer
        Email As String
        Phone_Nr As String
        Education As String
        Marital_Stat As String
        Nr_Of_Children As Integer
    End Type
    

    build_employee_from_range 函数:

    Public Function build_employee_from_range(rowNumber As Long) As Employee
        Dim tbl As ListObject
        Set tbl = SheetCRUD.ListObjects("EmpTable")
        
        Dim emp As Employee
        Dim idx As Long
        idx = rowNumber
        emp.Emp_ID = tbl.DataBodyRange(idx, 1).Value
        emp.Gender = tbl.DataBodyRange(idx, 2).Value
        emp.Age = tbl.DataBodyRange(idx, 3).Value
        emp.Email = tbl.DataBodyRange(idx, 4).Value
        emp.Phone_Nr = tbl.DataBodyRange(idx, 5).Value
        emp.Education = tbl.DataBodyRange(idx, 6).Value
        emp.Marital_Stat = tbl.DataBodyRange(idx, 7).Value
        emp.Nr_Of_Children = tbl.DataBodyRange(idx, 8).Value
        
        build_employee_from_range = emp
    End Function
    

    convert_emp_to_json_text 函数:

    Public Function convert_emp_to_json_text(emp As Employee) As String
        Dim payloadDict As New Dictionary
        
        payloadDict.Add "EMP_ID", emp.Emp_ID
        payloadDict.Add "GENDER", emp.Gender
        payloadDict.Add "AGE", emp.Age
        payloadDict.Add "EMAIL", emp.Email
        payloadDict.Add "PHONE_NR", emp.Phone_Nr
        payloadDict.Add "EDUCATION", emp.Education
        payloadDict.Add "MARITAL_STAT", emp.Marital_Stat
        payloadDict.Add "NR_OF_CHILDREN", emp.Nr_Of_Children
        
        Dim payload As String
        payload = JsonConverter.ConvertToJson(payloadDict)
        
        convert_emp_to_json_text = payload
    End Function
    

    相关文章

      网友评论

        本文标题:Excel 也可以玩 REST (3)

        本文链接:https://www.haomeiwen.com/subject/bkzmwftx.html