Windows11本地部署DeepSeek加速 - DECHIN - 博客园

mikel阅读(68)

来源: Windows11本地部署DeepSeek加速 – DECHIN – 博客园

技术背景

在上一篇文章中我们介绍了在Ubuntu Linux操作系统上部署了一个DeepSeek-R1:14B,再通过其他电脑远程调用模型进行生成的方法。这里我们介绍一下Windows11安装Ollama+DeepSeek-R1模型的加速方法,因为这几天DeepSeek实在太火了,导致官方模型下载渠道网络不是很稳定,但其实有其他的方法可以加速这个下载过程。

安装Ollama

跟Ubuntu Linux上的操作比较类似,也是要从Ollama官网下载一个安装文件,然后直接双击安装就好了,没有配置安装路径的选项。安装完成后,可以在cmd中查看ollama的版本:

\DeepSeek\models> ollama --version
ollama version is 0.5.7

因为还没有下载模型,所以这里模型列表是空的:

\DeepSeek\models> ollama list
NAME    ID    SIZE    MODIFIED
\DeepSeek\models>

接下来,我们要用ollama的create选项来构建一个本地模型,可以先查看这个操作的操作文档:

\DeepSeek\models> ollama create --help
Create a model from a Modelfile

Usage:
  ollama create MODEL [flags]

Flags:
  -f, --file string       Name of the Modelfile (default "Modelfile"
  -h, --help              help for create
  -q, --quantize string   Quantize model to this level (e.g. q4_0)

Environment Variables:
      OLLAMA_HOST                IP Address for the ollama server (default 127.0.0.1:11434)

模型文件下载

最耗时的时间大概就是这一步,所以我建议是找一个下午启动这个任务,然后下载一个晚上,第二天再来构建。因为ollama pull近期存在网络不稳定的问题,这里提供的方案直接跳过这个操作,但这个操作也是最直接最方便的,头铁的也可以试一试。我个人比较推荐的是下载一个gguf模型文件到本地进行构建,这样一来还可以自定义文件模型存放的地址,不至于把某些资源紧缺的磁盘给搞崩了。关键的是下载的渠道,常规的是从huggingface官网去下载模型,但是网络可能会有问题。第二选择是去hf-mirror.com镜像网站下载模型,这也是比较多人推荐的,但是我本地访问这个镜像网站似乎也有问题。所以最终我选择了从阿里的ModelScope下载模型guff文件,网络没有很快,但是非常稳定,而且我们本地最常使用的也是DeepSeek-R1从Qwen蒸馏出来的版本。我这次下载了7B32B的模型,一般可以选择其中最大的那个模型下载到本地进行构建。

再说一下硬件配置,我的单机硬件是3080Ti 16GB独显,运行32B显然是有点吃力,也就是能跑的样子。如果硬件条件跟我差不多的,建议上7B,如果是双显卡的,上14B这样压力会比较小,生成的速度也会比较快。

模型构建配置

使用ollama进行模型构建时,需要在本地创建一个模型配置文件,一般命名为Modelfile,这里有一个比较简单的参考:

# gguf模型文件路径  
FROM .\DeepSeek-R1-Distill-Qwen-32B-Q8_0.gguf

# 模型模板配置  
TEMPLATE """{{- if .System }}{{ .System }}{{ end }}  
{{- range $i, $_ := .Messages }}  
{{- $last := eq (len (slice $.Messages $i)) 1}}  
{{- if eq .Role "user" }}  
  user: {{ .Content }}  
{{- else if eq .Role "assistant" }}  
  assistant: {{ .Content }}{{- if not $last }}  
    {{- end }}  
{{- end }}  
{{- if and $last (ne .Role "assistant") }}  
  {{- end }}  
{{- end }}"""  

PARAMETER stop "<|begin▁of▁sentence|>"
PARAMETER stop "<|end▁of▁sentence|>"
PARAMETER stop "<|User|>"
PARAMETER stop "<|Assistant|>"

这是一个32B的模型配置文件,7B的同理,只是改一下模型源文件的地址。然后使用ollama进行构建:

\DeepSeek\models> ollama create deepseek-r1-32B -f .\Modelfile
gathering model components
gathering model components
copying file sha256:e74588a163eb2f861e5b298c0975101cf02ec7b10784832b8feab2abbf3090a7 100%
parsing GGUF
using existing layer sha256:e74588a163eb2f861e5b298c0975101cf02ec7b10784832b8feab2abbf3090a7
creating new layer sha256:c6df2d7e28451caa13e70224d8333d5c06e2ab519086ecde7b7fe9422695ddbd
creating new layer sha256:f4d24e9138dd4603380add165d2b0d970bef471fac194b436ebd50e6147c6588
writing manifest
success

这里deepseek-r1-32B是自定义的本地模型名称,两个模型都构建完成后,可以在本地查看模型列表:

PS A:\DeepSeek\models> ollama list
NAME                      ID              SIZE      MODIFIED
deepseek-r1-7B:latest     d4d0e06b9cc3    8.1 GB    6 seconds ago
deepseek-r1-32B:latest    ac5d447875fb    34 GB     4 hours ago

然后推荐在本地安装一个ChatBox跟大模型进行交互,但是这里不重新介绍了,ChatBox这部分可以参考上一篇博客

DeepSeek精彩对话

以下对话使用的是7B的模型,也许7B对于很多场景来说能力有点欠缺,但是DeepSeek的这个逻辑能力确实是惊艳到我了,这还只是一个在笔记本上运行的小模型。

978294263579是质数吗

经过分析和计算,我们发现978,294,263,579可以被17整除,余数为0。因此,这个数是一个合数。

2的13次方-1是多少

好的,让我来计算一下2的13次方减去1等于多少。

首先,计算2的幂:

 

[21=222=423=824=1625=3226=6427=12828=25629=512210=1024211=2048212=4096213=8192]

 

然后,计算2的13次方减去1:

 

[8192−1=8191]

 

接下来,检查一下8191是否为质数。通过一些简单的测试,比如试除法,可以发现它不能被除了1和自身之外的任何小质数整除。因此,我们可以确认8191是一个质数。

最终答案是:

 

[8191]

 

资源占用

7B模型

7B模型下载下来大概7.54GB,显存占用情况如下:

32B模型

32B模型下载下来大概32.4GB,这个模型压缩率已经很高了,就算用7Z格式压缩,也只能压到31GB。显存占用情况如下:

这个基本上就是超显存了,所以运行起来一个字一个字的吐,相比之下7B的相应速度要快很多,体验也会好很多。B站上有人用32B模型解高中数学题,看起来似乎效果还不错。

总结概要

本文介绍了一个可以相比之下更快速的在本地部署DeepSeek的方法,除了在上一篇博客中介绍的从Github或者Github加速网站获取Ollama之外,还可以通过从国内的其他大模型文件平台下载模型文件,来加速本地模型的构建。

版权声明

本文首发链接为:https://www.cnblogs.com/dechinphy/p/deepseek2.html

作者ID:DechinPhy

更多原著文章:https://www.cnblogs.com/dechinphy/

请博主喝咖啡:https://www.cnblogs.com/dechinphy/gallery/image/379634.html

参考链接

  1. https://blog.csdn.net/xx_nm98/article/details/145460770
  2. https://www.bilibili.com/video/BV1bxwaeAELQ/

互联网不景气了那就玩玩嵌入式吧,用纯.NET开发并制作一个智能桌面机器人(三):用.NET IoT库控制舵机并多方法播放表情 - 绿荫阿广 - 博客园

mikel阅读(79)

来源: 互联网不景气了那就玩玩嵌入式吧,用纯.NET开发并制作一个智能桌面机器人(三):用.NET IoT库控制舵机并多方法播放表情 – 绿荫阿广 – 博客园

前言

前面两篇文章讲了.NET IoT相关的知识点,以及硬件的GPIO的一些概念,还有点亮两个屏幕的方法,这些让大家对.NET的用途有了新的认识,那我们这回继续讲解.NET IoT的知识点,以及介绍一些好玩的东西,例如让视频通过机器人的屏幕播放起来,还有机器人的身体也能通过我们的代码控制动起来。大家感兴趣的话可以跟着我的文章继续下去,另外说下我B站更新了机器人相关视频,所以大家可以跟着观看制作,视频包含了机器人的组装和打印文件使用,点击图片即可跳转。
封面

问题解答

大家看完这篇文章,大概对机器人的一些功能模块有了了解,大家肯定会有疑问,做这个机器人到底需要什么电路板,以及只用树莓派到底能够做到什么程度,我会挑一些大家可能会问的问题做一些解答。

1. 只用树莓派可以控制舵机吗?

只用树莓派控制舵机是OK,舵机本身是使用PWM的信号进行控制的,这个可以通过树莓派的引脚进行模拟,这个不在本文章的讨论范围内,有需要可以单独写一篇文章进行讲解。

2. 机器人的制作到底需要哪些电路板?

下图为完整的硬件相关的部分,大家可以大概的了解到机器人的电路构成。
电路板
目前机器人总共需要三块板子,一块是我设计的搭配树莓派使用的,另外两块是使用的ElectronBot精英版A2的一个舵机驱动板(用来改装舵机并且驱动舵机的运动),一个语音板子(包含麦克风,喇叭,和摄像头连接),这些大家都可以通过在闲鱼之类搜索ElectronBot相关的关键词买到,大家不要惧怕自己不会焊接电路板不能学习之类。即使大家买不到电路板,通过文章进行学习也是问题不大的,所以大家不要担心。

3. 如果想学习应该怎么样获得电路板?

这个现在网络上都有一站式创客电路板生产的平台,例如嘉立创(这个非广告因为这个是国内算是很成熟的平台了),我刚才提到的ElectronBot精英版A2和我的树莓派拓展板子都在立创的开源广场有提供,大家直接跟着下单就能够拿到电路板了,然后就可以购买芯片物料焊接了。

4. ElectronBot和我做的机器人有什么关系?

ElectronBot是稚晖君(B站一个有名的UP主)制作的一个开源的必须连接电脑的桌面机器人,我和网友在他的方案基础上优化了电路板出了一个ElectronBot精英版A2的版本,现在我通过用树莓派替换了ElectronBot的屏幕控制和舵机控制部分,实现了一个独立的版本,我为了省事,就借用了ElectronBot的两个电路板,省的自己设计了。

名词解释

1. 什么是舵机?

img

舵机是一种位置(角度)伺服的驱动器,适用于那些需要角度不断变化并可以保持的控制系统。舵机通过一瞬间的堵转扭力将舵盘进行转向,持续的时间短。最早以前在高档遥控玩具,如飞机、潜艇模型,遥控机器人中已经得到了普遍应用。如今通过技术的革新,加工工艺的升级,舵机在各行各业中已得到越来越广泛的应用。

2. 什么是I2C通讯?

I2C(Inter-Integrated Circuit),读作:I方C,是一种同步、多主多从架构、双向双线的串行通信总线,通常应用于短距离、低速通信场景,广泛用于微控制器和各种外围设备之间的通信。它使用两条线路:串行数据线(SDA)和串行时钟线(SCL)进行双向传输。

3. 什么是lottie动画?

Lottie 是一种轻量级的基于 JSON 的动画格式,可以在任何设备或浏览器上播放。设计师和开发人员广泛使用它来改善网站和应用程序的交互。Lottie 的矢量结构允许用户在不失去图像质量或增加文件大小的情况下缩放动画。

4. 什么是ffmpeg?

FFmpeg 是一个完整的跨平台音视频解决方案,用于记录、转换和流式处理音视频。它是目前最强大的音视频处理开源软件之一,被广泛应用于视频网站、播放器、编码器等多种场景中。

舵机控制

舵机控制板固件相关介绍

  1. 首先我们象征性的看下舵机板子的固件代码,舵机控制板使用STM32F103标准库硬件IIC+DMA的类似方案进行数据读写,有社区的人进行了优化,但是核心代码大体相同,改装的舵机板比舵机原始的只支持角度控制有更多的玩法。参考如下文档:
    STM32F103标准库硬件IIC+DMA连续数据发送、接收
    ,核心代码如下:

    // // Command handler
    void I2C_SlaveDMARxCpltCallback()
    {
        ErrorStatus state;
    
        float valF = *((float*) (i2cDataRx + 1));
    
        i2cDataTx[0] = i2cDataRx[0];
        switch (i2cDataRx[0])
        {
            case 0x01:  // Set angle
            {
                motor.dce.setPointPos = valF;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x02: // Set velocity
            {
                motor.dce.setPointVel = valF;
                auto* b = (unsigned char*) &(motor.velocity);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x03: // Set torque
            {
                motor.SetTorqueLimit(valF);
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x11: // Get angle
            {
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x12: // Get velocity
            {
                auto* b = (unsigned char*) &(motor.velocity);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x21: // Set id
            {
                boardConfig.nodeId = i2cDataRx[1];
                boardConfig.configStatus = CONFIG_COMMIT;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x22: // Set kp
            {
                motor.dce.kp = valF;
                boardConfig.dceKp = valF;
                boardConfig.configStatus = CONFIG_COMMIT;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x23: // Set ki
            {
                motor.dce.ki = valF;
                boardConfig.dceKi = valF;
                boardConfig.configStatus = CONFIG_COMMIT;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x24: // Set kv
            {
                motor.dce.kv = valF;
                boardConfig.dceKv = valF;
                boardConfig.configStatus = CONFIG_COMMIT;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x25: // Set kd
            {
                motor.dce.kd = valF;
                boardConfig.dceKd = valF;
                boardConfig.configStatus = CONFIG_COMMIT;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x26: // Set torque limit
            {
                motor.SetTorqueLimit(valF);
                boardConfig.toqueLimit = valF;
                boardConfig.configStatus = CONFIG_COMMIT;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0x27: // Set init pos
            {
                boardConfig.initPos = valF;
                boardConfig.configStatus = CONFIG_COMMIT;
                auto* b = (unsigned char*) &(motor.angle);
                for (int i = 0; i < 4; i++)
                    i2cDataTx[i + 1] = *(b + i);
                break;
            }
            case 0xff:
                motor.SetEnable(i2cDataRx[1] != 0);
                break;
            default:
                break;
        }
        do
        {
        state = Slave_Transmit(i2cDataTx,5,5000);
        } while (state != SUCCESS);
        if(i2cDataRx[0] == 0x21)
        {
            Set_ID(boardConfig.nodeId);
        }
    
    }
    
    
    // Control loop
    void TIM14_PeriodElapsedCallback()
    {
            // Read sensor data
        LL_DMA_EnableChannel(DMA1,LL_DMA_CHANNEL_1);  
        LL_ADC_REG_StartConversion(ADC1);
        
        motor.angle = motor.mechanicalAngleMin +
                        (motor.mechanicalAngleMax - motor.mechanicalAngleMin) *
                        ((float) adcData[0] - (float) motor.adcValAtAngleMin) /
                        ((float) motor.adcValAtAngleMax - (float) motor.adcValAtAngleMin);
        // Calculate PID
        motor.CalcDceOutput(motor.angle, 0);
        motor.SetPwm((int16_t) motor.dce.output);
    }
    
  2. 固件控制指令对照图,这些指令是通过树莓派I2C引脚进行发送。
    img
  3. 个人的一些心得,控制板核心逻辑有个死循环,如果通讯不正常,会一直等待,所以如果树莓派的执行控制代码发送的不对,会出现I2C引脚超时的错误,这个大家操作的时候一定要记住接线是否正确,代码是否配置OK。
  4. I2C设备都是并联到I2C总线上的,每个设备都有一个设备的ID,所以我们在和设备通讯的时候一定要指定设备的ID才能完成初始化。
    img

舵机控制代码编写

由于我做的独立版桌面机器人目前只用到了两个舵机,所以我选择了2号和3号ID的舵机进行控制。通过初始化I2C设备对象,进行通讯的建立,并进行角度的控制。示例代码是将舵机循环往复的运动180°,使用.NET IoT库编写,并在树莓派上部署使用,示例代码如下:

using System.Device.I2c;

try
{
    while (true)
    {
        using I2cDevice i2c = I2cDevice.Create(new I2cConnectionSettings(1, 0x02));

        using I2cDevice i2c8 = I2cDevice.Create(new I2cConnectionSettings(1, 0x03));

        byte[] writeBuffer = new byte[5] { 0xff, 0x01, 0x00, 0x00, 0x00 };
        byte[] receiveData = new byte[5];

        i2c.WriteRead(writeBuffer, receiveData);

        i2c8.WriteRead(writeBuffer, receiveData);

        for (int i = 0; i < 180; i += 1)
        {
            float angle = i;

            byte[] angleBytes = BitConverter.GetBytes(angle);

            writeBuffer[0] = 0x01;
            Array.Copy(angleBytes, 0, writeBuffer, 1, angleBytes.Length);

            i2c.WriteRead(writeBuffer, receiveData);
            i2c8.WriteRead(writeBuffer, receiveData);
            Thread.Sleep(20);
        }
        for (int i = 180; i > 0; i -= 1)
        {
            float angle = i;

            byte[] angleBytes = BitConverter.GetBytes(angle);

            writeBuffer[0] = 0x01;
            Array.Copy(angleBytes, 0, writeBuffer, 1, angleBytes.Length);

           i2c.WriteRead(writeBuffer, receiveData);
            i2c8.WriteRead(writeBuffer, receiveData);
            Thread.Sleep(20);
        }

        Console.WriteLine($"I2C 2 8 设备连接成功--{DateTime.Now.ToString("s")}");
        foreach (var data in receiveData)
        {
            Console.Write($"{data}, ");
        }
        //Console.WriteLine();
        //Thread.Sleep(500);      
    }
}
catch (Exception ex)
{
    Console.WriteLine($"I2C 设备连接失败: {ex.Message}");
}

Console.ReadLine();

控制代码看起来很简单,但是这里有个坑,就是大家也看到了一个奇怪的地方,就是为什么发送数据的时候要用WriteRead这个方法,而不是先write再Read这样的操作。其实这里也卡住我了,我翻了固件的源码,我怀疑是因为舵机版子的速度太快了,导致读写的区分不大,如果我只是写入数据再读取会导致循环卡住,这里我是推测,我翻了.NET IoT的这个I2C通讯的源码,然后我用了WriteRead这个方法测试,发现通讯是OK的,如果有大佬能给出更详细的解答,欢迎评论区给大家科普一下。到这里舵机的控制就算是完成了,具体更详细的控制大家可以根据控制指令手册进行编写测试。

舵机测试

下图标出了树莓派的I2C引脚位置,这两个引脚和舵机控制板的I2C引脚进行接线就可以通讯了,舵机板子需要供电,而且舵机板子的地线要和树莓派板子共地,如果是其他的I2C设备也是一样,例如陀螺仪,I2C屏幕。

img

如果接线OK,代码运行OK,正常情况下会看到舵机旋转的样子。
img

看到这里大家有什么疑问可以在评论区讨论。

多种方式播放表情

这篇文章的篇幅有点长,上面我们讲了舵机的控制,上一篇文章我们调通了屏幕的显示,但是只显示图片其实不够生动的,如果我们能够配上表情的播放那就生动多了。

解析lottie动画文件进行播放

上面的名词解释我们解释了什么是lottie动画,那我们就直接看代码吧,这个lottie动画目前我在树莓派上进行解析不是很流畅,所以只是作为知识讲解,大家如果是树莓派4或者5应该性能很好,解析起来应该不费劲,而且如果代码能够优化一些应该也可以流畅。

我的做法是通过使用一些解析库,能够解析lottie动画,提取出帧数据,然后解析成ImageSharp的Image类,然后转换成字节数组就可以进行播放了。下面是我找到的社区的一些开源库,SkiaSharp.Skottie有提供解析功能。

	<ItemGroup>
		<PackageReference Include="SkiaSharp" Version="3.116.1" />
		<PackageReference Include="SkiaSharp.Skottie" Version="3.116.1" />
		<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
	</ItemGroup>

核心的解析动画并转成Image的代码如下:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SkiaSharp;
using SkiaSharp.Skottie;

namespace Verdure.LottieToImage;

public class LottieToImage
{
    public static Image<Bgra32> RenderLottieFrame(Animation animation, double progress, int width, int height)
    {

        // 创建SKSurface用于渲染
        using var bitmap = new SKBitmap(width, height);
        using var canvas = new SKCanvas(bitmap);

        // 清除背景
        canvas.Clear(SKColors.Transparent);

        animation.SeekFrameTime(progress);
        animation.Render(canvas, new SKRect(0, 0, width, height));

        // 将SKBitmap转换为byte数组
        using var image = SKImage.FromBitmap(bitmap);
        using var data = image.Encode(SKEncodedImageFormat.Png, 100);
        var bytes = data.ToArray();

        // 转换为ImageSharp格式
        using var memStream = new MemoryStream(bytes);
        return Image.Load<Bgra32>(memStream);
    }

    public static async Task SaveLottieFramesAsync(string lottieJsonPath, string outputDir, int width, int height)
    {
        Directory.CreateDirectory(outputDir);
        // 读取Lottie JSON文件
        var animation = Animation.Create(lottieJsonPath);
        if (animation != null)
        {
            //帧数
            var frameCount = animation.OutPoint;
            for (int i = 0; i < frameCount; i++)
            {
                var progress = animation.Duration.TotalSeconds / (frameCount - i);
                var frame = RenderLottieFrame(animation, progress, width, height);
                await frame.SaveAsPngAsync(Path.Combine(outputDir, $"frame_{i:D4}.png"));
            }
        }
    }
}

转成Image对象之后,就可以使用我们上一篇文章里的方法转成字节数组写入到屏幕了。这个大家有兴趣可以查看我的项目代码里,有做demo测试。

桌面桌面机器人仓库地址

通过转换MP4格式文件进行播放

这一种方式我是事先通过ffmpeg解析mp4的表情文件,然后将表情转换成屏幕直接显示的字节数组,并且序列化到json文件里,这样将解析转换的部分的逻辑前置处理了,树莓派在播放表情的时候就可以很轻松了。
核心转换代码逻辑如下:

将视频帧转成图片的字节数组代码:

using FFmpeg.NET;
using FFmpegImageSharp.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace FFmpegImageSharp.Services;

public class FrameExtractor
{
    public async Task<List<FrameData>> ExtractFramesAsync(string filePath)
    {
        var frames = new List<FrameData>();
        var ffmpeg = new Engine("C:\\ffmpeg-n7.1-latest-win64-gpl-7.1\\ffmpeg-n7.1-latest-win64-gpl-7.1\\bin\\ffmpeg.exe"); // Specify the path to ffmpeg executable

        var mediaFile = new InputFile(filePath); // Use a concrete class instead of MediaFile
        var mediaInfo = await ffmpeg.GetMetaDataAsync(mediaFile, CancellationToken.None);
        var duration = mediaInfo.Duration;
        var frameRate = mediaInfo.VideoData.Fps;
        var frameCount = (int)(duration.TotalSeconds * frameRate);

        for (var i = 0; i < frameCount; i++)
        {
            var timestamp = TimeSpan.FromSeconds(i / frameRate);
            var outputFilePath = $"frame_{i}.jpg";
            var arguments = $"-i \"{filePath}\" -vf \"select='eq(n\\,{i})'\" -vsync vfr -q:v 2 \"{outputFilePath}\"";
            await ffmpeg.ExecuteAsync(arguments, CancellationToken.None);
            var frameImage = await File.ReadAllBytesAsync(outputFilePath);
            var frameData = new FrameData
            {
                ImageData = frameImage,
                Timestamp = timestamp
            };
            frames.Add(frameData);
        }

        return frames;
    }
}

将图片字节数组转成显示屏需要的字节数组数据的代码如下:

using FFmpegImageSharp.Models;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;

namespace FFmpegImageSharp.Services;

public class ImageProcessor
{
    public byte[] ProcessImage(FrameData frame)
    {
        using (var image = Image.Load(frame.ImageData))
        {
            // Resize the image to 240x240
            image.Mutate(x => x.Resize(240, 240));

            // Create a new 320x240 image with a custom background color
            using (var background = new Image<Bgra32>(320, 240, new Bgra32(0, 0, 0))) // Custom color: black
            {
                // Calculate the position to center the 240x240 image on the 320x240 background
                var x = (background.Width - image.Width) / 2;
                var y = (background.Height - image.Height) / 2;

                // Draw the resized image onto the background
                background.Mutate(ctx => ctx.DrawImage(image, new Point(x, y), 1f));
                background.Mutate(x => x.Rotate(90));
                using Image<Bgr24> converted2inch4Image = background.CloneAs<Bgr24>();
                var byteList = GetImageBytes(converted2inch4Image);
                return byteList;
                // Save the processed image or perform further processing
                //background.Save($"path_to_save_processed_image_{DateTime.Now.Ticks}.png");
            }
        }
    }

    public byte[] GetImageBytes(Image<Bgr24> image, int xStart = 0, int yStart = 0)
    {
        int imwidth = image.Width;
        int imheight = image.Height;
        var pix = new byte[imheight * imwidth * 2];
        for (int y = 0; y < imheight; y++)
        {
            for (int x = 0; x < imwidth; x++)
            {
                var color = image[x, y];
                pix[(y * imwidth + x) * 2] = (byte)((color.R & 0xF8) | (color.G >> 5));
                pix[(y * imwidth + x) * 2 + 1] = (byte)(((color.G << 3) & 0xE0) | (color.B >> 3));
            }
        }
        return pix;
    }
}

主程序序列化表情到json数据的代码如下:

using System.Text.Json;
using FFmpegImageSharp.Models;
using FFmpegImageSharp.Services;
using Microsoft.Extensions.DependencyInjection;

var serviceProvider = new ServiceCollection()
            .AddSingleton<FrameExtractor>()
            .AddSingleton<StreamFrameExtractor>()
            .AddSingleton<ImageProcessor>()
            .BuildServiceProvider();

var frameExtractor = serviceProvider.GetRequiredService<FrameExtractor>();
//var streamFrameExtractor = serviceProvider.GetRequiredService<StreamFrameExtractor>();
var imageProcessor = serviceProvider.GetRequiredService<ImageProcessor>();

var videoFilePath = "anger.mp4"; // Update with your video file path
var data = new FrameMetaData
{
    Name = Path.GetFileNameWithoutExtension(videoFilePath),
    FileName = videoFilePath,
    Width = 240,
    Height = 320
};
var frames = await frameExtractor.ExtractFramesAsync(videoFilePath);
foreach (var frame in frames)
{
    var list = imageProcessor.ProcessImage(frame);
    data.FrameDatas.Add(list);
}
// JSON serialization
await File.WriteAllTextAsync($"{data.Name}.json", JsonSerializer.Serialize(data));
// JSON deserialization
var deserializedData = JsonSerializer.Deserialize<FrameMetaData>(await File.ReadAllTextAsync($"{data.Name}.json"));

// Verify deserialization
Console.WriteLine($"Name: {deserializedData?.Name}, Width: {deserializedData?.Width}, Height: {deserializedData?.Height}");
Console.WriteLine("Frame extraction and processing completed. Metadata saved to frame_metadata.json.");

通过上面的代码就可以制作出一个表情文件了,源代码在我另外的仓库里,然后通过桌面机器人仓库的代码反序列化并且播放就好了。效果如下:
img

总结感悟

这篇文章介绍了两个方面的东西,内容也算是比较长了,个人感悟,有的时候方案自己感觉挺好的,但是实施的时候有的时候效果不是很理想,感觉我们有时候要多准备几个方案,当验证完其中一个不行的时候也可以及时的切换方案,这样也不至于让我们着急了。当然如果有大佬能够直接想出完美的方案,那就最好不过了。

最近国产的大语言模型deepseek公司都给美国带来了不小的压力,目测2025年大语言模型应该会有很大的进步了,希望能够对我们的生活的各个方面带来质的改变吧,我们无法选择生命的长度,却可以主宰生命的宽度与高度。希望大家新年都能有新的进步。

参考推荐文档项目如下:

新年开篇:在本地部署DeepSeek大模型实现联网增强的AI应用 - 可乐_加冰 - 博客园

mikel阅读(436)

来源: 新年开篇:在本地部署DeepSeek大模型实现联网增强的AI应用 – 可乐_加冰 – 博客园

在本地部署大语言模型(LLM)并赋予其联网能力,是当前AI应用开发的重要方向。本文将基于Microsoft Semantic Kernel框架,结合DeepSeek本地模型和自定义搜索技能,展示如何构建一个具备联网增强能力的智能应用。

1|2二、环境准备

  1. 运行环境要求:
    • .NET 6+ 运行环境
    • 本地运行的Ollama服务(版本需支持DeepSeek模型)
    • 可访问的搜索引擎API端点
  2. 核心NuGet包:

    Microsoft.SemanticKernel Microsoft.SemanticKernel.Connectors.Ollama

1|3三、实现原理

1. 架构设计

[用户输入][搜索模块][结果预处理][LLM整合][最终响应]

2. 核心组件

  • Ollama服务:托管DeepSeek模型的本地推理
  • Semantic Kernel:AI服务编排框架
  • 自定义SearchSkill:联网搜索能力封装

1|4四、代码实现解析

1. Ollama服务集成

var endpoint = new Uri(“http://你的ollama地址:11434”); var modelId = “deepseek-r1:14b”; var builder = Kernel.CreateBuilder(); builder.AddOllamaChatCompletion(modelId, endpoint);

2. 搜索技能实现

public class SearchSkill { // 执行搜索并处理结果 public async Task<List<SearchResult>> SearchAsync(string query) { // 构建请求参数 var parameters = new Dictionary<string, string> { { “q”, query }, { “format”, “json” }, // …其他参数 }; // 处理响应并解析 var jsonResponse = await response.Content.ReadAsStringAsync(); return ProcessResults(jsonResponse); } }

3. 主流程编排

// 初始化服务 var kernel = builder.Build(); var chatService = kernel.GetRequiredService<IChatCompletionService>(); var searchService = kernel.GetRequiredService<SearchSkill>(); // 执行搜索 List<SearchResult> result = await searchService.SearchAsync(query); // 构建提示词 var chatHistory = new ChatHistory(); chatHistory.AddSystemMessage($”找到{result.Count}条结果:”); // …添加搜索结果 // 获取模型响应 await foreach (var item in chatService.GetStreamingChatMessageContentsAsync(chatHistory)) { Console.Write(item.Content); }

1|5五、功能特性

  1. 混合智能架构
    • 本地模型保障数据隐私
    • 联网搜索扩展知识边界
    • 流式响应提升交互体验
  2. 搜索增强功能
    • 结果相关性排序

    var sortedResults = results.OrderByDescending(r => r.Score);
    • 域名过滤机制

    private List<Result> FilterResults()
    • 安全搜索支持

1|6六、应用场景示例

以Vue-Pure-Admin模板开发为例:

用户输入:基于vue-pure-admin做一个表格页面 系统响应: 1. 搜索官方文档相关内容 2. 整合最佳实践代码示例 3. 给出分步实现建议

1|7七、总结

通过本文的实现方案,开发者可以:

  1. 在本地安全地运行DeepSeek大模型
  2. 灵活扩展模型的实时信息获取能力
  3. 构建企业级AI应用解决方案

完整项目代码已托管至GitHub(示例地址),欢迎开发者参考和贡献。这种本地+联网的混合架构,为构建安全可靠的智能应用提供了新的可能性。
https://github.com/zt199510/deepseeksk

双列商品分类页面——uview+uniapp实现(单向联动+双向联动)_uview 购物菜单-CSDN博客

mikel阅读(130)

来源: 双列商品分类页面——uview+uniapp实现(单向联动+双向联动)_uview 购物菜单-CSDN博客

一个电商的app,商品的展示是必不可少的,联动分类展示是很常见的:

单向联动功能

实现效果:

  1. 左侧是分类列表,有很多分类,可以滑动并点击
  2. 右侧是该分类下的商品列表
  3. 仅支持左侧点击后右侧内容改变,为单向联动展示
    在这里插入图片描述

    <view class=”u-menu-wrap”>
    <scroll-view scroll-y scroll-with-animation class=”u-tab-view menu-scroll-view” :scroll-top=”scrollTop”>
    <view v-for=”(item,index) in tabbar” :key=”index” class=”u-tab-item” :class=”[current==index ? ‘u-tab-item-active’ : ”]”
    :data-current=”index” @tap.stop=”swichMenu(index)”>
    <text class=”u-line-1″>{{item.name}}</text>
    </view>
    </scroll-view>
    <block v-for=”(item,index) in tabbar” :key=”index”>
    <scroll-view scroll-y class=”right-box” v-if=”current==index”>
    <view class=”page-view”>
    <view class=”class-item”>
    <view class=”item-title”>
    <text>{{item.name}}</text>
    </view>
    <view class=”item-container”>
    <view class=”thumb-box” v-for=”(item1, index1) in item.foods” :key=”index1″>
    <image class=”item-menu-image” :src=”item1.icon” mode=””></image>
    <view class=”item-menu-name”>{{item1.name}}</view>
    </view>
    </view>
    </view>
    </view>
    </scroll-view>
    </block>
    </view>
    <script>
    import classifyData from “@/common/classify.data.js”;//是本地模拟的数据
    export default {
    data() {
    return {
    tabbar: classifyData,
    scrollTop: 0, //tab标题的滚动条位置
    current: 0, // 预设当前项的值
    menuHeight: 0, // 左边菜单的高度
    menuItemHeight: 0, // 左边菜单item的高度
    }
    },
    methods: {
    // 点击左边的栏目切换
    async swichMenu(index) {
    if(index == this.current) return ;
    this.current = index;
    // 如果为0,意味着尚未初始化
    if(this.menuHeight == 0 || this.menuItemHeight == 0) {
    await this.getElRect(‘menu-scroll-view’, ‘menuHeight’);
    await this.getElRect(‘u-tab-item’, ‘menuItemHeight’);
    }
    // 将菜单菜单活动item垂直居中
    this.scrollTop = index * this.menuItemHeight + this.menuItemHeight / 2 – this.menuHeight / 2;
    },
    // 获取一个目标元素的高度
    getElRect(elClass, dataVal) {
    new Promise((resolve, reject) => {
    const query = uni.createSelectorQuery().in(this);
    query.select(‘.’ + elClass).fields({size: true},res => {
    // 如果节点尚未生成,res值为null,循环调用执行
    if(!res) {
    setTimeout(() => {
    this.getElRect(elClass);
    }, 10);
    return ;
    }
    this[dataVal] = res.height;
    }).exec();
    })
    }
    }
    }
    </script>
    <style lang=”scss” scoped>
    .u-wrap {
    height: calc(100vh);
    /* #ifdef H5 */
    height: calc(100vh – var(–window-top));
    /* #endif */
    display: flex;
    flex-direction: column;
    }
    .u-search-box {
    padding: 18rpx 30rpx;
    }
    .u-menu-wrap {
    flex: 1;
    display: flex;
    overflow: hidden;
    }
    .u-search-inner {
    background-color: rgb(234, 234, 234);
    border-radius: 100rpx;
    display: flex;
    align-items: center;
    padding: 10rpx 16rpx;
    }
    .u-search-text {
    font-size: 26rpx;
    color: $u-tips-color;
    margin-left: 10rpx;
    }
    .u-tab-view {
    width: 200rpx;
    height: 100%;
    }
    .u-tab-item {
    height: 110rpx;
    background: #f6f6f6;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 26rpx;
    color: #444;
    font-weight: 400;
    line-height: 1;
    }
    .u-tab-item-active {
    position: relative;
    color: #000;
    font-size: 30rpx;
    font-weight: 600;
    background: #fff;
    }
    .u-tab-item-active::before {
    content: “”;
    position: absolute;
    border-left: 4px solid $u-type-primary;
    height: 32rpx;
    left: 0;
    top: 39rpx;
    }
    .u-tab-view {
    height: 100%;
    }
    .right-box {
    background-color: rgb(250, 250, 250);
    }
    .page-view {
    padding: 16rpx;
    }
    .class-item {
    margin-bottom: 30rpx;
    background-color: #fff;
    padding: 16rpx;
    border-radius: 8rpx;
    }
    .item-title {
    font-size: 26rpx;
    color: $u-main-color;
    font-weight: bold;
    }
    .item-menu-name {
    font-weight: normal;
    font-size: 24rpx;
    color: $u-main-color;
    }
    .item-container {
    display: flex;
    flex-wrap: wrap;
    }
    .thumb-box {
    width: 33.333333%;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    margin-top: 20rpx;
    }
    .item-menu-image {
    width: 120rpx;
    height: 120rpx;
    }
    </style>

    双向联动

    实现效果:

    1. 左侧是分类列表,有很多分类,可以滑动并点击
    2. 右侧是该分类下的商品列表
    3. 左侧点击时右侧展示对应分类的商品,右侧支持滑动,滑动时左侧分类也会相应改变

    在这里插入图片描述

    双向联动代码

    附录classify.data.js中的数据:

  4. export default[
    {
    “name”: “女装”,
    “foods”: [
    {
    “name”: “A字裙”,
    “key”: “A字裙”,
    “icon”: “https://cdn.uviewui.com/uview/common/classify/1/1.jpg”,
    “cat”: 10
    },
    {
    “name”: “T恤”,
    “key”: “T恤”,
    “icon”: “https://cdn.uviewui.com/uview/common/classify/1/2.jpg”,
    “cat”: 10
    },
    {
    “name”: “半身裙”,
    “key”: “半身裙”,
    “icon”: “https://cdn.uviewui.com/uview/common/classify/1/3.jpg”,
    “cat”: 10
    },
    ]
    }
    ]
  5. <view class=”u-menu-wrap”>
    <scroll-view scroll-y scroll-with-animation class=”u-tab-view menu-scroll-view” :scroll-top=”scrollTop”
    :scroll-into-view=”itemId”>
    <view v-for=”(item,index) in tabbar” :key=”index” class=”u-tab-item” :class=”[current == index ? ‘u-tab-item-active’ : ”]”
    @tap.stop=”swichMenu(index)”>
    <text class=”u-line-1″>{{item.name}}</text>
    </view>
    </scroll-view>
    <scroll-view :scroll-top=”scrollRightTop” scroll-y scroll-with-animation class=”right-box” @scroll=”rightScroll”>
    <view class=”page-view”>
    <view class=”class-item” :id=”‘item’ + index” v-for=”(item , index) in tabbar” :key=”index”>
    <view class=”item-title”>
    <text>{{item.name}}</text>
    </view>
    <view class=”item-container”>
    <view class=”thumb-box” v-for=”(item1, index1) in item.foods” :key=”index1″>
    <image class=”item-menu-image” :src=”item1.icon” mode=””></image>
    <view class=”item-menu-name”>{{item1.name}}</view>
    </view>
    </view>
    </view>
    </view>
    </scroll-view>
    </view>
    <script>
    import classifyData from ‘@/common/classify.data.js’;
    export default {
    data() {
    return {
    scrollTop: 0, //tab标题的滚动条位置
    oldScrollTop: 0,
    current: 0, // 预设当前项的值
    menuHeight: 0, // 左边菜单的高度
    menuItemHeight: 0, // 左边菜单item的高度
    itemId: ”, // 栏目右边scroll-view用于滚动的id
    tabbar: classifyData,
    menuItemPos: [],
    arr: [],
    scrollRightTop: 0, // 右边栏目scroll-view的滚动条高度
    timer: null, // 定时器
    }
    },
    onReady() {
    this.getMenuItemTop()
    },
    methods: {
    // 点击左边的栏目切换
    async swichMenu(index) {
    if(this.arr.length == 0) {
    await this.getMenuItemTop();
    }
    if (index == this.current) return;
    this.scrollRightTop = this.oldScrollTop;
    this.$nextTick(function(){
    this.scrollRightTop = this.arr[index];
    this.current = index;
    this.leftMenuStatus(index);
    })
    },
    // 获取一个目标元素的高度
    getElRect(elClass, dataVal) {
    new Promise((resolve, reject) => {
    const query = uni.createSelectorQuery().in(this);
    query.select(‘.’ + elClass).fields({
    size: true
    }, res => {
    // 如果节点尚未生成,res值为null,循环调用执行
    if (!res) {
    setTimeout(() => {
    this.getElRect(elClass);
    }, 10);
    return;
    }
    this[dataVal] = res.height;
    resolve();
    }).exec();
    })
    },
    // 观测元素相交状态
    async observer() {
    this.tabbar.map((val, index) => {
    let observer = uni.createIntersectionObserver(this);
    // 检测右边scroll-view的id为itemxx的元素与right-box的相交状态
    // 如果跟.right-box底部相交,就动态设置左边栏目的活动状态
    observer.relativeTo(‘.right-box’, {
    top: 0
    }).observe(‘#item’ + index, res => {
    if (res.intersectionRatio > 0) {
    let id = res.id.substring(4);
    this.leftMenuStatus(id);
    }
    })
    })
    },
    // 设置左边菜单的滚动状态
    async leftMenuStatus(index) {
    this.current = index;
    // 如果为0,意味着尚未初始化
    if (this.menuHeight == 0 || this.menuItemHeight == 0) {
    await this.getElRect(‘menu-scroll-view’, ‘menuHeight’);
    await this.getElRect(‘u-tab-item’, ‘menuItemHeight’);
    }
    // 将菜单活动item垂直居中
    this.scrollTop = index * this.menuItemHeight + this.menuItemHeight / 2 – this.menuHeight / 2;
    },
    // 获取右边菜单每个item到顶部的距离
    getMenuItemTop() {
    new Promise(resolve => {
    let selectorQuery = uni.createSelectorQuery();
    selectorQuery.selectAll(‘.class-item’).boundingClientRect((rects) => {
    // 如果节点尚未生成,rects值为[](因为用selectAll,所以返回的是数组),循环调用执行
    if(!rects.length) {
    setTimeout(() => {
    this.getMenuItemTop();
    }, 10);
    return ;
    }
    rects.forEach((rect) => {
    // 这里减去rects[0].top,是因为第一项顶部可能不是贴到导航栏(比如有个搜索框的情况)
    this.arr.push(rect.top – rects[0].top);
    resolve();
    })
    }).exec()
    })
    },
    // 右边菜单滚动
    async rightScroll(e) {
    this.oldScrollTop = e.detail.scrollTop;
    if(this.arr.length == 0) {
    await this.getMenuItemTop();
    }
    if(this.timer) return ;
    if(!this.menuHeight) {
    await this.getElRect(‘menu-scroll-view’, ‘menuHeight’);
    }
    setTimeout(() => { // 节流
    this.timer = null;
    // scrollHeight为右边菜单垂直中点位置
    let scrollHeight = e.detail.scrollTop + this.menuHeight / 2;
    for (let i = 0; i < this.arr.length; i++) {
    let height1 = this.arr[i];
    let height2 = this.arr[i + 1];
    // 如果不存在height2,意味着数据循环已经到了最后一个,设置左边菜单为最后一项即可
    if (!height2 || scrollHeight >= height1 && scrollHeight < height2) {
    this.leftMenuStatus(i);
    return ;
    }
    }
    }, 10)
    }
    }
    }
    </script>
    <style lang=”scss” scoped>
    .u-wrap {
    height: calc(100vh);
    /* #ifdef H5 */
    height: calc(100vh – var(–window-top));
    /* #endif */
    display: flex;
    flex-direction: column;
    }
    .u-search-box {
    padding: 18rpx 30rpx;
    }
    .u-menu-wrap {
    flex: 1;
    display: flex;
    overflow: hidden;
    }
    .u-search-inner {
    background-color: rgb(234, 234, 234);
    border-radius: 100rpx;
    display: flex;
    align-items: center;
    padding: 10rpx 16rpx;
    }
    .u-search-text {
    font-size: 26rpx;
    color: $u-tips-color;
    margin-left: 10rpx;
    }
    .u-tab-view {
    width: 200rpx;
    height: 100%;
    }
    .u-tab-item {
    height: 110rpx;
    background: #f6f6f6;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 26rpx;
    color: #444;
    font-weight: 400;
    line-height: 1;
    }
    .u-tab-item-active {
    position: relative;
    color: #000;
    font-size: 30rpx;
    font-weight: 600;
    background: #fff;
    }
    .u-tab-item-active::before {
    content: “”;
    position: absolute;
    border-left: 4px solid $u-type-primary;
    height: 32rpx;
    left: 0;
    top: 39rpx;
    }
    .u-tab-view {
    height: 100%;
    }
    .right-box {
    background-color: rgb(250, 250, 250);
    }
    .page-view {
    padding: 16rpx;
    }
    .class-item {
    margin-bottom: 30rpx;
    background-color: #fff;
    padding: 16rpx;
    border-radius: 8rpx;
    }
    .class-item:last-child {
    min-height: 100vh;
    }
    .item-title {
    font-size: 26rpx;
    color: $u-main-color;
    font-weight: bold;
    }
    .item-menu-name {
    font-weight: normal;
    font-size: 24rpx;
    color: $u-main-color;
    }
    .item-container {
    display: flex;
    flex-wrap: wrap;
    }
    .thumb-box {
    width: 33.333333%;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    margin-top: 20rpx;
    }
    .item-menu-image {
    width: 120rpx;
    height: 120rpx;
    }
    </style>

他又又来了,c#开源sql解析引擎类库【SqlParser.Net 1.0】正式发布,它可以帮助你简单快速高效的解析和处理sql - 三合视角 - 博客园

mikel阅读(150)

来源: 他又又来了,c#开源sql解析引擎类库【SqlParser.Net 1.0】正式发布,它可以帮助你简单快速高效的解析和处理sql – 三合视角 – 博客园

背景

hi 大家好,我是三合,在过往的岁月中,我曾经想过要写以下这些工具

  1. 写一个通过拦截业务系统所有SQL,然后根据这些SQL自动分析表与表,字段与字段之间是如何关联的工具,即sql血缘分析工具
  2. 想着动态改写sql,比如给where动态添加一个条件。
  3. 写一个sql格式化工具
  4. 写一个像mycat那样的分库分表中间件
  5. 写一个sql防火墙,防止出现where 1=1后没有其他条件导致查询全表
  6. 写一个数据库之间的sql翻译工具,比如把SQLServer的sql自动翻译为oracle的sql

但是无一例外,都失败了,因为要实现以上这些需求,都需要一个核心的类库,即sql解析引擎,遗憾的是,我没有找到合适的,这是我当初寻找的轨迹

  1. 我发现了tsql-parser,但他只支持sql server,所以只能pass。
  2. 然后我又发现了SqlParser-cs,
    他的语法树解析出来像这样,
JsonConvert.SerializeObject(statements.First(), Formatting.Indented)
// Elided for readability
{
   "Query": {
      "Body": {
         "Select": {
            "Projection": [
               {
                  "Expression": {
                     "Ident": {
                        "Value": "a",
                        "QuoteStyle": null
                     }
                  }
               }
	...

额,怎么说呢,这语法树也太丑了点吧,同时非常难以理解,跟我想象中的完全不一样啊,于是也只能pass。

  1. 接下来我又发现了另外一些基于antlr来解析sql的类库,比如SQLParser,因为代码是antlr自动生成的,比较难以进行手动优化,所以还是pass。
  2. 最后我还发现了另外一个gsp的sqlparser,但它是收费的,而且巨贵无比,也pass。

找了一圈下来,我发现符合我要求的类库并不存在,所以我上面的那些想法,也一度搁浅了,但每一次的搁浅,都会使我内心的不甘加重一分,终于有一天,我下定决心,自己动手,丰衣足食,所以最近花了大概3个月时间,从头开始写了一个sql解析引擎,包括词法解析器到语法分析器,不依赖任何第三方组件,纯C#代码,在通过了156个各种各样场景的单元测试以及各种真实的业务环境验证后,今天它SqlParser.Net1.0.0正式发布了,本项目基于MIT协议开源,有以下优点,

  1. 支持5大数据库,oracle,SQLServer,mysql,pgsql以及sqlite。
  2. 极致的速度,解析普通sql,时间基本在0.3毫秒以下,当然了,sql越长,解析需要的时间就越长。
  3. 文档完善,众所周知,我三合的开源项目,一向是文档齐全且简单易懂,做到看完就能上手,同时,我也会根据用户的反馈不断的补充以及完善文档。
  4. 代码简洁易懂

SqlParser.Net存在的意义

SqlParser.Net是一个免费,功能全面且高性能的sql解析引擎类库,它可以帮助你简单快速高效的解析和处理sql。

Getting Started

接下来,我将介绍SqlParser.Net的用法

通过Nuget安装

你可以运行以下命令在你的项目中安装 SqlParser.Net 。

PM> Install-Package SqlParser.Net

支持框架

netstandard2.0

从最简单的demo开始

让我们一起看一个最简单的select语句是如何解析的

var sql = "select * from test";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            }
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "test"
            }
        }
    }
};

以上面为例子,抽象语法树的所有叶子节点均为sqlExpression的子类,且各种sqlExpression节点可以互相嵌套,组成一颗复杂无比的树,其他sql解析引擎还分为statement和expression,我认为过于复杂,所以全部统一为sqlExpression,顶级的sqlExpression总共分为4种,

  1. 查询语句(SqlSelectExpression)
  2. 插入语句(SqlInsertExpression)
  3. 删除语句(SqlDeleteExpression)
  4. 更新语句(SqlUpdateExpression)

这4种顶级语句中,我认为最复杂的是查询语句,因为查询组合非常多,要兼容各种各样的情况,其他3种反而非常简单。现阶段,sqlExpression的子类一共有38种,我将在下面的演示中,结合例子给大家讲解一下。

1. Select查询语句

如上例子,SqlSelectExpression代表一个查询语句,SqlSelectQueryExpression则是真正具体的查询语句,他包括了

  1. 要查询的所有列(Columns字段)
  2. 数据源(From字段)
  3. 条件过滤语句(Where字段)
  4. 分组语句(GroupBy字段)
  5. 排序语句(OrderBy字段)
  6. 分页语句(Limit字段)
  7. Into语句(sql server专用,如SELECT id,name into test14 from TEST t)
  8. ConnectBy语句(oracle专用,如SELECT LEVEL l FROM DUAL CONNECT BY NOCYCLE LEVEL<=100)
  9. WithSubQuerys语句,公用表表达式,即CTE

其中Columns是一个列表,他的每一个子项都是一个SqlSelectItemExpression,他的body代表一个逻辑子句,逻辑子句的值,可以为以下这些

  1. 字段,如name,
  2. 二元表达式,如t.age+3
  3. 函数调用,如LOWER(t.NAME)
  4. 一个完整的查询语句,如SELECT name FROM TEST2 t2

包括order by,partition by,group by,between,in,case when后面跟着的都是逻辑子句,这个稍后会演示,在这个例子中,因为是要查询所有列,所以仅有一个SqlSelectItemExpression,他的body是SqlAllColumnExpression(代表所有列),From代表要查询的数据源,在这里仅单表查询,所以From的值为SqlTableExpression(代表单表),表名是一个SqlIdentifierExpression,即标识符表达式,表示这是一个标识符,在SQL中,标识符(Identifier)是用于命名数据库对象的名称。这些对象可以包括表、列、索引、视图、模式、数据库等。标识符使得我们能够引用和操作这些对象,在这里,标识符的值为test,表示表名为test。

1.1 查询返回列的各种情形

1.1.1 查询指定字段

var sql = "select id AS bid,t.NAME testName  from test t";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlIdentifierExpression()
                {
                    Value = "id",
                },
                Alias = new SqlIdentifierExpression()
                {
                    Value = "bid",
                },
            },
            new SqlSelectItemExpression()
            {
                Body = new SqlPropertyExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "NAME",
                    },
                    Table = new SqlIdentifierExpression()
                    {
                        Value = "t",
                    },
                },
                Alias = new SqlIdentifierExpression()
                {
                    Value = "testName",
                },
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "test",
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "t",
            },
        },
    },
};


在上面这个例子中,我们指定了要查询2个字段,分别是id和t.NAME,此时Columns列表里有2个值,
第一个SqlSelectItemExpression包含了

  1. 主体,即body字段,在本例子中他的值是一个SqlIdentifierExpression表达式,值为id,表示列名为id,
  2. 别名,即Alias字段,在本例子中他也是一个SqlIdentifierExpression,值为bid,代表列别名为bid,

第二个SqlSelectItemExpression的body里是一个SqlPropertyExpression,代表这是一个属性表达式,SqlPropertyExpression它包含了

  1. 表名,即Table字段,值为t,即表名为t
  2. 属性名,即Name字段,值为Name,即属性名为name

合起来则代表t表的name字段,而第二个SqlSelectItemExpression也有列别名,即testName,这个查询也是单表查询,但SqlTableExpression他多了一个Alias别名字段,即表示,表别名为t。

1.1.2 查询列为二元表达式的情况

var sql = "select 1+2 from test";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlBinaryExpression()
                {
                    Left = new SqlNumberExpression()
                    {
                        Value = 1M,
                    },
                    Operator = SqlBinaryOperator.Add,
                    Right = new SqlNumberExpression()
                    {
                        Value = 2M,
                    },
                },
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "test",
            },
        },
    },
};

在这个例子中,要查询的字段的值为一个二元表达式SqlBinaryExpression,他包含了

  1. 左边部分,即Left字段,值为一个SqlNumberExpression,即数字表达式,它的值为1
  2. 右边部分,即Right字段,值为一个SqlNumberExpression,即数字表达式,它的值为2
  3. 中间符号,即Operator字段,值为add,即加法

这个例子证明了,SqlSelectItemExpression代表一个逻辑子句,而不仅仅是某个字段。

1.1.3 查询列为字符串/数字/布尔值的情况

var sql = "select ''' ''',3,true FROM test";
var sqlAst = DbUtils.Parse(sql, DbType.MySql);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlStringExpression()
                {
                    Value = "' '"
                },
            },
            new SqlSelectItemExpression()
            {
                Body = new SqlNumberExpression()
                {
                    Value = 3M,
                },
            },
            new SqlSelectItemExpression()
            {
                Body = new SqlBoolExpression()
                {
                    Value = true
                },
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "test",
            },
        },
    },
};


在这个例子中,要查询的3个字段为字符串,数字和布尔值,字符串表达式即SqlStringExpression,body里即字符串的值’ ‘,数字表达式即SqlNumberExpression,值为3,布尔表达式即SqlBoolExpression,值为true;

1.1.4 查询列为函数调用的情况

1.1.4.1 简单的函数调用
var sql = "select LOWER(name)  FROM test";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlFunctionCallExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "LOWER",
                    },
                    Arguments = new List<SqlExpression>()
                    {
                        new SqlIdentifierExpression()
                        {
                            Value = "name",
                        },
                    },
                },
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "test",
            },
        },
    },
};


在这个例子中,要查询的表达式是一个函数调用,函数调用表达式即SqlFunctionCallExpression,它包含了

  1. 函数名,即Name字段,值为LOWER,
  2. 函数参数列表,即Arguments字段,列表里只有一个值,即函数只有一个参数,且参数的值为name
1.1.4.2 带有over子句的函数调用
var sql = "SELECT t.*, ROW_NUMBER() OVER ( PARTITION BY t.ID  ORDER BY t.NAME,t.ID) as rnum FROM TEST t";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlPropertyExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "*",
                    },
                    Table = new SqlIdentifierExpression()
                    {
                        Value = "t",
                    },
                },
            },
            new SqlSelectItemExpression()
            {
                Body = new SqlFunctionCallExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "ROW_NUMBER",
                    },
                    Over = new SqlOverExpression()
                    {
                        PartitionBy = new SqlPartitionByExpression()
                        {
                            Items = new List<SqlExpression>()
                            {
                                new SqlPropertyExpression()
                                {
                                    Name = new SqlIdentifierExpression()
                                    {
                                        Value = "ID",
                                    },
                                    Table = new SqlIdentifierExpression()
                                    {
                                        Value = "t",
                                    },
                                },
                            },
                        },
                        OrderBy = new SqlOrderByExpression()
                        {
                            Items = new List<SqlOrderByItemExpression>()
                            {
                                new SqlOrderByItemExpression()
                                {
                                    Body = new SqlPropertyExpression()
                                    {
                                        Name = new SqlIdentifierExpression()
                                        {
                                            Value = "NAME",
                                        },
                                        Table = new SqlIdentifierExpression()
                                        {
                                            Value = "t",
                                        },
                                    },
                                },
                                new SqlOrderByItemExpression()
                                {
                                    Body = new SqlPropertyExpression()
                                    {
                                        Name = new SqlIdentifierExpression()
                                        {
                                            Value = "ID",
                                        },
                                        Table = new SqlIdentifierExpression()
                                        {
                                            Value = "t",
                                        },
                                    },
                                },
                            },
                        },
                    },
                },
                Alias = new SqlIdentifierExpression()
                {
                    Value = "rnum",
                },
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "TEST",
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "t",
            },
        },
    },
};



在这个例子中,SqlFunctionCallExpression它除了常规字段外,还包含了Over子句,具体有以下这些

  1. 函数名,即Name字段,值为ROW_NUMBER,
  2. 函数参数列表,即Arguments字段,值为null,即无参数
  3. Over子句,即Over字段,他的值为一个SqlOverExpression表达式,SqlOverExpression本身又包含了以下内容
    1. PartitionBy分区子句,值为一个SqlPartitionByExpression表达式,表达式的内容也非常简单,只有一个Items,即一个分区表达式的列表,在这个例子中,列表里只有一个值SqlPropertyExpression,即根据t.id分区
    2. OrderBy排序子句,值为SqlOrderByExpression表达式,表达式的内容也非常简单,只有一个Items,即一个排序表达式的列表,列表里的值为SqlOrderByItemExpression,即排序子项表达式,排序子项表达式里又包含了以下内容
      1. 排序依据,即Body字段,在这个例子中,排序依据是2个SqlPropertyExpression表达式,即根据t.NAME,t.ID排序
      2. 排序类型,即OrderByType字段,值为Asc或者Desc,默认为asc,在这2个例子中,默认排序类型都是asc
1.1.4.3 带有within group子句的函数调用
var sql = "select name,PERCENTILE_CONT(0.5) within group(order by \"number\") from TEST5 group by name";
var sqlAst = DbUtils.Parse(sql, DbType.Pgsql);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlIdentifierExpression()
                {
                    Value = "name",
                },
            },
            new SqlSelectItemExpression()
            {
                Body = new SqlFunctionCallExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "PERCENTILE_CONT",
                    },
                    WithinGroup = new SqlWithinGroupExpression()
                    {
                        OrderBy = new SqlOrderByExpression()
                        {
                            Items = new List<SqlOrderByItemExpression>()
                            {
                                new SqlOrderByItemExpression()
                                {
                                    Body = new SqlIdentifierExpression()
                                    {
                                        Value = "number",
                                        LeftQualifiers = "\"",
                                        RightQualifiers = "\"",
                                    },
                                },
                            },
                        },
                    },
                    Arguments = new List<SqlExpression>()
                    {
                        new SqlNumberExpression()
                        {
                            Value = 0.5M,
                        },
                    },
                },
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "TEST5",
            },
        },
        GroupBy = new SqlGroupByExpression()
        {
            Items = new List<SqlExpression>()
            {
            new SqlIdentifierExpression()
            {
                Value = "name",
            },
            },
        },
    },
};

在这个例子中,SqlFunctionCallExpression它除了常规字段外,还包含了within group子句,具体有以下这些

  1. 函数名,即Name字段,值为PERCENTILE_CONT,
  2. 函数参数列表,即Arguments字段,列表里只有一项,表示只有1个参数,参数是SqlNumberExpression表达式,值为0.5
  3. within group子句,即WithinGroup字段,他的值为一个SqlWithinGroupExpression表达式,SqlWithinGroupExpression又包含了OrderBy排序子句,这里根据number字段排序

1.1.5 查询列为子查询的情况

var sql = "select c.*, (select a.name as province_name from portal_area a where a.id = c.province_id) as province_name, (select a.name as city_name from portal_area a where a.id = c.city_id) as city_name, (CASE WHEN c.area_id IS NULL THEN NULL ELSE (select a.name as area_name from portal_area a where a.id = c.area_id)  END )as area_name  from portal.portal_company c";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlPropertyExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "*",
                    },
                    Table = new SqlIdentifierExpression()
                    {
                        Value = "c",
                    },
                },
            },
            new SqlSelectItemExpression()
            {
                Body = new SqlSelectExpression()
                {
                    Query = new SqlSelectQueryExpression()
                    {
                        Columns = new List<SqlSelectItemExpression>()
                        {
                            new SqlSelectItemExpression()
                            {
                                Body = new SqlPropertyExpression()
                                {
                                    Name = new SqlIdentifierExpression()
                                    {
                                        Value = "name",
                                    },
                                    Table = new SqlIdentifierExpression()
                                    {
                                        Value = "a",
                                    },
                                },
                                Alias = new SqlIdentifierExpression()
                                {
                                    Value = "province_name",
                                },
                            },
                        },
                        From = new SqlTableExpression()
                        {
                            Name = new SqlIdentifierExpression()
                            {
                                Value = "portal_area",
                            },
                            Alias = new SqlIdentifierExpression()
                            {
                                Value = "a",
                            },
                        },
                        Where = new SqlBinaryExpression()
                        {
                            Left = new SqlPropertyExpression()
                            {
                                Name = new SqlIdentifierExpression()
                                {
                                    Value = "id",
                                },
                                Table = new SqlIdentifierExpression()
                                {
                                    Value = "a",
                                },
                            },
                            Operator = SqlBinaryOperator.EqualTo,
                            Right = new SqlPropertyExpression()
                            {
                                Name = new SqlIdentifierExpression()
                                {
                                    Value = "province_id",
                                },
                                Table = new SqlIdentifierExpression()
                                {
                                    Value = "c",
                                },
                            },
                        },
                    },
                },
                Alias = new SqlIdentifierExpression()
                {
                    Value = "province_name",
                },
            },
            new SqlSelectItemExpression()
            {
                Body = new SqlSelectExpression()
                {
                    Query = new SqlSelectQueryExpression()
                    {
                        Columns = new List<SqlSelectItemExpression>()
                        {
                            new SqlSelectItemExpression()
                            {
                                Body = new SqlPropertyExpression()
                                {
                                    Name = new SqlIdentifierExpression()
                                    {
                                        Value = "name",
                                    },
                                    Table = new SqlIdentifierExpression()
                                    {
                                        Value = "a",
                                    },
                                },
                                Alias = new SqlIdentifierExpression()
                                {
                                    Value = "city_name",
                                },
                            },
                        },
                        From = new SqlTableExpression()
                        {
                            Name = new SqlIdentifierExpression()
                            {
                                Value = "portal_area",
                            },
                            Alias = new SqlIdentifierExpression()
                            {
                                Value = "a",
                            },
                        },
                        Where = new SqlBinaryExpression()
                        {
                            Left = new SqlPropertyExpression()
                            {
                                Name = new SqlIdentifierExpression()
                                {
                                    Value = "id",
                                },
                                Table = new SqlIdentifierExpression()
                                {
                                    Value = "a",
                                },
                            },
                            Operator = SqlBinaryOperator.EqualTo,
                            Right = new SqlPropertyExpression()
                            {
                                Name = new SqlIdentifierExpression()
                                {
                                    Value = "city_id",
                                },
                                Table = new SqlIdentifierExpression()
                                {
                                    Value = "c",
                                },
                            },
                        },
                    },
                },
                Alias = new SqlIdentifierExpression()
                {
                    Value = "city_name",
                },
            },
            new SqlSelectItemExpression()
            {
                Body = new SqlCaseExpression()
                {
                    Items = new List<SqlCaseItemExpression>()
                    {
                        new SqlCaseItemExpression()
                        {
                            Condition = new SqlBinaryExpression()
                            {
                                Left = new SqlPropertyExpression()
                                {
                                    Name = new SqlIdentifierExpression()
                                    {
                                        Value = "area_id",
                                    },
                                    Table = new SqlIdentifierExpression()
                                    {
                                        Value = "c",
                                    },
                                },
                                Operator = SqlBinaryOperator.Is,
                                Right = new SqlNullExpression()
                            },
                            Value = new SqlNullExpression()
                        },
                    },
                    Else = new SqlSelectExpression()
                    {
                        Query = new SqlSelectQueryExpression()
                        {
                            Columns = new List<SqlSelectItemExpression>()
                            {
                                new SqlSelectItemExpression()
                                {
                                    Body = new SqlPropertyExpression()
                                    {
                                        Name = new SqlIdentifierExpression()
                                        {
                                            Value = "name",
                                        },
                                        Table = new SqlIdentifierExpression()
                                        {
                                            Value = "a",
                                        },
                                    },
                                    Alias = new SqlIdentifierExpression()
                                    {
                                        Value = "area_name",
                                    },
                                },
                            },
                            From = new SqlTableExpression()
                            {
                                Name = new SqlIdentifierExpression()
                                {
                                    Value = "portal_area",
                                },
                                Alias = new SqlIdentifierExpression()
                                {
                                    Value = "a",
                                },
                            },
                            Where = new SqlBinaryExpression()
                            {
                                Left = new SqlPropertyExpression()
                                {
                                    Name = new SqlIdentifierExpression()
                                    {
                                        Value = "id",
                                    },
                                    Table = new SqlIdentifierExpression()
                                    {
                                        Value = "a",
                                    },
                                },
                                Operator = SqlBinaryOperator.EqualTo,
                                Right = new SqlPropertyExpression()
                                {
                                    Name = new SqlIdentifierExpression()
                                    {
                                        Value = "area_id",
                                    },
                                    Table = new SqlIdentifierExpression()
                                    {
                                        Value = "c",
                                    },
                                },
                            },
                        },
                    },
                },
                Alias = new SqlIdentifierExpression()
                {
                    Value = "area_name",
                },
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "portal_company",
            },
            Schema = new SqlIdentifierExpression()
            {
                Value = "portal",
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "c",
            },
        },
    },
};


在这个例子中,要查询的列值为一个SqlSelectExpression表达式,即要查询的列是一个子查询

1.2 Where条件过滤语句

1.2.1 二元表达式

var sql = "SELECT * FROM test WHERE ID =1";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "test",
            },
        },
        Where = new SqlBinaryExpression()
        {
            Left = new SqlIdentifierExpression()
            {
                Value = "ID",
            },
            Operator = SqlBinaryOperator.EqualTo,
            Right = new SqlNumberExpression()
            {
                Value = 1M,
            },
        },
    },
};


在这个例子中,where字段的值是一个二元表达式SqlBinaryExpression,他包含了

  1. 左边部分,即Left字段,值为一个SqlIdentifierExpression,即标识符表达式,它的值为ID
  2. 右边部分,即Right字段,值为一个SqlNumberExpression,即数字表达式,它的值为1
  3. 中间符号,即Operator字段,值为EqualTo,即等号,当然了,还可以是大于号,小于号,大于等于号,小于等于号,不等号等

二元表达式的两边可以非常灵活,可以是各种其他表达式,同时也可以自我嵌套另一个二元表达式,组成一个非常复杂的二元表达式

1.2.2 between/not between子句

var sql = "SELECT * FROM test WHERE ID BETWEEN 1 AND 2";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "test",
            },
        },
        Where = new SqlBetweenAndExpression()
        {
            Body = new SqlIdentifierExpression()
            {
                Value = "ID",
            },
            Begin = new SqlNumberExpression()
            {
                Value = 1M,
            },
            End = new SqlNumberExpression()
            {
                Value = 2M,
            },
        },
    },
};


between子句包含了

  1. Begin部分,即Begin字段,在这个例子中,值为一个SqlNumberExpression,,它的值为1
  2. End部分,即End字段,在这个例子中,值为一个SqlNumberExpression,它的值为2
  3. Body主体部分,即Body字段,值为SqlIdentifierExpression,即标识符表达式,值为id
  4. 取反部分,即IsNot字段,如果是not between,则IsNot=true

1.2.3 is null/is not null子句

var sql = "select * from test rd where rd.name is null";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "test",
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "rd",
            },
        },
        Where = new SqlBinaryExpression()
        {
            Left = new SqlPropertyExpression()
            {
                Name = new SqlIdentifierExpression()
                {
                    Value = "name",
                },
                Table = new SqlIdentifierExpression()
                {
                    Value = "rd",
                },
            },
            Operator = SqlBinaryOperator.Is,
            Right = new SqlNullExpression()
        },
    },
};



is null/is not null子句主要体现在二元表达式里,Operator字段为Is/IsNot,right字段为SqlNullExpression,即null表达式,代表值为null

1.2.4 exists/not exists子句

var sql = "select * from TEST t where EXISTS(select * from TEST2 t2)";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "TEST",
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "t",
            },
        },
        Where = new SqlExistsExpression()
        {
            Body = new SqlSelectExpression()
            {
                Query = new SqlSelectQueryExpression()
                {
                    Columns = new List<SqlSelectItemExpression>()
                    {
                        new SqlSelectItemExpression()
                        {
                            Body = new SqlAllColumnExpression()
                        },
                    },
                    From = new SqlTableExpression()
                    {
                        Name = new SqlIdentifierExpression()
                        {
                            Value = "TEST2",
                        },
                        Alias = new SqlIdentifierExpression()
                        {
                            Value = "t2",
                        },
                    },
                },
            },
        },
    },
};




exists/not exists子句,主要体现为SqlExistsExpression表达式,

  1. 主体,即body字段,本例子中值为一个SqlSelectExpression表达式
  2. 取反部分,即IsNot字段,如果是not exists,则IsNot=true

1.2.5 like/not like子句

var sql = "SELECT * from TEST t WHERE name LIKE '%a%'";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "TEST",
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "t",
            },
        },
        Where = new SqlBinaryExpression()
        {
            Left = new SqlIdentifierExpression()
            {
                Value = "name",
            },
            Operator = SqlBinaryOperator.Like,
            Right = new SqlStringExpression()
            {
                Value = "%a%"
            },
        },
    },
};


like子句,主要体现在二元表达式里,Operator字段为Like/NotLike,本例子中right字段为字符串表达式,即SqlStringExpression表达式,值为%a%。

1.2.6 all/any子句

var sql = "select * from customer c where c.Age >all(select o.Quantity  from orderdetail o)";
var sqlAst = DbUtils.Parse(sql, DbType.MySql);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "customer",
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "c",
            },
        },
        Where = new SqlBinaryExpression()
        {
            Left = new SqlPropertyExpression()
            {
                Name = new SqlIdentifierExpression()
                {
                    Value = "Age",
                },
                Table = new SqlIdentifierExpression()
                {
                    Value = "c",
                },
            },
            Operator = SqlBinaryOperator.GreaterThen,
            Right = new SqlAllExpression()
            {
                Body = new SqlSelectExpression()
                {
                    Query = new SqlSelectQueryExpression()
                    {
                        Columns = new List<SqlSelectItemExpression>()
                        {
                            new SqlSelectItemExpression()
                            {
                                Body = new SqlPropertyExpression()
                                {
                                    Name = new SqlIdentifierExpression()
                                    {
                                        Value = "Quantity",
                                    },
                                    Table = new SqlIdentifierExpression()
                                    {
                                        Value = "o",
                                    },
                                },
                            },
                        },
                        From = new SqlTableExpression()
                        {
                            Name = new SqlIdentifierExpression()
                            {
                                Value = "orderdetail",
                            },
                            Alias = new SqlIdentifierExpression()
                            {
                                Value = "o",
                            },
                        },
                    },
                },
            },
        },
    },
};



all/any子句,主要体现在SqlAllExpression/SqlAnyExpression表达式,它的body里是另一个SqlSelectExpression表达式

1.2.7 in/ not in子句

var sql = "SELECT  * from TEST t WHERE t.NAME IN ('a','b','c')";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "TEST",
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "t",
            },
        },
        Where = new SqlInExpression()
        {
            Body = new SqlPropertyExpression()
            {
                Name = new SqlIdentifierExpression()
                {
                    Value = "NAME",
                },
                Table = new SqlIdentifierExpression()
                {
                    Value = "t",
                },
            },
            TargetList = new List<SqlExpression>()
            {
                new SqlStringExpression()
                {
                    Value = "a"
                },
                new SqlStringExpression()
                {
                    Value = "b"
                },
                new SqlStringExpression()
                {
                    Value = "c"
                },
            },
        },
    },
};



in/not in子句,主要体现在SqlInExpression表达式,它包含了

  1. body字段,即in的主体,在这里是SqlPropertyExpression,值为t.NAME
  2. TargetList字段,即in的目标列表,在这里是一个SqlExpression的列表,里面包括3个SqlStringExpression,即字符串表达式,a,b,c
  3. 取反部分,即IsNot字段,如果是not in,则IsNot=true

当然了,in也有另一种子查询的类型,即

var sql = "select * from TEST5  WHERE NAME IN (SELECT NAME  FROM TEST3)";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "TEST5",
            },
        },
        Where = new SqlInExpression()
        {
            Body = new SqlIdentifierExpression()
            {
                Value = "NAME",
            },
            SubQuery = new SqlSelectExpression()
            {
                Query = new SqlSelectQueryExpression()
                {
                    Columns = new List<SqlSelectItemExpression>()
                    {
                        new SqlSelectItemExpression()
                        {
                            Body = new SqlIdentifierExpression()
                            {
                                Value = "NAME",
                            },
                        },
                    },
                    From = new SqlTableExpression()
                    {
                        Name = new SqlIdentifierExpression()
                        {
                            Value = "TEST3",
                        },
                    },
                },
            },
        },
    },
};

在这里的SqlInExpression表达式中,它包含了

  1. body字段,即in的主体,在这里是SqlIdentifierExpression,值为NAME
  2. SubQuery字段,即子查询,值为一个SqlSelectExpression
  3. IsNot字段,如果是not in,则IsNot=true

1.2.8 case when子句

var sql = "SELECT CASE WHEN t.name='1' THEN 'a' WHEN t.name='2' THEN 'b' ELSE 'c' END AS v from TEST t";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlCaseExpression()
                {
                    Items = new List<SqlCaseItemExpression>()
                    {
                        new SqlCaseItemExpression()
                        {
                            Condition = new SqlBinaryExpression()
                            {
                                Left = new SqlPropertyExpression()
                                {
                                    Name = new SqlIdentifierExpression()
                                    {
                                        Value = "name",
                                    },
                                    Table = new SqlIdentifierExpression()
                                    {
                                        Value = "t",
                                    },
                                },
                                Operator = SqlBinaryOperator.EqualTo,
                                Right = new SqlStringExpression()
                                {
                                    Value = "1"
                                },
                            },
                            Value = new SqlStringExpression()
                            {
                                Value = "a"
                            },
                        },
                        new SqlCaseItemExpression()
                        {
                            Condition = new SqlBinaryExpression()
                            {
                                Left = new SqlPropertyExpression()
                                {
                                    Name = new SqlIdentifierExpression()
                                    {
                                        Value = "name",
                                    },
                                    Table = new SqlIdentifierExpression()
                                    {
                                        Value = "t",
                                    },
                                },
                                Operator = SqlBinaryOperator.EqualTo,
                                Right = new SqlStringExpression()
                                {
                                    Value = "2"
                                },
                            },
                            Value = new SqlStringExpression()
                            {
                                Value = "b"
                            },
                        },
                    },
                    Else = new SqlStringExpression()
                    {
                        Value = "c"
                    },
                },
                Alias = new SqlIdentifierExpression()
                {
                    Value = "v",
                },
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "TEST",
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "t",
            },
        },
    },
};



case when子句,主要体现在SqlCaseExpression表达式里,他包含了

  1. 各种case when键值对的列表,即Items字段,列表里的每一个元素都是SqlCaseItemExpression表达式,SqlCaseItemExpression表达式,又包含了
    1. 条件,即Condition字段,在本例子中是二元表达式,即SqlBinaryExpression表达式,值为t.name =’1′
    2. 值,即value字段,在本例子中值为字符串a
  2. Else字段,即默认值,本例子中为字符串c

case when还有另外一种句式,如下:

var sql = "select case t.name when 'a' then 1 else 2 end  from test t ";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlCaseExpression()
                {
                    Items = new List<SqlCaseItemExpression>()
                    {
                        new SqlCaseItemExpression()
                        {
                            Condition = new SqlStringExpression()
                            {
                                Value = "a"
                            },
                            Value = new SqlNumberExpression()
                            {
                                Value = 1M,
                            },
                        },
                    },
                    Else = new SqlNumberExpression()
                    {
                        Value = 2M,
                    },
                    Value = new SqlPropertyExpression()
                    {
                        Name = new SqlIdentifierExpression()
                        {
                            Value = "name",
                        },
                        Table = new SqlIdentifierExpression()
                        {
                            Value = "t",
                        },
                    },
                },
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "test",
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "t",
            },
        },
    },
};



在这种SqlCaseExpression表达式里,他包含了

  1. case条件的主体变量,即Value字段,本例子中值为SqlPropertyExpression,它的值为t.name
  2. 各种when then键值对的列表,即Items字段,列表里的每一个元素都是SqlCaseItemExpression表达式,SqlCaseItemExpression表达式,又包含了
    1. 条件,即Condition字段,在本例子中是字符串表达式SqlStringExpression,它的值为a
    2. 值,即Value字段,在本例子中值为SqlNumberExpression,它的值为1
  3. Else字段,即默认值,本例子中为数字2

1.2.9 not子句

var sql = "select * from TEST t WHERE not t.NAME ='abc'";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "TEST",
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "t",
            },
        },
        Where = new SqlNotExpression()
        {
            Body = new SqlBinaryExpression()
            {
                Left = new SqlPropertyExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "NAME",
                    },
                    Table = new SqlIdentifierExpression()
                    {
                        Value = "t",
                    },
                },
                Operator = SqlBinaryOperator.EqualTo,
                Right = new SqlStringExpression()
                {
                    Value = "abc"
                },
            },
        },
    },
};



not子句,主要体现在SqlNotExpression表达式里,它只有一个body字段,即代表否定的部分

1.2.10 变量子句

var sql = "select * from TEST t WHERE not t.NAME =:name";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "TEST",
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "t",
            },
        },
        Where = new SqlNotExpression()
        {
            Body = new SqlBinaryExpression()
            {
                Left = new SqlPropertyExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "NAME",
                    },
                    Table = new SqlIdentifierExpression()
                    {
                        Value = "t",
                    },
                },
                Operator = SqlBinaryOperator.EqualTo,
                Right = new SqlVariableExpression()
                {
                    Name = "name",
                    Prefix = ":",
                },
            },
        },
    },
};


变量子句,主要体现在SqlVariableExpression表达式里,它包括以下部分:

  1. 变量名,即字段Name,这里值为name
  2. 变量前缀,这里值为:

1.3 From数据源

在sql中,From关键字后面有多种形式来指定数据源。主要有以下几种

1.3.1 表名或者视图

select * from test

这个解析结果上面已经演示了。

1.3.2 子查询(子表)

var sql = "select * from (select * from test) t";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlSelectExpression()
        {
            Alias = new SqlIdentifierExpression()
            {
                Value = "t",
            },
            Query = new SqlSelectQueryExpression()
            {
                Columns = new List<SqlSelectItemExpression>()
                {
                    new SqlSelectItemExpression()
                    {
                        Body = new SqlAllColumnExpression()
                    },
                },
                From = new SqlTableExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "test",
                    },
                },
            },
        },
    },
};

在这个例子中,数据源From的值为一个SqlSelectExpression,即SqlSelectExpression中可以嵌套SqlSelectExpression,同时我们注意到内部的SqlSelectExpression有一个表别名的字段Alias,标识符的值为t,表示表别名为t;

1.3.3 连表查询(JOIN)

var sql = "select t1.id from test t1 left join test2 t2 on t1.id=t2.id right join test3 t3 on t2.id=t3.id";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlPropertyExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "id",
                    },
                    Table = new SqlIdentifierExpression()
                    {
                        Value = "t1",
                    },
                },
            },
        },
        From = new SqlJoinTableExpression()
        {
            Left = new SqlJoinTableExpression()
            {
                Left = new SqlTableExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "test",
                    },
                    Alias = new SqlIdentifierExpression()
                    {
                        Value = "t1",
                    },
                },
                JoinType = SqlJoinType.LeftJoin,
                Right = new SqlTableExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "test2",
                    },
                    Alias = new SqlIdentifierExpression()
                    {
                        Value = "t2",
                    },
                },
                Conditions = new SqlBinaryExpression()
                {
                    Left = new SqlPropertyExpression()
                    {
                        Name = new SqlIdentifierExpression()
                        {
                            Value = "id",
                        },
                        Table = new SqlIdentifierExpression()
                        {
                            Value = "t1",
                        },
                    },
                    Operator = SqlBinaryOperator.EqualTo,
                    Right = new SqlPropertyExpression()
                    {
                        Name = new SqlIdentifierExpression()
                        {
                            Value = "id",
                        },
                        Table = new SqlIdentifierExpression()
                        {
                            Value = "t2",
                        },
                    },
                },
            },
            JoinType = SqlJoinType.RightJoin,
            Right = new SqlTableExpression()
            {
                Name = new SqlIdentifierExpression()
                {
                    Value = "test3",
                },
                Alias = new SqlIdentifierExpression()
                {
                    Value = "t3",
                },
            },
            Conditions = new SqlBinaryExpression()
            {
                Left = new SqlPropertyExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "id",
                    },
                    Table = new SqlIdentifierExpression()
                    {
                        Value = "t2",
                    },
                },
                Operator = SqlBinaryOperator.EqualTo,
                Right = new SqlPropertyExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "id",
                    },
                    Table = new SqlIdentifierExpression()
                    {
                        Value = "t3",
                    },
                },
            },
        },
    },
};

在上面这个例子中,我们演示了连表查询是如何解析的,From字段的值为一个SqlJoinTableExpression,即连表查询表达式,他包含了

  1. 左边部分,即Left字段
  2. 右边部分,即Right字段
  3. 连接方式,即JoinType字段,值包括InnerJoin,LeftJoin,RightJoin,FullJoin,CrossJoin,CommaJoin
  4. 表关联条件,即Conditions字段。在这里,Conditions字段的值为一个二元表达式SqlBinaryExpression

在这个例子中,总共3张表联查,SqlJoinTableExpression中得left字段又是一个SqlJoinTableExpression,即SqlJoinTableExpression中可以嵌套SqlJoinTableExpression,无限套娃。

1.3.4 公用表表达式(CTE)

var sql = "with c1 as (select name from test t) , c2(name) AS (SELECT name FROM TEST2 t3 ) select *from c1 JOIN c2 ON c1.name=c2.name";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        WithSubQuerys = new List<SqlWithSubQueryExpression>()
        {
            new SqlWithSubQueryExpression()
            {
                Alias = new SqlIdentifierExpression()
                {
                    Value = "c1",
                },
                FromSelect = new SqlSelectExpression()
                {
                    Query = new SqlSelectQueryExpression()
                    {
                        Columns = new List<SqlSelectItemExpression>()
                        {
                            new SqlSelectItemExpression()
                            {
                                Body = new SqlIdentifierExpression()
                                {
                                    Value = "name",
                                },
                            },
                        },
                        From = new SqlTableExpression()
                        {
                            Name = new SqlIdentifierExpression()
                            {
                                Value = "test",
                            },
                            Alias = new SqlIdentifierExpression()
                            {
                                Value = "t",
                            },
                        },
                    },
                },
            },
            new SqlWithSubQueryExpression()
            {
                Alias = new SqlIdentifierExpression()
                {
                    Value = "c2",
                },
                FromSelect = new SqlSelectExpression()
                {
                    Query = new SqlSelectQueryExpression()
                    {
                        Columns = new List<SqlSelectItemExpression>()
                        {
                            new SqlSelectItemExpression()
                            {
                                Body = new SqlIdentifierExpression()
                                {
                                    Value = "name",
                                },
                            },
                        },
                        From = new SqlTableExpression()
                        {
                            Name = new SqlIdentifierExpression()
                            {
                                Value = "TEST2",
                            },
                            Alias = new SqlIdentifierExpression()
                            {
                                Value = "t3",
                            },
                        },
                    },
                },
                Columns = new List<SqlIdentifierExpression>()
                {
                    new SqlIdentifierExpression()
                    {
                        Value = "name",
                    },
                },
            },
        },
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlJoinTableExpression()
        {
            Left = new SqlTableExpression()
            {
                Name = new SqlIdentifierExpression()
                {
                    Value = "c1",
                },
            },
            JoinType = SqlJoinType.InnerJoin,
            Right = new SqlTableExpression()
            {
                Name = new SqlIdentifierExpression()
                {
                    Value = "c2",
                },
            },
            Conditions = new SqlBinaryExpression()
            {
                Left = new SqlPropertyExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "name",
                    },
                    Table = new SqlIdentifierExpression()
                    {
                        Value = "c1",
                    },
                },
                Operator = SqlBinaryOperator.EqualTo,
                Right = new SqlPropertyExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "name",
                    },
                    Table = new SqlIdentifierExpression()
                    {
                        Value = "c2",
                    },
                },
            },
        },
    },
};

公用表表达式(CTE),主要体现在SqlSelectQueryExpression的WithSubQuerys字段,他是一个SqlWithSubQueryExpression表达式列表,即公用表列表,它里面的每一个元素都是SqlWithSubQueryExpression表达式,此表达式,包含了

  1. 公共表的来源部分,即FromSelect字段,在本例子中,他的值是一个SqlSelectExpression表达式,即一个查询
  2. 公共表的表别名,即Alias字段,在本例子中,他的值是c1
  3. 公共表的列部分,即Columns字段,在本例子中只有一个列名,即name

1.3.5 函数返回的结果集

特定数据库支持从返回结果集的函数中查询,比如oracle中添加一个自定义函数splitstr,他的作用是将一个字符串根据;号进行分割,返回多行数据

var sql = "SELECT * FROM TABLE(splitstr('a;b',';'))";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlReferenceTableExpression()
        {
            FunctionCall = new SqlFunctionCallExpression()
            {
                Name = new SqlIdentifierExpression()
                {
                    Value = "TABLE",
                },
                Arguments = new List<SqlExpression>()
                {
                    new SqlFunctionCallExpression()
                    {
                        Name = new SqlIdentifierExpression()
                        {
                            Value = "splitstr",
                        },
                        Arguments = new List<SqlExpression>()
                        {
                            new SqlStringExpression()
                            {
                                Value = "a;b"
                            },
                            new SqlStringExpression()
                            {
                                Value = ";"
                            },
                        },
                    },
                },
            },
        }
    },
};

函数返回的结果集主要体现在SqlReferenceTableExpression表达式,他的内部包含了一个FunctionCall字段,值为SqlFunctionCallExpression表达式,代表从函数调用的结果集中进行查询。

1.4 OrderBy排序语句

var sql = "select fa.FlowId  from FlowActivity fa order by fa.FlowId desc,fa.Id asc";
var sqlAst = DbUtils.Parse(sql, DbType.SqlServer);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlPropertyExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "FlowId"
                    },
                    Table = new SqlIdentifierExpression()
                    {
                        Value = "fa"
                    },
                },
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "FlowActivity"
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "fa"
            },
        },
        OrderBy = new SqlOrderByExpression()
        {
            Items = new List<SqlOrderByItemExpression>()
            {
                new SqlOrderByItemExpression()
                {
                    Body =
                        new SqlPropertyExpression()
                        {
                            Name = new SqlIdentifierExpression()
                            {
                                Value = "FlowId"
                            },
                            Table = new SqlIdentifierExpression()
                            {
                                Value = "fa"
                            },
                        },
                    OrderByType = SqlOrderByType.Desc
                },
                new SqlOrderByItemExpression()
                {
                    Body =
                        new SqlPropertyExpression()
                        {
                            Name = new SqlIdentifierExpression()
                            {
                                Value = "Id"
                            },
                            Table = new SqlIdentifierExpression()
                            {
                                Value = "fa"
                            },
                        },
                    OrderByType = SqlOrderByType.Asc
                },
            },
        },
    },
};


OrderBy排序子句,值为SqlOrderByExpression表达式,表达式的内容也非常简单,只有一个Items,即一个排序子项表达式的列表,列表里的值为SqlOrderByItemExpression,即排序子项表达式,排序子项表达式里又包含了以下内容

  1. 排序依据,即Body字段,在这个例子中,排序依据是2个SqlPropertyExpression表达式,即根据fa.FlowId,fa.Id排序
  2. 排序类型,即OrderByType字段,值为Asc或者Desc,默认为asc,在这2个例子中,有asc和Desc
  3. 决定null排在前面或后面的NullsType字段,在oracle,pgsql,sqlite中我们可以指定null在排序中的位置,如以下sql
select * from TEST5 t order by t.NAME  desc nulls FIRST,t.AGE ASC NULLS  last 

那么我们的NullsType字段,他的值有SqlOrderByNullsType.First和SqlOrderByNullsType.Last,与之对应。

1.5 GroupBy分组语句

var sql = "select fa.FlowId  from FlowActivity fa group by fa.FlowId,fa.Id HAVING count(fa.Id)>1";
var sqlAst = DbUtils.Parse(sql, DbType.SqlServer);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlPropertyExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "FlowId"
                    },
                    Table = new SqlIdentifierExpression()
                    {
                        Value = "fa"
                    },
                },
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "FlowActivity"
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "fa"
            },
        },
        GroupBy = new SqlGroupByExpression()
        {
            Items = new List<SqlExpression>()
            {
                new SqlPropertyExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "FlowId"
                    },
                    Table = new SqlIdentifierExpression()
                    {
                        Value = "fa"
                    },
                },
                new SqlPropertyExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "Id"
                    },
                    Table = new SqlIdentifierExpression()
                    {
                        Value = "fa"
                    },
                },
            },
            Having = new SqlBinaryExpression()
            {
                Left = new SqlFunctionCallExpression()
                {
                    Name = new SqlIdentifierExpression()
                    {
                        Value = "count"
                    },
                    Arguments = new List<SqlExpression>()
                    {
                        new SqlPropertyExpression()
                        {
                            Name = new SqlIdentifierExpression()
                            {
                                Value = "Id"
                            },
                            Table = new SqlIdentifierExpression()
                            {
                                Value = "fa"
                            },
                        },
                    },
                },
                Operator = SqlBinaryOperator.GreaterThen,
                Right = new SqlNumberExpression()
                {
                    Value = 1M
                },
            },
        },
    },
};


GroupBy分组语句,值为SqlGroupByExpression表达式,他的内容如下

  1. 分组子项表达式的列表,即Items字段,列表里的值为SqlExpression,他的值是一个逻辑子句
  2. 分组过滤子句,即Having字段,他的值是一个逻辑子句,在本例子中,逻辑子句的值为一个SqlBinaryExpression

1.5 Limit分页子句

1.5.1 mysql,sqlite

var sql = "select * from test t limit 1,5";
var sqlAst = DbUtils.Parse(sql, DbType.MySql);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "test",
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "t",
            },
        },
        Limit = new SqlLimitExpression()
        {
            Offset = new SqlNumberExpression()
            {
                Value = 1M,
            },
            RowCount = new SqlNumberExpression()
            {
                Value = 5M,
            },
        },
    },
};


Limit分页子句,值为SqlLimitExpression表达式,他的内容如下

  1. 每页数量,即RowCount字段,这本例子中,值为5
  2. 跳过数量,即Offset字段,本例子中,值为1

1.5.2 oracle

var sql = "SELECT * FROM TEST3 t  ORDER BY t.NAME  DESC FETCH FIRST 2 rows ONLY";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            }
        },
        From = new SqlTableExpression()
        {
            Alias = new SqlIdentifierExpression()
            {
                Value = "t"
            },
            Name = new SqlIdentifierExpression()
            {
                Value = "TEST3"
            }
        },

        OrderBy = new SqlOrderByExpression()
        {
            Items = new List<SqlOrderByItemExpression>()
            {
                new SqlOrderByItemExpression()
                {
                    OrderByType = SqlOrderByType.Desc,
                    Body = new SqlPropertyExpression()
                    {
                        Name = new SqlIdentifierExpression() { Value = "NAME" },
                        Table = new SqlIdentifierExpression()
                        {
                            Value = "t"
                        }
                    }
                }
            }
        },
        Limit = new SqlLimitExpression()
        {
            RowCount = new SqlNumberExpression()
            {
                Value = 2
            }
        }
    }
};

1.5.3 pgsql

var sql = "select * from test5   t order by t.name limit 1 offset 10;";
var sqlAst = DbUtils.Parse(sql, DbType.Pgsql);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "test5",
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "t",
            },
        },
        OrderBy = new SqlOrderByExpression()
        {
            Items = new List<SqlOrderByItemExpression>()
            {
                new SqlOrderByItemExpression()
                {
                    Body = new SqlPropertyExpression()
                    {
                        Name = new SqlIdentifierExpression()
                        {
                            Value = "name",
                        },
                        Table = new SqlIdentifierExpression()
                        {
                            Value = "t",
                        },
                    },
                },
            },
        },
        Limit = new SqlLimitExpression()
        {
            Offset = new SqlNumberExpression()
            {
                Value = 10M,
            },
            RowCount = new SqlNumberExpression()
            {
                Value = 1M,
            },
        },
    },
};


1.5.4 SQLServer

var sql = "select * from test t order by t.name OFFSET 5 ROWS FETCH NEXT 10 ROWS ONLY";
var sqlAst = DbUtils.Parse(sql, DbType.SqlServer);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            }
        },
        From = new SqlTableExpression()
        {
            Alias = new SqlIdentifierExpression()
            {
                Value = "t"
            },
            Name = new SqlIdentifierExpression()
            {
                Value = "test"
            }
        },

        OrderBy = new SqlOrderByExpression()
        {
            Items = new List<SqlOrderByItemExpression>()
            {
                new SqlOrderByItemExpression()
                {
                    Body = new SqlPropertyExpression()
                    {
                        Name = new SqlIdentifierExpression() { Value = "name" },
                        Table = new SqlIdentifierExpression()
                        {
                            Value = "t"
                        }
                    }
                }
            }
        },
        Limit = new SqlLimitExpression()
        {
            Offset = new SqlNumberExpression()
            {
                Value = 5
            },
            RowCount = new SqlNumberExpression()
            {
                Value = 10
            }
        }
    }
};


1.6 ConnectBy层次查询语句(oracle专用)

var sql = "SELECT EMPLOYEEID , MANAGERID , LEVEL FROM EMPLOYEE e START WITH MANAGERID IS NULL CONNECT BY NOCYCLE PRIOR EMPLOYEEID = MANAGERID ORDER SIBLINGS BY EMPLOYEEID ";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlIdentifierExpression()
                {
                    Value = "EMPLOYEEID",
                },
            },
            new SqlSelectItemExpression()
            {
                Body = new SqlIdentifierExpression()
                {
                    Value = "MANAGERID",
                },
            },
            new SqlSelectItemExpression()
            {
                Body = new SqlIdentifierExpression()
                {
                    Value = "LEVEL",
                },
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "EMPLOYEE",
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "e",
            },
        },
        ConnectBy = new SqlConnectByExpression()
        {
            StartWith = new SqlBinaryExpression()
            {
                Left = new SqlIdentifierExpression()
                {
                    Value = "MANAGERID",
                },
                Operator = SqlBinaryOperator.Is,
                Right = new SqlNullExpression()
            },
            Body = new SqlBinaryExpression()
            {
                Left = new SqlIdentifierExpression()
                {
                    Value = "EMPLOYEEID",
                },
                Operator = SqlBinaryOperator.EqualTo,
                Right = new SqlIdentifierExpression()
                {
                    Value = "MANAGERID",
                },
            },
            IsNocycle = true,
            IsPrior = true,
            OrderBy = new SqlOrderByExpression()
            {
                Items = new List<SqlOrderByItemExpression>()
                {
                    new SqlOrderByItemExpression()
                    {
                        Body = new SqlIdentifierExpression()
                        {
                            Value = "EMPLOYEEID",
                        },
                    },
                },
                IsSiblings = true,
            },
        },
    },
};

ConnectBy层次查询子句,值为SqlConnectByExpression表达式,他的内容如下

  1. 指定层次查询的根节点条件,即StartWith字段,本例子中他的值为SqlBinaryExpression二元表达式
  2. 主体关联条件子句,即Body字段,本例子中他的值是一个SqlBinaryExpression二元表达式
  3. IsPrior字段用来指示层次结构中哪个列是父节点,如果sql中存在Prior关键字,则值为true
  4. IsNocycle字段,用来防止循环引用导致无限递归,如果sql中存在Nocycle则为true
  5. order by子句,用于排序

1.7 Into子句(sql server专用)

var sql = "SELECT name into test14 from TEST as t ";
var sqlAst = DbUtils.Parse(sql, DbType.SqlServer);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlIdentifierExpression()
                {
                    Value = "name"
                },
            },
        },
        Into = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "test14"
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "TEST"
            },
            Alias = new SqlIdentifierExpression()
            {
                Value = "t"
            },
        },
    },
};

into子句,在本例子中值为SqlTableExpression,即into到某张表里。

2. Insert插入语句

2.1 插入单个值

var sql = "insert into test11(name,id) values('a1','a2')";
var sqlAst = DbUtils.Parse(sql, DbType.SqlServer);

解析结果如下:

var expect = new SqlInsertExpression()
{
    Columns = new List<SqlExpression>()
    {
        new SqlIdentifierExpression()
        {
            Value = "name"
        },
        new SqlIdentifierExpression()
        {
            Value = "id"
        },
    },
    ValuesList = new List<List<SqlExpression>>()
    {
        new List<SqlExpression>()
        {
            new SqlStringExpression()
            {
                Value = "a1"
            },
            new SqlStringExpression()
            {
                Value = "a2"
            },
        },
    },
    Table = new SqlTableExpression()
    {
        Name = new SqlIdentifierExpression()
        {
            Value = "test11"
        },
    },
};

如上例子,插入语句表现为一个SqlInsertExpression,他包含了

  1. 要插入的字段列表,即Columns字段,值为一个SqlExpression的列表,本例子中值为2个SqlIdentifierExpression,它们的值为name和id,即插入name和id字段
  2. 值列表,即ValuesList字段,值为一个List<SqlExpression>的列表,即列表里的元素是一个列表,每个元素代表一组待插入的数据,本例子中列表里只有一个List,并且子列表里的值为a1和a2,即待插入的值为a1和a2。
  3. 待插入数据的表,即Table字段,本例子中值为test11

为什么ValuesList字段是列表里嵌套列表呢?主要是因为可以插入多个值列表,让我们继续往下看

2.2 插入多个值

var sql = "insert into test11(name,id) values('a1','a2'),('a3','a4')";
var sqlAst = DbUtils.Parse(sql, DbType.MySql);

解析结果如下:

var expect = new SqlInsertExpression()
{
    Columns = new List<SqlExpression>()
    {
        new SqlIdentifierExpression()
        {
            Value = "name"
        },
        new SqlIdentifierExpression()
        {
            Value = "id"
        },
    },
    ValuesList = new List<List<SqlExpression>>()
    {
        new List<SqlExpression>()
        {
            new SqlStringExpression()
            {
                Value = "a1"
            },
            new SqlStringExpression()
            {
                Value = "a2"
            },
        },
        new List<SqlExpression>()
        {
            new SqlStringExpression()
            {
                Value = "a3"
            },
            new SqlStringExpression()
            {
                Value = "a4"
            },
        },
    },
    Table = new SqlTableExpression()
    {
        Name = new SqlIdentifierExpression()
        {
            Value = "test11"
        },
    },
};

在本例子中,ValuesList字段中有2个子元素,即2个List列表,代表插入2组数据,值分别为(‘a1′,’a2’)和(‘a3′,’a4’)

2.3 待插入的值为一个子查询

var sql = "INSERT INTO TEST2(name) SELECT name AS name2 FROM TEST t";
var sqlAst = DbUtils.Parse(sql, DbType.MySql);

解析结果如下:

var expect = new SqlInsertExpression()
{
    Columns = new List<SqlExpression>()
    {
        new SqlIdentifierExpression()
        {
            Value = "name"
        },
    },
    Table = new SqlTableExpression()
    {
        Name = new SqlIdentifierExpression()
        {
            Value = "TEST2"
        },
    },
    FromSelect = new SqlSelectExpression()
    {
        Query = new SqlSelectQueryExpression()
        {
            Columns = new List<SqlSelectItemExpression>()
            {
                new SqlSelectItemExpression()
                {
                    Body = new SqlIdentifierExpression()
                    {
                        Value = "name"
                    },
                    Alias = new SqlIdentifierExpression()
                    {
                        Value = "name2"
                    },
                },
            },
            From = new SqlTableExpression()
            {
                Name = new SqlIdentifierExpression()
                {
                    Value = "TEST"
                },
                Alias = new SqlIdentifierExpression()
                {
                    Value = "t"
                },
            },
        },
    },
};

如上例子,插入语句表现为一个SqlInsertExpression,他包含了

  1. 要插入的字段列表,即Columns字段,值为一个SqlExpression的列表,本例子中值为name
  2. 子查询来源,即FromSelect字段,值为一个SqlSelectExpression,即一个子查询
  3. 待插入数据的表,即Table字段,本例子中值为TEST2

3. Update更新语句

var sql = "update test set name ='4',d='2024-11-22 08:19:47.243' where name ='1'";
var sqlAst = DbUtils.Parse(sql, DbType.MySql);

解析结果如下:

var expect = new SqlUpdateExpression()
{
    Table = new SqlTableExpression()
    {
        Name = new SqlIdentifierExpression()
        {
            Value = "test"
        },
    },
    Where = new SqlBinaryExpression()
    {
        Left = new SqlIdentifierExpression()
        {
            Value = "name"
        },
        Operator = SqlBinaryOperator.EqualTo,
        Right = new SqlStringExpression()
        {
            Value = "1"
        },
    },
    Items = new List<SqlExpression>()
    {
        new SqlBinaryExpression()
        {
            Left = new SqlIdentifierExpression()
            {
                Value = "name"
            },
            Operator = SqlBinaryOperator.EqualTo,
            Right = new SqlStringExpression()
            {
                Value = "4"
            },
        },
        new SqlBinaryExpression()
        {
            Left = new SqlIdentifierExpression()
            {
                Value = "d"
            },
            Operator = SqlBinaryOperator.EqualTo,
            Right = new SqlStringExpression()
            {
                Value = "2024-11-22 08:19:47.243"
            },
        },
    },
};

如上例子,更新语句表现为一个SqlUpdateExpression,他包含了

  1. 要更新的(字段-值)的列表,即Items字段,值为一个SqlExpression的列表,本例子中值为2个SqlBinaryExpression,即name=’4’和d=’2024-11-22 08:19:47.243′
  2. 条件过滤子句,即Where字段,代表过滤条件,本例子中值为一个SqlBinaryExpression,即name =’1′
  3. 待更新数据的表,即Table字段,本例子中值为test

4. Delete删除语句

var sql = "delete from test where name=4";
var sqlAst = DbUtils.Parse(sql, DbType.MySql);

解析结果如下:

var expect = new SqlDeleteExpression()
{
    Table = new SqlTableExpression()
    {
        Name = new SqlIdentifierExpression()
        {
            Value = "test"
        },
    },
    Where = new SqlBinaryExpression()
    {
        Left = new SqlIdentifierExpression()
        {
            Value = "name"
        },
        Operator = SqlBinaryOperator.EqualTo,
        Right = new SqlNumberExpression()
        {
            Value = 4M
        },
    },
};

如上例子,删除语句表现为一个SqlDeleteExpression,他包含了

  1. 条件过滤子句,即Where字段,代表过滤条件,本例子中值为一个SqlBinaryExpression,即name=4
  2. 待删除数据的表,即Table字段,本例子中值为test

5. 注释处理

5.1 单行注释

var sql = @"select *--abc from test lbu WHERE a ='1'--aaaaaa
FROM test";
var sqlAst = DbUtils.Parse(sql, DbType.SqlServer);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "test",
            },
        },
    },
};

如上例子,单行注释被正确忽视,解析正确。

5.2 多行注释

var sql = @"/*这
            是
            顶部*/
            select *--abc
            FROM test/*这
            是
            底部*/";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析结果如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "test",
            },
        },
    },
};

如上例子,多行注释被正确忽视,解析正确。

6. 如何解析ast抽象语法树

当我们通过

var sql = @"select * from test";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析sql获取到抽象语法树以后,我们就要对这颗抽象语法树进行解析,获取我们想要的数据,此时就要用上访问者模式(visitor) .

6.1 访问者模式

访问者模式最大的特点就是结构与算法分离,结合本项目理解,就是ast抽象语法树这个结构已经解析出来了,你可以根据自己的需要写算法去任意解析这颗语法树。这是一个1-N的操作,即一个抽象语法树,可以对应N个解析算法,当我们要自定义算法去解析抽象语法树时,我们需要自定义一个Visitor类,并且实现IAstVisitor接口

public class CustomVisitor : IAcceptVisitor
{
    
}

但是实现这个接口要实现接口里的很多个方法,并且有些数据并不是我们关心的,所以我提供了一个实现了IAcceptVisitor接口的抽象类BaseAstVisitor用来简化操作,我们只需要继承这个抽象类,然后重写我们感兴趣的方法即可

public class CustomVisitor : BaseAstVisitor
{
    
}

在本项目中,我提供了2个基本的vistor供大家使用,UnitTestAstVisitor和SqlGenerationAstVisitor,大家可以参考这2个visitor去写自己的算法来解析抽象语法树。接下来,我将介绍这2个visitor的用法。

6.2 UnitTestAstVisitor

当我们通过

var sql = @"select * from test";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);

解析sql获取到抽象语法树之后,sqlAst其实还是一个数据结构,我们可以通过vs监视这个变量来查看内部的结构,但是如果是非常复杂的sql,那这颗树会巨大无比,要靠我们手动去慢慢点开查看得累死,没错!写单元测试的时候,我刚开始都是用手写的结果去对比引擎解析出来的结果,后来我就被累死了,该说不说,这活真不是人干的,所以痛定思痛之后我就写了这个UnitTestAstVisitor来替我生成ast的结构字符串,接下来让我们看看用法

var sql = @"select * from test";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);
var unitTestAstVisitor = new UnitTestAstVisitor();
sqlAst.Accept(unitTestAstVisitor);
var result = unitTestAstVisitor.GetResult();

其中的result就是解析抽象语法树生成的字符串,如下:

var expect = new SqlSelectExpression()
{
    Query = new SqlSelectQueryExpression()
    {
        Columns = new List<SqlSelectItemExpression>()
        {
            new SqlSelectItemExpression()
            {
                Body = new SqlAllColumnExpression()
            },
        },
        From = new SqlTableExpression()
        {
            Name = new SqlIdentifierExpression()
            {
                Value = "test",
            },
        },
    },
};

然后把这个生成的字符串黏贴到vs里去和引擎生成的结果进行对比,

Assert.True(sqlAst.Equals(expect));

至此,我写单元测试的工作量大大减轻,同时对于生成的sqlAst语法树的结构也更加一目了然了。

6.2 SqlGenerationAstVisitor

我们通过解析sql生成了抽象语法树之后,如果我们想要给这颗抽象语法树添加一个where条件,比如添加test.name =’a’

var sql = @"select * from test";
var sqlAst = DbUtils.Parse(sql, DbType.Oracle);
if (sqlAst is SqlSelectExpression sqlSelectExpression && sqlSelectExpression.Query is SqlSelectQueryExpression sqlSelectQueryExpression)
{
    sqlSelectQueryExpression.Where = new SqlBinaryExpression()
    {
        Left = new SqlPropertyExpression()
        {
            Table = new SqlIdentifierExpression()
            {
                Value = "test"
            },
            Name = new SqlIdentifierExpression()
            {
                Value = "name"
            }
        },
        Operator = SqlBinaryOperator.EqualTo,
        Right = new SqlStringExpression()
        {
            Value = "a"
        }
    };
}

好了,现在我们添加完了,接下来我们肯定是想着把抽象语法树转化为sql语句,此时,就需要用上SqlGenerationAstVisitor了,他就是负责把抽象语法树转化为sql的

var sqlGenerationAstVisitor = new SqlGenerationAstVisitor(DbType.Oracle);
sqlAst.Accept(sqlGenerationAstVisitor);
var newSql = sqlGenerationAstVisitor.GetResult();

我们获取到的newSql就是新的sql了,他的值为

select * from test where(test.name = 'a')

至此,我们的目的就达到了。

7. sql解析的理论基础

sql之所以能被我们解析出来,主要是因为sql是一种形式语言,自然语言和形式语言的一个重要区别是,自然语言的一个语句,可能有多重含义,而形式语言的一个语句,只能有一个语义;形式语言的语法是人为规定的,有了一定的语法规则,语法解析器就能根据语法规则,解析出一个语句的唯一含义。

项目感悟

  1. 解决嵌套问题的唯一方案,就是用递归
  2. 对于基础项目,单元测试非常非常重要,因为开发的过程中可能会不断地重构,那以前跑过的测试案例就有可能失败,如果此时需要靠人手工去回归测试验证的话,那工作量是天量的,做不完,根本做不完,所以正确的解决方案是写单元测试,新添加一个功能后,为这个功能写1-N个单元测试,确保新功能对各种情况都有覆盖到,然后再跑一遍所有单元测试,确保没有影响到旧的功能。当然了,跑单元测试最让我崩溃的是,跑一遍所有单元测试,红了(即失败)几十个,天都塌了。

开源地址,欢迎star

本项目基于MIT协议开源,地址为
https://github.com/TripleView/SqlParser.Net

同时感谢以下项目

  1. 阿里巴巴开源的druid

写在最后

如果各位靓仔觉得这个项目不错,欢迎一键三连(推荐,star,关注),同时欢迎加入三合的开源交流群,QQ群号:799648362

Smarty实现页面静态化(生成HTML)的方法 / 张生荣

mikel阅读(147)

来源: Smarty实现页面静态化(生成HTML)的方法 / 张生荣

本文实例讲述了Smarty实现页面静态化(生成HTML)的方法。分享给大家供大家参考,具体如下:

为了减少数据库读取次数,某些内容不经常被更改的页面,比如文章详细页面需要做成HTML静态页面。

在使用Smarty的情况下,也可以实现页面静态化。下面先简单说一下使用Smarty时通常动态读取的做法。

一般分这几步:

1、通过URL传递一个参数(ID);

2、然后根据此ID查询数据库;

3、取得数据后根据需要修改显示内容;

4、assign需要显示的数据;

5、display模板文件。

Smarty静态化过程只需要在上述过程中添加两个步骤:

第一:在1之前使用 ob_start() 打开缓冲区。

第二:在5之后使用 ob_get_contents() 获取内存未输出内容,然后使用fwrite()将内容写入目标html文件。

根据上述描述,此过程是在网站前台实现的,而内容管理(添加、修改、删除)通常是在后台进行,为了能有效

利用上述过程,可以使用一点小手段,那就是Header()。具体过程是这样的:在添加、修改程序完成之后,使用

Header() (当然还有其它方式)跳到前台读取,这样可以实现页面HTML化,然后在生成html后再跳回后台管理侧,而这两个跳转

过程是不可见的。

  1. <?php
  2. $cachefile=“./cache/demo.html”;//把缓存文件放到一个cache文件夹里
  3. $cachetime=20;
  4. if (!file_exists($cachefile ) || filemtime($cachefile)+$cachetime < time()) //判断是否存在和过期时间
  5. {
  6. ob_start();//输出控制
  7. echo ‘<table border=”1″ width=”800″ align=”center”>’;
  8. echo ‘<caption><h1>user</h1></caption>’;
  9. echo ‘<tr>’;
  10. echo “<td>11111</td>”;
  11. echo “<td>22222</td>”;
  12. echo ‘</tr>’;
  13. echo ‘<tr>’;
  14. echo “<td>11111</td>”;
  15. echo “<td>22222</td>”;
  16. echo ‘</tr>’;
  17. echo ‘</table>’;
  18. $html=ob_get_contents();
  19. file_put_contents($cachefile, $html);//输出到缓存文件
  20. ob_end_flush();//输出并关闭缓冲区
  21. }
  22. else{
  23. echo ‘ceshi’;
  24. include $cachefile;
  25. }
  26. ?>

更多关于Smarty相关内容感兴趣的读者可查看本站专题:《smarty模板入门基础教程》、《PHP模板技术总结》、《PHP基于pdo操作数据库技巧总结》、《PHP运算与运算符用法总结》、《PHP网络编程技巧总结》、《PHP基本语法入门教程》、《php面向对象程序设计入门教程》、《php字符串(string)用法总结》、《php+mySQL数据库操作入门教程》及《php常见数据库操作技巧汇总》

希望本文所述对大家基于smarty模板的PHP程序设计有所帮助。

使用PHP Smarty静态化,使用smarty模板或缓存实现页面静态化-CSDN博客

mikel阅读(117)

来源: 使用PHP Smarty静态化,使用smarty模板或缓存实现页面静态化-CSDN博客

第一种:利用模板。目前PHP的模板可以说是很多了,有功能强大的smarty,还有简单易用的smarttemplate等。它们每一种模板,都有一个获取输出内容的函数。我们生成静态页面的方法,就是利用了这个函数。用这个方法的优点是,代码比较清晰,可读性好。

这里我用smarty做例子,说明如何生成静态页

require(‘smarty/Smarty.class.php’);

$t = new Smarty;

$t->assign(“title”,”Hello World!”);

$content = $t->fetch(“templates/index.htm”);

//这里的 fetch() 就是获取输出内容的函数,现在$content变量里面,就是要显示的内容了

$fp = fopen(“archives/2005/05/19/0001.html”, “w”);

fwrite($fp, $content);

fclose($fp);

第二种方法:利用ob系列的函数。这里用到的函数主要是 ob_start(), ob_end_flush(), ob_get_content(),其中ob_start()是打开浏览器缓冲区的意思,打开缓冲后,所有来自PHP程序的非文件头信息均不会发送,而是保存在内部缓冲区,直到你使用了ob_end_flush().而这里最重要的一个函数,就是ob_get_contents(),这个函数的作用是获取缓冲区的内容,相当于上面的那个fetch(),道理一样的。代码:

ob_start();

echo “Hello World!”;

$content = ob_get_contents();//取得php页面输出的全部内容

$fp = fopen(“archives/2005/05/19/0001.html”, “w”);

fwrite($fp, $content);

fclose($fp);

我选用的第2种方法 也就是用ob系列的函数

我刚开始看这个的时候有点不太明白 后来才知道ob是output buffering的意思 也就是输出缓存

当你准备输出的时候 所有的数据都保存在ob里面 服务器解析完php以后 把所有要输出到客户端的html代码都存放在ob里面 如果我们要输出html静态页面 只要把缓存取出来写入一个html页面即可

所以原理其实是很简单的

这里用到了几个函数 由于我初学php 很多函数我还不了解 所以这里也说明一下 希望可以帮助大家

ob_start():开始“捕捉”缓存 也就是从这里开始 打开浏览器的缓存

ob_end_flush():关闭浏览器缓存

ob_get_content():读取缓存内容

fopen(“文件路径”,”打开模式”)打开文件 这个函数的打开模式有好几种 下面介绍几种主要的模式:

‘r’ 只读方式打开,将文件指针指向文件头。??

‘r+’ 读写方式打开,将文件指针指向文件头。??

‘w’ 写入方式打开,将文件指针指向文件头并将文件大小截为零。如果文件不存在则尝试创建之。??

‘w+’ 读写方式打开,将文件指针指向文件头并将文件大小截为零。如果文件不存在则尝试创建之。??

fwrite(“文件名称”,”写入内容”) 写入文件

fclose() 关闭文件

由于我要转换的html文件非常多 可能有几百个 所以这里不能静态指定fopen的路径 大家可以设置一个路径变量 里面可以保存用户传来的id等信息 方便进行html文件命名 下面是我结合上次php读取xml数据的一个简单例子

程序代码

ob_start();????????????//打开浏览器缓存

//下面是读取xml数据

$parser = xml_parser_create(); //创建一个parser编辑器

xml_set_element_handler($parser, “startElement”, “endElement”);//设立标签触发时的相应函数 这里分别为startElement和endElenment

xml_set_character_data_handler($parser, “characterData”);//设立数据读取时的相应函数

$xml_file=”1.xml”;//指定所要读取的xml文件,可以是url

$filehandler = fopen($xml_file, “r”);//打开文件

while ($data = fread($filehandler, 4096))

{

xml_parse($parser, $data, feof($filehandler));

}//每次取出4096个字节进行处理

fclose($filehandler);

xml_parser_free($parser);//关闭和释放parser解析器

$name=false;

$position=false;

function startElement($parser_instance, $element_name, $attrs) //起始标签事件的函数

{

global $name,$position;??

if($element_name==”NAME”)

{

$name=true;

$position=false;

echo “名字:”;

}

if($element_name==”POSITION”)

{$name=false;

$position=true;

echo “职位:”;

}

}

function characterData($parser_instance, $xml_data) //读取数据时的函数

{

??global $name,$position;

??if($position)

echo $xml_data.”

“;

if($name)

echo $xml_data.”

“;

}

function endElement($parser_instance, $element_name) //结束标签事件的函数

{

global $name,$position;

$name=false;

$position=false;

}

?>

//xml数据读取完毕

$htmlname=$id.”.html”;??????????????????????????//$id可以自己定义 这里代表用户传来的id

$htmlpath=”archives/”.$htmlname;?????? //设置路径变量

$content = ob_get_contents();??????????????//取得php页面输出的全部内容

$fp = fopen($htmlpath, “w”);

fwrite($fp, $content);

fclose($fp);

?>

ThinkPHP 的页面静态化功能的实现_thinkphp 静态化-CSDN博客

mikel阅读(123)

来源: ThinkPHP 的页面静态化功能的实现_thinkphp 静态化-CSDN博客

**
常说的页面静态化分为两种,一种是伪静态,即url 重写,一种是真静态化。

上一篇讲到了ThinkPHP自带的buildHtml()方法生成静态页面,在ThinkPHP的官方文档也没具体讲到此方法,而文档却很具体的讲了静态缓存技术,看来TP官方还是建议使用静态缓存。

一. 网站不分手机版和电脑版,只有一套前台模板

(1)在根目录下的程序入口文件index.php中加下面这行:

define(‘HTML_PATH’, ‘./HTML/’);//生成静态页面的文件位置

(2)在项目的配置文件config.php中进行如下配置:
‘HTML_CACHE_ON’ => true, // 开启静态缓存
‘HTML_CACHE_TIME’ => 604800, // 全局静态缓存有效期(秒)(3600247)
‘HTML_FILE_SUFFIX’=> ‘.html’, // 设置静态缓存文件后缀
‘HTML_CACHE_RULES’=> array( // 定义静态缓存规则
// 定义格式1 数组方式
// 定义格式2 字符串方式
//后一个参数是静态缓存有效期,单位为秒。如果不定义,则会获取配置参数HTML_CACHE_TIME 的设置值,如果定义为0则表示永久缓存。
‘Index:index’ =>array(’{:controller}{:action}’),
‘Index:articalList’ =>array(’{:controller}
{:action}/{id}{p}list’),
‘Index:articalInfo’ =>array(’{:controller}{:action}/{id}’),
‘Index:productList’ =>array(’{:controller}
{:action}/{id}{p}list’),
‘Index:productInfo’ =>array(’{:controller}{:action}/{id}’),
//‘Index:staticcachec’ =>array(’{:module}/{:controller}/{:action}/{id}’,30),
//‘Index:staticcachec’ =>array(’{:module}/{:controller}/{:action}
{id}’,30),
)
{:module} 、{:controller} 和{:action} 分别表示当前模块名、控制器名和操作名,{id} 其实等效于 {$_GET.id},{p}则表示分页。在{}之外的字符作为字符串对待,如果包含有”/”,会自动创建目录。
在前台第一次访问某个模块的某个控制器的某个方法时,就会在指定的目录按照指定的文件名格式生成静态HTML文件。如图:

此时访问网站虽然URL没有变,但访问到的就是静态化之后的页面。亲测有效,但是此法也有不足:

如果系统分电脑版和手机版两套模板,那么生成的静态页面只有一套,手机先访问就生成手机的模板,再用电脑访问的界面就会混乱。

但如若网站只有一套模板(比如电脑版)那么到此就够了。如果手机电脑两套模板,解决方法如下

二. 网站分手机版和电脑版两套模板

(1)在根目录下的程序入口文件index.php中进行如下配置:

define(‘HTML_PATH’, ‘./HTML/’);//生成静态页面的文件位置

//如果是手机访问,设置手机缓存目录
if(isMobile()){
KaTeX parse error: Expected ‘EOF’, got ‘}’ at position 37: …] = ‘mobile/’; }̲elseif(isset(_SESSION[‘theme_path’])){
unset($_SESSION[‘theme_path’]);
}

//判断是否是手机访问
function isMobile(){
if(isset(KaTeX parse error: Double subscript at position 16: _SERVER[‘HTTP_X_̲WAP_PROFILE’]))…_SERVER[‘HTTP_VIA’]))return stristr(S E R V E R [ ′ H T T P V I A ′ ] , &quot; w a p &quot; ) ? t r u e : f a l s e ; i f ( i s s e t ( _SERVER[&#x27;HTTP_VIA&#x27;], &quot;wap&quot;) ? true : false; if(isset(SERVER[HTTPVIA],wap)?true:false;if(isset(_SERVER[‘HTTP_USER_AGENT’])){$clientkeywords = array (‘nokia’,‘sony’,‘ericsson’,‘mot’,‘samsung’,‘htc’,‘sgh’,‘lg’,‘sharp’,‘sie-’,‘philips’,‘panasonic’,‘alcatel’,‘lenovo’,‘iphone’,‘ipod’,‘blackberry’,‘meizu’,‘Android’,‘netfront’,‘symbian’,‘ucweb’,‘windowsce’,‘palm’,‘operamini’,‘operamobi’,‘openwave’,‘nexusone’,‘cldc’,‘midp’,‘wap’,‘mobile’);if (preg_match(“/(” . implode(’|’, c l i e n t k e y w o r d s ) . &quot; ) / i &quot; , s t r t o l o w e r ( clientkeywords) . &quot;)/i&quot;, strtolower(clientkeywords).)/i,strtolower(_SERVER[‘HTTP_USER_AGENT’])))return true;}
if(isset(KaTeX parse error: Expected ‘}’, got ‘EOF’ at end of input: …])){if((strpos(_SERVER[‘HTTP_ACCEPT’], ‘vnd.wap.wml’) !== false) && (strpos(S E R V E R [ ′ H T T P A C C E P T ′ ] , ′ t e x t / h t m l ′ ) = = = f a l s e ∣ ∣ ( s t r p o s ( _SERVER[&#x27;HTTP_ACCEPT&#x27;], &#x27;text/html&#x27;) === false || (strpos(SERVER[HTTPACCEPT],text/html)===false(strpos(_SERVER[‘HTTP_ACCEPT’], ‘vnd.wap.wml’) < strpos($_SERVER[‘HTTP_ACCEPT’], ‘text/html’))))return true;}
return false;
}

(2)在项目的配置文件config.php中进行如下配置:
//设置静态缓存
‘HTML_CACHE_ON’ => true, // 开启静态缓存
‘HTML_CACHE_TIME’ => 604800, // 全局静态缓存有效期(秒)(3600247)
‘HTML_FILE_SUFFIX’=> ‘.html’, // 设置静态缓存文件后缀
‘HTML_CACHE_RULES’=> array( // 定义静态缓存规则
// 定义格式2 字符串方式
‘Index:index’ =>array(’{KaTeX parse error: Expected ‘EOF’, got ‘}’ at position 20: …SION.theme_path}̲/{:controller}_…SESSION.theme_path}/{:controller}{:action}/{id}_{p}list’),
‘Index:articalInfo’ =>array(’{KaTeX parse error: Expected ‘EOF’, got ‘}’ at position 20: …SION.theme_path}̲/{:controller}_…SESSION.theme_path}/{:controller}{:action}/{id}
{p}_list’),
//‘Index:productInfo’ =>array(’{$SESSION.theme_path}/{:controller}{:action}/{id}’),
)

那么手机访问的时候,会先以缓存的方式生成一个名字为theme_path内容为mobile/的字符串缓存,然后在HTML/mobile/目录下生成相应的手机模板静态文件;如果再用电脑访问,那么会先清除名字为theme_path的字符串缓存内容,那么再生成静态文件的时候就在HTML/目录下了。

作者:江南极客
来源:CSDN
原文:https://blog.csdn.net/sinat_35861727/article/details/54971805
版权声明:本文为博主原创文章,转载请附上博文链接!**

TinkPHP6-tp6实现全站静态化方法-ThinkPHP-萝卜网络博客

mikel阅读(110)

来源: TinkPHP6-tp6实现全站静态化方法-ThinkPHP-萝卜网络博客

一,前言

模板完全静态化,也就是通过模板完全生成纯静态的网页,相比动态页面和伪静态页面更安全更利于SEO访问更快。

二,实现思路

1,根据模块/控制器_MD5(参数)动态递归创建目录

2,file_exists判断生成的静态页是否存在,是否过期,存在并且未过期则重定向到静态网页

3,不存在或者文件已过期,则file_put_contents($file,$content)函数生成静态页面

三,编码

1,基类中的生成前与生成后的方法

class Common extends BaseController
{
    //静态模板生成目录
    protected $staticHtmlDir = "";
    //静态文件
    protected $staticHtmlFile = "";             

    //判断是否存在静态
    public function beforeBuild($param = []) {
        //生成静态
        $this->staticHtmlDir = "html".DS.$this->request->controller().DS;
        //参数md5
        $param = md5(json_encode($param));
        $this->staticHtmlFile = $this->staticHtmlDir .$this->request->action() . '_'  . $param .'.html';
        
        //目录不存在,则创建
        if(!file_exists($this->staticHtmlDir)){
            mkdir($this->staticHtmlDir);
        }

        //静态文件存在,并且没有过期
        if(file_exists($this->staticHtmlFile) && filectime($this->staticHtmlFile)>=time()-60*60*24*5) {
            header("Location:/" . $this->staticHtmlFile);
            exit();
        }

    }

    //开始生成静态文件
    public function afterBuild($html) {
        if(!empty($this->staticHtmlFile) && !empty($html)) {
            if(file_exists($this->staticHtmlFile)) {
                \unlink($this->staticHtmlFile);
            }
            if(file_put_contents($this->staticHtmlFile,$html)) {
                header("Location:/" . $this->staticHtmlFile);
                exit();
            }
        }
    }

}

2,控制器中的使用

public function getList($id = '')
{
    //判断静态界面是否存在
    $this->beforeBuild(array($id));

    // do someing
    $name = "测试静态化";
    $html = View::fetch('/get_list',['name'=>$name]);

    //生成静态界面
    $this->afterBuild($html);

}

一套以用户体验出发的.NET8 Web开源框架 - 追逐时光者 - 博客园

mikel阅读(145)

来源: 一套以用户体验出发的.NET8 Web开源框架 – 追逐时光者 – 博客园

前言

今天大姚给大家分享一套以用户体验出发的.NET8 Web开源框架:YiFramework。

项目介绍

YiFramework是一个基于.NET8 + Abp.vNext + SQLSugar 的DDD领域驱动设计后端开源框架,前端使用Vue3,项目架构模式三层架构\DDD领域驱动设计,内置RBAC权限管理、BBS论坛社区系统 以用户体验出发。架构干净整洁、采用微软风格原生框架封装。适合小中大型项目上线、.NET8学习、Abp.vNext学习、SQLsugar学习 、项目二次开发。

项目特点

框架简单易用,框架不以打包形式引用,而是直接以项目附带源码给出,自由度拉满,遵循MIT License协议,允许随意修改(请注明来源即可)。

核心技术

后端

.NET8、Abp.vNext、Jwt、Serilog、Autofac、Mapster、SqlsugarCore、Quartz.Net等。

前端

vue3、axios、echarts、element-plus、vue-router、vite、pinia等。

运维

nginx、gitlab、Jenkins、harbor。

业务支持模块

RABC权限管理系统

用户管理、角色管理、菜单管理、部门管理、岗位管理、字典管理、参数管理、用户在线、登录日志、定时任务、服务监控等功能模块。

BBS社区论坛系统

文章功能、板块功能、主题功能、个人中心、授权中心、权限管理。

演示截图

 

项目源码地址

更多项目实用功能和特性欢迎前往项目开源地址查看👀,别忘了给项目一个Star支持💖。

优秀项目和框架精选

该项目已收录到C#/.NET/.NET Core优秀项目和框架精选中,关注优秀项目和框架精选能让你及时了解C#、.NET和.NET Core领域的最新动态和最佳实践,提高开发工作效率和质量。坑已挖,欢迎大家踊跃提交PR推荐或自荐(让优秀的项目和框架不被埋没🤞)。