ThinkPHP8.0搭建的后台管理系统EasyAdmin8

项目介绍

EasyAdmin8在EasyAdmin的基础上,使用ThinkPHP v8.0重构,并且开发了Laravel v10.20和webman等框架的版本,是市面上常用框架的快速开发后台管理系统。

官网链接:https://easyadmin8.top/

项目特性

快速CURD命令行
一键生成控制器、模型、视图、JS文件
支持关联查询、字段预设置等等
基于auth的权限管理系统
通过注解方式来实现auth权限节点管理
具备一键更新auth权限节点,无需手动输入管理
完善的后端权限验证以及前面页面按钮显示、隐藏控制
完善的菜单管理
分模块管理
无限极菜单
菜单编辑会提示权限节点
完善的前端组件功能
对layui的form表单重新封装,无需手动拼接数据请求
简单好用的图片、文件上传组件
简单好用的富文本编辑器ckeditor
对弹出层进行再次封装,以极简的方式使用
对table表格再次封装,在使用上更加舒服
根据table的cols参数再次进行封装,提供接口实现image、switch、list等功能,再次基础上可以自己再次扩展
根据table参数一键生成搜索表单,无需自己编写
完善的后台操作日志
记录用户的详细操作信息
按月份进行分表记录
上传文件记录管理
后台路径自定义,防止别人找到对应的后台地址

项目简介

EasyAdmin8 在 EasyAdmin 的基础上更新 ThinkPHP 框架到 8.0 ,PHP 最低版本要求不低于 8.0
ThinkPHP v8.0 和 layui v2.8.x 的快速开发的后台管理系统。

项目地址:
https://github.com/wolf-leo/EasyAdmin8

https://gitee.com/wolf18/easyAdmin8
EasyAdmin8-Laravel 在 EasyAdmin 的基础上使用 Laravel 10.x 重构,PHP 最低版本要求不低于 8.1
Laravel v10.x 和 layui v2.8.x 的快速开发的后台管理系统。

项目地址:

https://github.com/wolf-leo/EasyAdmin8-Laravel

https://gitee.com/wolf18/EasyAdmin8-Laravel
EasyAdmin8-webman 在 EasyAdmin 的基础上使用 webman 最新版重构,PHP 最低版本要求不低于 8.0
webman 和 layui v2.8.x 的快速开发的后台管理系统。

项目地址:

https://github.com/wolf-leo/EasyAdmin8-webman

https://gitee.com/wolf18/EasyAdmin8-webman

特别感谢
以下项目排名不分先后

EasyAdmin: https://github.com/zhongshaofa/easyadmin

ThinkPHP:https://github.com/top-think/framework

Laravel:https://laravel.com/docs/10.x

webman:https://github.com/walkor/webman

Layuimini:https://github.com/zhongshaofa/layuimini

Annotations:https://github.com/doctrine/annotations

Layui:https://github.com/sentsin/layui

Jquery:https://github.com/jquery/jquery

RequireJs:https://github.com/requirejs/requirejs

WangEditor:https://github.com/wangfupeng1988/wangEditor

Echarts:https://github.com/apache/incubator-echarts

BUG反馈
项目使用过程成,如遇到BUG,请到各版本的Gitee中的issue进行提问!

版权信息
EasyAdmin8遵循EasyAdmin的MIT开源协议发布,并提供免费使用。

换脸roop部署cpu和gpu模式

roop 介绍

上面介绍的一键换脸项目叫做Roop,项目官方介绍如下:你只需要一张所需脸部的图像,没有数据集,无需训练,你就可以将拍摄视频其中的面孔替换为你选择的面孔。该工具旨在为快速发展的人工智能生成媒体行业做出富有成效的贡献,它将帮助艺术家完成诸如动画自定义角色或使用角色作为服装模型等任务。本地安装Roop,官方给出了2种选择,基于CPU安装或者基于GPU安装。

1.roop是新开源了一个单图就可以进行视频换脸的项目,只需要一张所需面部的图像。不需要数据集,不需要训练。

2.大概的测试了一下,正脸换脸效果还不错,融合也比较自然。但如果人脸比较大,最终换出的效果可能会有些模糊。侧脸部分的幅度不宜过大,否则会出现人脸乱飘的情况。在多人场景下,也容易出现混乱。

3.使用简单,在处理单人视频和单人图像还是的换脸效果还是可以的,融合得也不错,适合制作一些小视频或单人图像。

roop 安装

获取代码,

sudo apt install git -y
git clone --depth 1 https://github.com/s0md3v/roop.git

安装依赖,

cd roop
pip install -r requirements.txt

# 如遇错误,可依提示。例如:
#  pip install -r requirements.txt --use-pep517
# 如遇 dependency conflict,可修改依赖版本。例如:
#  numpy>=1.23.5

# 若配置镜像
pip config set global.index-url http://mirrors.aliyun.com/pypi/simple
pip config set install.trusted-host mirrors.aliyun.com
cat ~/.config/pip/pip.conf

其实到这里就可以直接运行如下代码进行视频换脸了,首次启动会下载529M的模型文件,目前使用的是CPU,如果你显卡不高不想折腾了的话可以开始体验了。但是我们继续折腾,去开启GPU显卡处理功能。

python run.py

安装CUDA

(1)这是NVIDIA显卡的操作步骤,其它显卡用不了,打开链接:https://developer.nvidia.com/cuda-11-8-0-download-archive,安装程序下载下来之后,直接双击运行安装即可,安装选项选择精简版,安装完成关闭窗口即可。

(2)安装依赖

还在打开Python虚拟环境的cmd窗口中依次输入如下命令并回车,安装依赖,一条命令执行完再运行下一条命令。

pip uninstall onnxruntime onnxruntime-gpu
pip install onnxruntime-gpu

然后再输入如下命令即可启动roop操作窗口进行视频换脸了,

python run.py –execution-provider cuda
左侧点击select a face按钮选择一张待使用的人脸,右侧按钮select a target选择待转换的视频,点击start按钮会提示选择输出视频路径,选择完毕后即可开始转换。

roop运行常用指令
只用CPU处理视频:python run.py
使用GPU处理视频:python run.py –execution-provider cuda
图片保存jpg格式 python run.py –execution-provider cuda –temp-frame-format jpg
视频高清化处理: python run.py –execution-provider cuda –temp-frame-format jpg –frame-processor face_swapper face_enhancer
处理脸部跳闪: python run.py –execution-provider cuda –temp-frame-format jpg –frame-processor face_swapper face_enhancer –similar-face-distance 1.5
指定帧识别人脸并替换:python run.py –execution-provider cuda –reference-face-position 3 –reference-frame-number 166 –similar-face-distance 1.5
其他参数
–temp-frame-format {jpg,png} 用于帧提取的图像格式
–temp-frame-quality [0-100] 用于帧提取的图像质量
–output-video-encoder {libx264,libx265,libvpx-vp9,h264_nvenc,hevc_nvenc} 用于输出视频的编码器
–output-video-quality [0-100] 用于输出视频的质量
–max-memory MAX_MEMORY 最大RAM量(单位:GB)
–execution-threads EXECUTION_THREADS 执行线程数量

python中requirements.txt的生成与使用

在导入一个新项目的时候往往需要下载安装很多依赖,这时如果只是一个个下载非常繁琐,好在很多代码中都附带有requirements.txt文件,可以很方便的安装相应依赖,下面记录一下如何使用与生成该文件。

使用

激活项目具体使用的环境,切换到requirements.txt目录下执行该命令即可。

pip install -r requirements.txt

生成

方式一

生成的是该环境中所有的依赖包,对于想要获得该环境中的某个项目的依赖不友好。

pip freeze > requirements.txt 
# 在当前路径下生成,或者可以指定生成路径如 E:\requirements.txt

方式二

直接获得某个项目的依赖包,但需要另外安装包pipreqs。

# 当前环境下直接安装 
pip install pipreqs

利用该包可以完成对项目依赖包的获取。

# 切换到项目根目录下执行命令 
pipreqs ./ --encoding=utf8 
# 如果当前路径下已经存在requirements.txt文件,会提示附加上--force 
pipreqs ./ --encoding=utf8 --force

Golang经典校验库validator用法解析

一、概述

在接口开发经常会遇到一个问题是后端需要写大量的繁琐代码进行数据校验,所以就想着有没有像前端校验一样写规则进行匹配校验,然后就发现了validator包,一个比较强大的校验工具包下面是一些学习总结,详细内容可以查看validator

二、操作符说明

标记 标记说明
, 多操作符分割
| 或操作
跳过字段验证

三、常用标记说明

标记 标记说明
required 必填 Field或Struct validate:"required"
omitempty 空时忽略 Field或Struct validate:"omitempty"
len 长度 Field validate:"len=0"
eq 等于 Field validate:"eq=0"
gt 大于 Field validate:"gt=0"
gte 大于等于 Field validate:"gte=0"
lt 小于 Field validate:"lt=0"
lte 小于等于 Field validate:"lte=0"
eqfield 同一结构体字段相等 Field validate:"eqfield=Field2"
nefield 同一结构体字段不相等 Field validate:"nefield=Field2"
gtfield 大于同一结构体字段 Field validate:"gtfield=Field2"
gtefield 大于等于同一结构体字段 Field validate:"gtefield=Field2"
ltfield 小于同一结构体字段 Field validate:"ltfield=Field2"
ltefield 小于等于同一结构体字段 Field validate:"ltefield=Field2"
eqcsfield 跨不同结构体字段相等 Struct1.Field validate:"eqcsfield=Struct2.Field2"
necsfield 跨不同结构体字段不相等 Struct1.Field validate:"necsfield=Struct2.Field2"
gtcsfield 大于跨不同结构体字段 Struct1.Field validate:"gtcsfield=Struct2.Field2"
gtecsfield 大于等于跨不同结构体字段 Struct1.Field validate:"gtecsfield=Struct2.Field2"
ltcsfield 小于跨不同结构体字段 Struct1.Field validate:"ltcsfield=Struct2.Field2"
ltecsfield 小于等于跨不同结构体字段 Struct1.Field validate:"ltecsfield=Struct2.Field2"
min 最小值 Field validate:"min=1"
max 最大值 Field validate:"max=2"
structonly 仅验证结构体,不验证任何结构体字段 Struct validate:"structonly"
nostructlevel 不运行任何结构级别的验证 Struct validate:"nostructlevel"
dive 向下延伸验证,多层向下需要多个dive标记 [][]string validate:"gt=0,dive,len=1,dive,required"
dive Keys & EndKeys 与dive同时使用,用于对map对象的键的和值的验证,keys为键,endkeys为值 map[string]string validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,required"
required_with 其他字段其中一个不为空且当前字段不为空 Field validate:"required_with=Field1 Field2"
required_with_all 其他所有字段不为空且当前字段不为空 Field validate:"required_with_all=Field1 Field2"
required_without 其他字段其中一个为空且当前字段不为空 Field `validate:”required_without=Field1 Field2″
required_without_all 其他所有字段为空且当前字段不为空 Field validate:"required_without_all=Field1 Field2"
isdefault 是默认值 Field validate:"isdefault=0"
oneof 其中之一 Field validate:"oneof=5 7 9"
containsfield 字段包含另一个字段 Field validate:"containsfield=Field2"
excludesfield 字段不包含另一个字段 Field validate:"excludesfield=Field2"
unique 是否唯一,通常用于切片或结构体 Field validate:"unique"
alphanum 字符串值是否只包含 ASCII 字母数字字符 Field validate:"alphanum"
alphaunicode 字符串值是否只包含 unicode 字符 Field validate:"alphaunicode"
alphanumunicode 字符串值是否只包含 unicode 字母数字字符 Field validate:"alphanumunicode"
numeric 字符串值是否包含基本的数值 Field validate:"numeric"
hexadecimal 字符串值是否包含有效的十六进制 Field validate:"hexadecimal"
hexcolor 字符串值是否包含有效的十六进制颜色 Field validate:"hexcolor"
lowercase 符串值是否只包含小写字符 Field validate:"lowercase"
uppercase 符串值是否只包含大写字符 Field validate:"uppercase"
email 字符串值包含一个有效的电子邮件 Field validate:"email"
json 字符串值是否为有效的 JSON Field validate:"json"
file 符串值是否包含有效的文件路径,以及该文件是否存在于计算机上 Field validate:"file"
url 符串值是否包含有效的 url Field validate:"url"
uri 符串值是否包含有效的 uri Field validate:"uri"
base64 字符串值是否包含有效的 base64值 Field validate:"base64"
contains 字符串值包含子字符串值 Field validate:"contains=@"
containsany 字符串值包含子字符串值中的任何字符 Field validate:"containsany=abc"
containsrune 字符串值包含提供的特殊符号值 Field validate:"containsrune=☢"
excludes 字符串值不包含子字符串值 Field validate:"excludes=@"
excludesall 字符串值不包含任何子字符串值 Field validate:"excludesall=abc"
excludesrune 字符串值不包含提供的特殊符号值 Field validate:"containsrune=☢"
startswith 字符串以提供的字符串值开始 Field validate:"startswith=abc"
endswith 字符串以提供的字符串值结束 Field validate:"endswith=abc"
ip 字符串值是否包含有效的 IP 地址 Field validate:"ip"
ipv4 字符串值是否包含有效的 ipv4地址 Field validate:"ipv4"
datetime 字符串值是否包含有效的 日期 Field validate:"datetime"

四、标记使用注意

1、当搜索条件与特殊标记冲突时,如:逗号(,),或操作(|),中横线(-)等则需要使用 UTF-8十六进制表示形式

例:

type Test struct {
   Field1 string  `validate:"excludesall=|"`    // 错误
   Field2 string `validate:"excludesall=0x7C"` // 正确.
}

2、可通过validationErrors := errs.(validator.ValidationErrors)获取错误对象自定义返回响应错误 3、自定义校验结果翻译

// 初始化翻译器
func validateInit() {
	zh_ch := zh.New()
	uni := ut.New(zh_ch)               // 万能翻译器,保存所有的语言环境和翻译数据
	Trans, _ = uni.GetTranslator("zh") // 翻译器
	Validate = validator.New()
	_ = zh_translations.RegisterDefaultTranslations(Validate, Trans)
	// 添加额外翻译
	_ = Validate.RegisterTranslation("required_without", Trans, func(ut ut.Translator) error {
		return ut.Add("required_without", "{0} 为必填字段!", true)
	}, func(ut ut.Translator, fe validator.FieldError) string {
		t, _ := ut.T("required_without", fe.Field())
		return t
	})
}

五、使用示例

package main
import (
   "fmt"
   "github.com/go-playground/validator/v10"
)
// 实例化验证对象
var validate = validator.New()
func main() {
   // 结构体验证
   type Inner struct {
      String string `validate:"contains=111"`
   }
   inner := &Inner{String: "11@"}
   errs := validate.Struct(inner)
   if errs != nil {
      fmt.Println(errs.Error())
   }
   // 变量验证
   m := map[string]string{"": "", "val3": "val3"}
   errs = validate.Var(m, "required,dive,keys,required,endkeys,required")
   if errs != nil {
      fmt.Println(errs.Error())
   }
}

包下载:go get github.com/go-playground/validator/v10
demo示例:https://github.com/Jambo-Git/validator-demo

Centos上配置开机自启动的几种方式

CentOS开机自动启动脚本

前言

Linux作为服务器实在是太香了,唯一麻烦的就是服务器重启的时候,一些程序又得手动启动。其实可以通过添加开机自动启动脚本的方法来进行自动启动。

自启动方法

在/etc/rc.d/rc.local中添加启动脚本

chmod +x /etc/rc.d/rc.local

二、在 /etc/rc.d/rc.local 中添加要执行的指定命令

在 /etc/rc.d/rc.local 中添加要执行的指定命令,格式如下:

su - 用户 -c “执行命令”

示例如下:

su - root -c "/home/source/start-api.sh"

手动配置

1、在/etc/rc.d/rc.local中添加服务启动命令

/etc/rc.d/rc.local脚本会在Centos系统启动时被自动执行,所以可以把需要开机后执行的命令直接放在这里。

示例:配置开机启动apollo

vi /etc/rc.d/rc.local

想简单点可以像上面这样直接将服务的启动命令添加到/etc/rc.d/rc.local中。

也可以自己编写服务启动的脚本。由于重启时是以root用户重启,需要保证root用户有脚本执行权限。

1)、编写服务启动的脚本

vi /opt/script/autostart.sh

#!/bin/bash
/root/Downloads/docker-quick-start/docker-compose up -d

2)、赋予脚本可执行权限(/opt/script/autostart.sh是你的脚本路径)

chmod +x /opt/script/autostart.sh

3)、打开/etc/rc.d/rc.local文件,在末尾增加如下内容

/opt/script/autostart.sh

3)、在centos7中,/etc/rc.d/rc.local的权限被降低了,所以需要执行如下命令赋予其可执行权限

chmod +x /etc/rc.d/rc.local

通过chkconfig配置

在CentOS7之前,可以通过chkconfig来配置开机自启动服务。

chkconfig相关命令:

chkconfig –-add xxx //把服务添加到chkconfig列表
chkconfig --del xxx //把服务从chkconfig列表中删除
chkconfig xxx on //开启开机自动启动
chkconfig xxx off //关闭开机自动启动
chkconfig --list //查看所有chklist中服务
chkconfig --list xxx 查看指定服务

chkconfig运行级别level和启动顺序的概念:

chkconfig --list

这里的0到6其实指的就是服务的level。
–level<等级代号> 指定系统服务要在哪一个执行等级中开启或关毕。
等级0表示:表示关机
等级1表示:单用户模式
等级2表示:无网络连接的多用户命令行模式
等级3表示:有网络连接的多用户命令行模式
等级4表示:不可用
等级5表示:带图形界面的多用户模式
等级6表示:重新启动
比如如下命令:

//设定mysqld在等级3和5为开机运行服务
chkconfig --level 35 mysqld on 
//设置network服务开机自启动,会把2~5的等级都设置为on
chkconfig network on

表示开机启动配置成功。
服务的启动顺序又指的什么呢?
服务的启动顺序是指在服务器启动后服务启动脚本执行的顺序。
以系统默认服务network说明:

cat /etc/init.d/network

其中 # chkconfig: 2345 10 90用来指定服务在各个level下的启动顺序。
该配置的含义是network服务在2、3、4、5的level下的启动顺序是10,在1和6的level等级下的启动顺序是90。
chkconfig配置的服务启动顺序最后都会在/etc/rc.d/目录下体现出来:

cd /etc/rc.d/

文件中脚本命名规则,首字母K表示关闭脚本,首字母S表示启用脚本,数字表示启动的顺序.
chkconfig配置实例
通常kibana的官方配置是没有介绍如何配置开机自启动的。这里我配置kibana开机自启动来说明。
1、在/etc/init.d目录下,新建脚本kibana

cd /etc/init.d
vi kibana

脚本内容如下:

#!/bin/bash
# chkconfig: 2345 98 02
# description:  kibana
KIBANA_HOME=/usr/local/kibana-6.2.4-linux-x86_64
case $1 in
 start)
         $KIBANA_HOME/bin/kibana &
         echo "kibana start"
         ;;
 stop)
    kibana_pid_str=`netstat -tlnp |grep 5601 | awk '{print $7}'`
    kibana_pid=`echo ${kibana_pid_str%%/*}`
    kill -9 $kibana_pid
    echo "kibana stopped"
    ;;
 restart)
    kibana_pid_str=`netstat -tlnp |grep 5601 | awk '{print $7}'`
    kibana_pid=${kibana_pid_str%%/*}
    kibana_pid=`echo ${kibana_pid_str%%/*}`
    kill -9 $kibana_pid
    echo "kibana stopped"
    $KIBANA_HOME/bin/kibana &
    echo "kibana start"
    ;;
 status)
    kibana_pid_str=`netstat -tlnp |grep 5601 | awk '{print $7}'`
    if test -z $kibana_pid_str; then
       echo "kibana is stopped"
    else
       pid=`echo ${kibana_pid_str%%/*}`
       echo "kibana is started,pid:"${pid}
    fi
    ;;
*)
    echo "start|stop|restart|status"
    ;;
esac

注意⚠️:
每个被chkconfig管理的服务需要在对应的init.d下的脚本加上两行或者更多行的注释。
第一行告诉chkconfig缺省启动的运行级以及启动和停止的优先级。如果某服务缺省不在任何运行级启动,那么使用 – 代替运行级。
第二行对服务进行描述,可以用\ 跨行注释。

#!/bin/bash
#chkconfig:2345 98 02
#description:kibana

解释说明:
配置kibana服务在2、3、4、5的level等级下脚本执行顺序是98,
1、6的level等级下脚本执行顺序是01。
2、增加脚本的可执行权限

chmod +x kibana

3、查看chkconfig list

chkconfig --list

4、把服务添加到chkconfig列表

chkconfig --add kibana

5、设置kibana服务自启动

chkconfig kibana on //开启开机自动启动

6、查看kibana服务自启动状态

chkconfig --list kibana

7、服务的启动、停止、重启和状态查看

//查看服务状态
service kibana status
//服务启动
service kibana start
//服务停止
service kibana stop
//服务重启
service kibana restart

Golang操作MongoDB:mongo-driver

这篇文章主要介绍了使用GO操作MongoDB,包括安装MongoDB驱动程序连接mongodb的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

1.安装 MongoDB Go 驱动程序

MongoDB Go Driver 由几个包组成。如果您只是使用 go get,则可以使用以下命令安装驱动程序:

go get github.com/mongodb/mongo-go-driver

这个输出可能看起来像一个警告,说明类似于 package github.com/mongodb/mongo-go-driver: no Go files in (…)。这是预期的输出。

如果您使用 govendor 包管理器,则需要使用以下命令安装主 mongo 包以及 bson 和 mongo/options 包:

govendor fetch github.com/mongodb/mongo-go-driver/mongo

govendor fetch go.mongodb.org/mongo-driver/bson

govendor fetch go.mongodb.org/mongo-driver/mongo/options

2.设置连接

导入 MongoDB Go 驱动程序后,我们可以使用 Client.Connect(context) 连接到 MongoDB 部署。我们需要设置一个新客户端,我们需要在设置客户端时传递 mongo 数据库的 URI。然后我们只需要使用上下文调用 client.Connect(context),这将建立我们的连接。

以下代码为我们建立了一个连接:

//Set up a context required by mongo.Connect
ctx, cancel := context.WithTimeout(context.Background(), 10\*time.Second)

//To close the connection at the end
defer cancel()

//We need to set up a client first
//It takes the URI of your database
client, error := mongo.NewClient(options.Client().ApplyURI("your\_database\_uri"))

if error != nil {
 log.Fatal(err)
}

//Call the connect function of client
error = client.Connect(ctx)

//Checking the connection
error = client.Ping(context.TODO(), nil)
fmt.Println("Database connected")

3.在 Go 中使用 BSON 对象

MongoDB 中的 JSON 文档存储在称为 BSON(二进制编码 JSON)的二进制表示中。与将 JSON 数据存储为简单字符串和数字的其他数据库不同,BSON 编码扩展了 JSON 表示以包括其他类型,例如 int、long、date、floating point 和 decimal128。这使得应用程序更容易可靠地处理、排序和比较数据。 Go Driver 有两个表示 BSON 数据的类型:D 类型和 Raw 类型。

D 系列类型用于使用本机 Go 类型简洁地构建 BSON 对象。这对于构造传递给 MongoDB 的命令特别有用。 D 系列包括四种类型:

  • D:BSON 文档。这种类型应该在顺序很重要的情况下使用,例如 MongoDB 命令。
  • M:无序映射。它与 D 相同,只是它不保持顺序。
  • A:一个 BSON 数组。
  • E:D 中的单个元素。

这是一个使用 D 类型构建的过滤器文档的示例,可用于查找名称字段与 Alice 或 Bob 匹配的文档:

bson.D{{
 "name", 
 bson.D{{
 "$in", 
 bson.A{"Alice", "Bob"}
 }}
}}

4. CRUD 操作

对于 CRUD 和其他操作,我们需要使用集合对象,我们可以通过引用数据库中各自的集合来创建它,例如:

BooksCollection := client.Database("test").Collection("books")

插入
对于创建,我们可以将 collection.InsertOne() 用于单个条目,也可以使用 collection.InsertMany() 来接收对象切片。

/\*\*
\* Create - Adding a new book
\* res -\> the insert command returns the inserted id of the oject
\*/

res, err := BooksCollection.InsertOne(ctx, bson.M{"name": "The Go Language", "genre": "Coding", "authorId": "4"})

if err != nil {
log.Fatal(err)
}

在 collection.InsertOne() 中,我们可以通过 bson.M{} 传递一个字符串对象,或者我们可以创建一个我们各自类型结构的对象并传递该对象。

阅读
为了查找文档,我们需要一个过滤文档以及一个指向可以将结果解码成的值的指针。要查找单个文档,请使用 collection.FindOne()。此方法返回一个可以解码为值的结果。过滤器对象指定我们要查找的内容。

filter := bson.D{{"name", "Book 1"}}

// create a value into which the result can be decoded
var result bookType

err = collection.FindOne(context.TODO(), filter).Decode(&result)
if err != nil {
 log.Fatal(err)
}

fmt.Printf("Found a single Book: %+v\n", result)

要查找多个文档,请使用 collection.Find()。此方法返回一个光标。游标提供了一个文档流,我们可以通过它一次迭代和解码一个。一旦游标用尽,我们应该关闭游标。

cur, error := BooksCollection.Find(ctx, bson.D{{}})

var allbooks []\*bookType

//Loops over the cursor stream and appends to result array
for cur.Next(context.TODO()) {
var booksResultHolder bookType

err := cur.Decode(&bookResultHolder)

if err != nil {
log.Fatal(err)
}

allbooks = append(allbooks, &booksResultHolder)
}

//dont forget to close the cursor
defer cur.Close(context.TODO())

// Loop over the result array and perform whatever required
for \_, element := range allbooks {
book := \*element
fmt.Println(book)
}

更新
collection.UpdateOne() 方法允许您更新单个文档。它需要一个过滤文档来匹配数据库中的文档,并需要一个更新文档来描述更新操作。这些可以按照我们在阅读时制作过滤器对象的方式构建。

/\*\*
\* Update
\* Collection has functions like UpdateOne and UpdateMany
\* Returns the Matched and Modified Count
\*/

filter := bson.D{{"name", "Book 1"}}

// Need to specify the mongodb output operator too
newName := bson.D{
{"$set", bson.D{
{"name", "Updated Name of Book 1"},
}},
}

res, err := BooksCollection.UpdateOne(ctx, filter, newName)

if err != nil {
log.Fatal(err)
}

updatedObject := \*res

fmt.Printf("The matched count is : %d, the modified count is : %d", updatedObject.MatchedCount, updatedObject.ModifiedCount)

删除
最后,我们可以使用 collection.DeleteOne() 或 collection.DeleteMany() 删除文档。在这里,我们可以传递 nil 作为过滤器参数,它将匹配集合中的所有文档或任何其他特定参数。我们还可以使用collection.Drop()删除整个集合。

filter = bson.D{{"name", "Updated Name of Book 2"}}

deleteResult, error := BooksCollection.DeleteOne(ctx, filter)

后续步骤
本教程的源代码可以在这里找到。

MongoDB Go 驱动程序的文档可在GoDoc上找到。您可能对有关使用聚合或交易的文档特别感兴趣。

希望本教程对您来说更简单,祝大家编码愉快!

mongodb – 内存占用过高,wiredTigerCacheSizeGB限制

问题

在使用 MongoDB 过程中,会遇到 内存占用随着数据操作而线性增加 的情况;
如果数据持续的大量写入的话,会大量占用服务器内存,出现 OOM 问题,在服务器内存保护机制作用下,MongoDB 会被 kill 掉。

内存增加的原因

mongo为了优化他的读写效率,将内存当做缓存,所以你读写次数越多,缓存就越大。默认值:
从3.4开始,WiredTiger内部缓存默认使用较大的一个, 我用的是4.1
50%(RAM – 1 GB),或256 MB。
例如,我是8G内存,那么最大缓存0.5*(8-1)=3.5G,看到了么。。。mongo默认3.5G都是他的缓存。

cacheSizeGB的介绍

storage.wiredTiger. engineConfig.cacheSizeGB

wiredtiger将使用所有数据的最大缓存大小,wiredTiger缓存工作集(working set)数据的内存大小,单位:GB,

此值决定了wiredTiger与mmapv1的内存模型不同,它可以限制mongod对内存的使用量,而mmapv1则不能(依赖于系统级的mmap)。

默认情况下,cacheSizeGB的值为假定当前节点只部署一个mongod实例,在MongoDB 3,默认情况下,wiredtiger缓存,使用1 GB或安装的物理内存的一半,以较大者为准。

如果当前节点部署了多个mongod进程,那么需要合理配置此值。

如果mongod部署在虚拟容器中(比如,lxc,cgroups,Docker)等,它将不能使用整个系统的物理内存,则需要适当调整此值。默认值为物理内存的一半。

解决

在配置中限制mongo的缓存大小, 引擎需要更换为wiredTiger 默认的mmapv1依赖于mmap不能指定
官方也声称wiredTiger更加优秀
修改(增加)cacheSizeGB配置。

配置如下:

# Where and how to store data.
  engine: wiredTiger
#  mmapv1:
  wiredTiger:
    engineConfig:
      cacheSizeGB: 1

使用Go编译为可执行文件(windows/linux)

案例场景:创建一个两层目录,并在该目录下创建一个文件,将“Hello World”字符写入该文件,并读取出来。

目标:(1)测试案例是否能执行成功;(2)编译代码成windows与linux两种环境下的可执行文件。

测试代码文件名为main.go,内容如下:

package main
 
import (
	"fmt"
	"io/ioutil"
	"os"
)
 
func main() {
	// 文件夹名
	_dir := "data/test"
	exist, err := pathExists(_dir)
	if err != nil {
		fmt.Printf("get dir error![%v]\n", err)
		return
	}
 
	if exist {
		fmt.Printf("has dir![%v]\n", _dir)
	} else {
		fmt.Printf("no dir![%v]\n", _dir)
		// 创建文件夹
		//err := os.Mkdir(_dir, os.ModePerm)
		err := os.MkdirAll(_dir, 0666)
		if err != nil {
			fmt.Printf("mkdir failed![%v]\n", err)
		} else {
			fmt.Printf("mkdir success!\n")
			fileName := _dir + "/test.txt"
			// 创建文件
			os.Create(fileName)
			// 打开文件
			file, _ := os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND, 0666)
			// 当执行完,关闭文件
			defer file.Close()
			// 写内容到文件中
			file.WriteString("Hello World!")
			//读取文件
			data, _ := ioutil.ReadFile(fileName)
			// 打印内容
			fmt.Println(string(data))
		}
	}
}
 
func pathExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, err
}

经过调试,上述代码可正常执行。测试通过。

编译成windows环境exe可执行文件过程,打开文件所在目录,在资源路径框中输入cmd,打开cmd命令框,通过“go env”查看当期环境变量,以windows10环境为例,默认为windows环境。

// 配置环境变量
SET CGO_ENABLED=1
SET GOOS=windows
SET GOARCH=amd64
// 编译命令
go build main.go

编译出来后就是一个可执行文件main.exe,可用鼠标双击直接执行,传到其他电脑上操作,依然可执行,不依赖第三方包(不像Java会依赖JDK)。

执行后,在main.exe所在目录下,生成一个data/test/test.txt,并且打开test.txt文件可看到Hello World。

编译成Linux环境可执行文件,此处除编译环境参数外,其他步骤与上面类似,编译参数如下

// 配置参数
SET CGO_ENABLED=0 
SET GOOS=linux 
SET GOARCH=amd64 
// 编译命令
go build main.go

编译输出的可执行文件名为main,上传至centos7.x系统,使用 “chmod +x main”添加可执行权限,执行 ” ./main “,输出结果与上述windows结果一样。

备注:上述编译环境所在的操作系统均为Windows10,即在windows10上开发代码,编译输出windows与linux两种环境的可执行文件。

Go 语言MySQL 事务操作

1.MySQL驱动下载
描述: Go语言中的database/sql包提供了保证SQL或类SQL数据库的泛用接口, 并不提供具体的数据库驱动, 所以使用database/sql包时必须注入(至少)一个数据库驱动。

Go语言中我们常用的数据库操作, 基本上都有完整的第三方实现,例如本节的MySQL驱动(https://github.com/go-sql-driver/mysql)

# 下载mysql驱动依赖, 第三方的依赖默认保存在 `$GOPATH/src` (注意是在项目目录里)
➜ go get -u github.com/go-sql-driver/mysql
go: downloading github.com/go-sql-driver/mysql v1.6.0
➜ weiyigeek.top go get github.com/go-sql-driver/mysql

# 项目地址
➜ weiyigeek.top pwd
/home/weiyigeek/app/program/project/go/src/weiyigeek.top

# 第三方包地址
➜ go-sql-driv pwd
/home/weiyigeek/app/program/project/go/pkg/mod/github.com/go-sql-driv 作者:WeiyiGeek https://www.bilibili.com/read/cv14620719/ 出处:bilibili

2.MySQL驱动格式
描述: 使用MySQL驱动格式函数原型如下所示:

func Open(driverName, dataSourceName string) (*DB, error) : Open方法是打开一个dirverName指定的数据库,dataSourceName指定数据源,一般至少包括数据库文件名和其它连接必要的信息。

func (db *DB) SetMaxOpenConns(n int) : SetMaxOpenConns方法是设置与数据库建立连接的最大数目。

如果n大于0且小于最大闲置连接数,会将最大闲置连接数减小到匹配最大开启连接数的限制。

如果n<=0,不会限制最大开启连接数,默认为0(无限制)。

func (db *DB) SetMaxIdleConns(n int) : SetMaxIdleConns方法是设置连接池中的最大闲置连接数。

如果n大于最大开启连接数,则新的最大闲置连接数会减小到匹配最大开启连接数的限制。

如果n<=0,不会保留闲置连接。

基础示例

import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	// 数据库DSN(Data Source Name)连接数据源
	dsn := "root:WwW.weiyigeek.top@tcp(10.20.172.248:3306)/test?charset=utf8&parseTime=True"

	// 连接数据库
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		fmt.Printf("DSN : %s Format failed, Error: %v \n", dsn, err)
		panic(err)
	}
	// 此行代码要写在上面err判断的下面(注意点)。
	defer db.Close()

	// 判断连接的数据库
	err = db.Ping()
	if err != nil {
		fmt.Printf("Connection %s Failed, Error: %v \n", dsn, err)
		return
	}

	fmt.Println("数据库连接成功!")
} 

Tips: 为什么上面代码中的defer db.Close()语句不应该写在if err != nil的前面呢?

 

3.MySQL初始化连接
描述: 上面的例子可以看到Open函数可能只是验证其参数格式是否正确,实际上并不创建与数据库的连接,此时我们如果要检查数据源的名称是否真实有效,应该调用Ping方法。

下述代码中sql.DB是表示连接的数据库对象(结构体实例),它保存了连接数据库相关的所有信息。它内部维护着一个具有零到多个底层连接的连接池,它可以安全地被多个goroutine同时使用。

MySQL 用户密码更改:

-- MySQL 5.7.x & MySQL 8.x
ALTER USER `root`@`%` IDENTIFIED BY 'weiyigeek.top';

初始化示例:

// Go 语言利用 MySQL Driver 连接 MySQL 示例
package main
import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)
// 定义一个全局对象db
var db *sql.DB
// 定义一个初始化数据库的函数
func initDB() (err error) {
	// DSN(Data Source Name) - 数据库连接数据源
	// MySQL 5.7.X 与 MySQL 8.x 都是支持的
	dsn := "root:weiyigeek.top@tcp(10.20.172.248:3306)/test?charset=utf8&parseTime=True"
	// 注册第三方mysql驱动到sql中,此处并不会校验账号密码是否正确,此处赋值给全局变量db。
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		fmt.Printf("DSN : %s Format failed\n %v \n", dsn, err)
		return err
	}
	// 尝试与数据库建立连接(校验DSN是否正确)
	err = db.Ping()
	if err != nil {
		fmt.Printf("Connection %s Failed,\n%v \n", dsn, err)
		return err
	}
	// 设置与数据库建立连接的最大数目
	db.SetMaxOpenConns(1024)
	// 设置连接池中的最大闲置连接数,0 表示不会保留闲置。
	db.SetMaxIdleConns(0)
	fmt.Println("数据库初始化连接成功!")
	return nil
}

func main() {
	// 调用输出化数据库的函数
	err := initDB()
	defer db.Close()

	if err != nil {
		fmt.Println("Database Init failed!")
		return
	}
} 

执行结果:

# 连接成功时
数据库初始化连接成功!

# 连接失败时
Connection root:www.weiyigeek.top@tcp(10.20.172.248:3306)/test?charset=utf8&parseTime=True Failed,
Error 1045: Access denied for user 'root'@'10.20.172.108' (using password: YES)
Database Init failed! 

4.MySQL的CRUD操作
库表准备
我们首先需要在MySQL(8.x)数据库中创建一个名为test数据库和一个user表,SQL语句如下所示:

-- 建库建表
CREATE DATABASE test;
USE test;
CREATE TABLE `user` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(20) DEFAULT '',
  `age` INT(11) DEFAULT '0',
  PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

-- 测试数据插入
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (1, 'WeiyiGeek', 20);
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (2, 'Elastic', 18);
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (3, 'Logstash', 20);
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (4, 'Beats', 10);
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (5, 'Kibana', 19);
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (6, 'C', 25);
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (7, 'C++', 25);
INSERT INTO `test`.`user`(`uid`, `name`, `age`) VALUES (8, 'Python', 26); 

示例结构体声明:

type user struct {
	id   int
	age  int
	name string
} 

单行查询
函数原型: func (db *DB) QueryRow(query string, args …interface{}) *Row
函数说明: 单行查询db.QueryRow()执行一次查询,并期望返回最多一行结果(即Row)。
Tips: QueryRow总是返回非nil的值,直到返回值的Scan方法被调用时,才会返回被延迟的错误。(如:未找到结果)

简单示例:

// 查询单条数据示例
func queryRowDemo() {
  var u user
	sqlStr := "select id, name, age from user where id=?"
	// 非常重要:确保QueryRow之后调用Scan方法,否则持有的数据库链接不会被释放 [注意点]
	err := db.QueryRow(sqlStr, 1).Scan(&u.id, &u.name, &u.age)
	if err != nil {
		fmt.Printf("scan failed, err:%v\n", err)
		return
	}
	fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
} 

多行查询
函数原型: func (db *DB) Query(query string, args …interface{}) (*Rows, error)

函数说明: 多行查询db.Query()执行一次查询,返回多行结果(即 Rows), 一般用于执行select命令, 参数args表示 query中的占位参数(空接口)。

简单示例:

// 查询多条数据示例
func queryMultiRowDemo() {
	sqlStr := "select id, name, age from user where id > ?"
	rows, err := db.Query(sqlStr, 0)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", err)
		return
	}
	// 非常重要:关闭rows释放持有的数据库链接 [否则将一直占有连接池资源导致后续无法正常连接]
	defer rows.Close()

	// 循环读取结果集中的数据
	for rows.Next() {
		var u user
		err := rows.Scan(&u.id, &u.name, &u.age)
		if err != nil {
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
	}
} 

插入/更新/删除数据
函数原型: func (db *DB) Exec(query string, args …interface{}) (Result, error)
函数说明: Exec执行一次命令(包括查询、删除、更新、插入等),返回的Result是对已执行的SQL命令的总结。参数args表示query中的占位参数。

具体插入数据示例代码如下:

// 插入数据
func insertRowDemo() {
	sqlStr := "insert into user(name, age) values (?,?)"
	ret, err := db.Exec(sqlStr, "王五", 38)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
  // 新插入数据的id
	theID, err := ret.LastInsertId()
	if err != nil {
		fmt.Printf("get lastinsert ID failed, err:%v\n", err)
		return
	}
	fmt.Printf("insert success, the id is %d.\n", theID)
}

具体更新数据示例代码如下:

// 更新数据
func updateRowDemo() {
	sqlStr := "update user set age=? where id = ?"
	ret, err := db.Exec(sqlStr, 39, 3)
	if err != nil {
		fmt.Printf("update failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影响的行数
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("update success, affected rows:%d\n", n)
} 

具体删除数据的示例代码如下:

// 删除数据
func deleteRowDemo() {
	sqlStr := "delete from user where id = ?"
	ret, err := db.Exec(sqlStr, 3)
	if err != nil {
		fmt.Printf("delete failed, err:%v\n", err)
		return
	}
	n, err := ret.RowsAffected() // 操作影响的行数
	if err != nil {
		fmt.Printf("get RowsAffected failed, err:%v\n", err)
		return
	}
	fmt.Printf("delete success, affected rows:%d\n", n)
} 

综合实践
下述代码简单实现利用Go语言操作MySQL数据库的增、删、改、查等。

数据库连接封装:weiyigeek.top/studygo/Day09/MySQL/mypkg

// 自定义mypkg包 initdb.go
package mypkg
import (
	"database/sql"
	"fmt"

	_ "github.com/go-sql-driver/mysql"
)

// 定义一个mysqlObj结构体
type MysqlObj struct {
	Mysql_host             string
	Mysql_port             uint16
	Mysql_user, Mysql_pass string
	Database               string
	Db                     *sql.DB
}

// 定一个Person结构体
type Person struct {
	Uid  int
	Name string
	Age  int
}

// 定义一个初始化数据库的函数
func (conn *MysqlObj) InitDB() (err error) {

	// DSN(Data Source Name) 数据库连接字符串
	// MySQL 5.7.X 与 MySQL 8.x 都是支持的
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True", conn.Mysql_user, conn.Mysql_pass, conn.Mysql_host, conn.Mysql_port, conn.Database)

	// 注册第三方mysql驱动到sql中,此处并不会校验账号密码是否正确,此处赋值给全局变量db。
	conn.Db, err = sql.Open("mysql", dsn)
	if err != nil {
		fmt.Printf("DSN : %s Format failed\n%v \n", dsn, err)
		return err
	}

	// 尝试与数据库建立连接(校验DSN是否正确)
	err = conn.Db.Ping()
	if err != nil {
		fmt.Printf("Connection %s Failed,\n%v \n", dsn, err)
		return err
	}

	// 设置与数据库建立连接的最大数目
	conn.Db.SetMaxOpenConns(1024)

	// 设置连接池中的最大闲置连接数
	conn.Db.SetMaxIdleConns(0) // 不会保留闲置

	return nil
} 

实践 main 入口函数:

package main
import (
	"database/sql"
	"fmt"
	db "weiyigeek.top/studygo/Day09/MySQL/mypkg"
)

// 单结果语句查询函数示例
func queryPersonOne(conn *sql.DB, Uid int) (res db.Person) {
	// 1.单条SQL语句
	sqlStr := `select Uid,name,age from test.user where Uid=?;`
	// 2.执行SQL语句并返回一条结果
	rowObj := conn.QueryRow(sqlStr, Uid)
	// 3.必须对rowObj调用Scan方法,因为查询后我们需要释放数据库连接对象,而它调用后会自动释放。
	rowObj.Scan(&res.Uid, &res.Name, &res.Age)
	// 4.返回一个person对象
	return res
}

// 多结果语句查询函数示例
func queryPersonMore(conn *sql.DB, id int) {
	// 1.SQL 语句
	sqlStr := `select Uid,name,age from test.user where Uid > ?;`
	// 2.执行 SQL
	rows, err := conn.Query(sqlStr, id)
	if err != nil {
		fmt.Printf("Exec %s query failed!,err : %v \n", sqlStr, err)
		return
	}
	// 3.调用结束后关闭rows,释放数据库连接资源
	defer rows.Close()
	// 4.循环读取结果集中的数据
	for rows.Next() {
		var u db.Person
		err := rows.Scan(&u.Uid, &u.Name, &u.Age)
		if err != nil {
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		fmt.Printf("Uid:%d name:%s age:%d\n", u.Uid, u.Name, u.Age)
	}
}

// 执行插入操作的函数示例
func insertPerson(conn *sql.DB) {
	// 1.SQL 语句
	sqlStr := `insert into user(name,age) values("Go语言",15)`
	// 2.执行插入语句
	ret, err := conn.Exec(sqlStr)
	if err != nil {
		fmt.Printf("Insert Failed, err : %v \n", err)
		return
	}
	// 3.插入数据操作,拿到插入数据库的id值
	Uid, err := ret.LastInsertId()
	if err != nil {
		fmt.Printf("Get Id Failed, err : %v \n", err)
		return
	}
	// 4.打印插入数据的id值
	fmt.Println("插入语句Uid值: ", Uid)
}

// 执行更新操作的函数示例
func updatePerson(conn *sql.DB, age, Uid int) {
	// 1.SQL 语句
	sqlStr := `update user set age=? where Uid = ?`
	// 2.执行插入语句
	ret, err := conn.Exec(sqlStr, age, Uid)
	if err != nil {
		fmt.Printf("Update Failed, err : %v \n", err)
		return
	}
	// 3.更新数据操作,获取到受影响的行数
	count, err := ret.RowsAffected()
	if err != nil {
		fmt.Printf("Get Id Failed, err : %v \n", err)
		return
	}
	// 4.打印数据影响的行数
	fmt.Println("更新数据影响的行数: ", count)
}

// 执行删除数据的操作函数示例
func deletePerson(conn *sql.DB, Uid int) {
	// 1.SQL 语句
	sqlStr := `delete from user where Uid > ?`
	// 2.执行删除的语句
	ret, err := conn.Exec(sqlStr, Uid)
	if err != nil {
		fmt.Printf("Delete Failed, err : %v \n", err)
		return
	}
	// 3.删除数据操作,获取到受影响的行数
	count, err := ret.RowsAffected()
	if err != nil {
		fmt.Printf("Get Id Failed, err : %v \n", err)
		return
	}
	// 4.打印删除数据的影响的行数:
	fmt.Println("删除数据影响的行数: ", count)
}

func main() {
	// 1.mysqlObj 结构体实例化
	conn := &db.MysqlObj{
		Mysql_host: "10.20.172.248",
		Mysql_port: 3306,
		Mysql_user: "root",
		Mysql_pass: "weiyigeek.top",
		Database:   "test",
	}
	// 2.初始化数据库
	err := conn.InitDB()
	if err != nil {
		panic(err)
	} else {
		fmt.Println("数据库初始化连接成功!")
	}

	// 3.程序结束时关闭数据库连接
	defer conn.Db.Close()

	// 4.单行查询
	res := queryPersonOne(conn.Db, 1)
	fmt.Printf("单行查询: %#v\n", res)

	// 5.多行查询
	fmt.Println("多行查询")
	queryPersonMore(conn.Db, 6)

	// 6.插入数据
	fmt.Println("插入数据")
	insertPerson(conn.Db)

	// 7.更新数据
	fmt.Println("更新数据")
	updatePerson(conn.Db, 16, 9)

	// 8.删除数据
	fmt.Println("删除数据")
	deletePerson(conn.Db, 10)
}

执行结果&数据库查询结果:

数据库初始化连接成功!
单行查询: main.person{uid:1, name:"WeiyiGeek", age:20}
多行查询
uid:7 name:C++ age:25
uid:8 name:Python age:26
uid:9 name:Golang age:15
插入数据
插入语句uid值:  10
更新数据
更新数据影响的行数:  1
删除数据
删除数据影响的行数:  1 

5.MySQL预处理

 

基础介绍
什么是预处理?

普通SQL语句执行过程:

客户端对SQL语句进行占位符替换得到完整的SQL语句。

客户端发送完整SQL语句到MySQL服务端

MySQL服务端执行完整的SQL语句并将结果返回给客户端。

预处理执行过程:

把SQL语句分成两部分,命令部分与数据部分。

先把命令部分发送给MySQL服务端,MySQL服务端进行SQL预处理。

然后把数据部分发送给MySQL服务端,MySQL服务端对SQL语句进行占位符替换。

MySQL服务端执行完整的SQL语句并将结果返回给客户端。

 

为什么要预处理?

优化MySQL服务器重复执行SQL的方法,可以提升服务器性能,提前让服务器编译,一次编译多次执行,节省后续编译的成本。

避免SQL注入问题。

SQL注入
描述: 非常注意, 我们任何时候都不应该自己拼接SQL语句, 可能会导致SQL注入的问题。

此处演示一个自行拼接SQL语句的示例,编写一个根据name字段查询user表的函数如下:

// 可被 sql 注入示例
func sqlInjectDemo(name string) {
  var u user
	sqlStr := fmt.Sprintf("select id, name, age from user where name='%s'", name)  // 关键点
	fmt.Printf("SQL:%s\n", sqlStr)
	err := db.QueryRow(sqlStr).Scan(&u.id, &u.name, &u.age)
	if err != nil {
		fmt.Printf("exec failed, err:%v\n", err)
		return
	}
	fmt.Printf("user:%#v\n", u)
} 

当name变量输入以下字符串时便会引发SQL注入问题:

sqlInjectDemo("xxx' or 1=1#")
sqlInjectDemo("xxx' union select * from user #")
sqlInjectDemo("xxx' and (select count(*) from user) <10 #") 

示例演示
Go是如何实现MySQL预处理
描述: database/sql 中使用下面的Prepare方法来实现预处理操作。
函数原型: func (db *DB) Prepare(query string) (*Stmt, error)
函数说明: Prepare方法会先将sql语句发送给MySQL服务端,返回一个准备好的状态用于之后的查询和命令。返回值可以同时执行多个查询和命令。

示例演示:
描述: 此处引用上面封装的结构体成员以及方法,进行数据库的初始化操作。

package main

import (
	"database/sql"
	"fmt"

	db "weiyigeek.top/studygo/Day09/MySQL/mypkg"
)

// ## 预处理查询示例函数
func prepareQuery(conn *sql.DB, id int) {
	// SQL语句
	sqlStr := "select uid,name,age from user where uid > ?;"
	// 预处理
	stmt, err := conn.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v\n", err)
		return
	}
	// 释放预处理
	defer stmt.Close()

	// 查询 uid 为 id 以上的数据
	rows, err := stmt.Query(id)
	if err != nil {
		fmt.Printf("query failed, err:%v\n", err)
		return
	}
	// 释放 rows
	defer rows.Close()

	// 循环读取结果集中的数据,此处利用map来装我们遍历获取到的数据,注意内存申请。
	res := make(map[int]db.Person, 5)
	for rows.Next() {
		var u db.Person
		err := rows.Scan(&u.Uid, &u.Name, &u.Age)
		if err != nil {
			fmt.Printf("scan failed, err:%v\n", err)
			return
		}
		_, ok := res[u.Uid]
		if !ok {
			res[u.Uid] = u
		}
		fmt.Printf("id:%d name:%s age:%d\n", u.Uid, u.Name, u.Age)
	}
	fmt.Printf("%#v\n", res)
}


// ## 插入、更新和删除操作的预处理十分类似,这里以插入操作的预处理为例:
func prepareInsert(conn *sql.DB) {
	// 插入的SQL语句
	sqlStr := "insert into user(name,age) values (?,?)"
	// 进行SQL语句的预处理
	stmt, err := conn.Prepare(sqlStr)
	if err != nil {
		fmt.Printf("prepare failed, err:%v\n", err)
		return
	}
	// 释放 stmt 资源
	defer stmt.Close()

	// 执行预处理后的SQL (可以多次执行)
	_, err = stmt.Exec("WeiyiGeek", 18)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	// 执行预处理后的SQL
	_, err = stmt.Exec("插入示例", 82)
	if err != nil {
		fmt.Printf("insert failed, err:%v\n", err)
		return
	}
	// 插入成功会显示如下
	fmt.Println("insert success.")
}

// 入口函数
func main() {
	// MysqlObj 结构体初始化
	conn := &db.MysqlObj{
		Mysql_host: "10.20.172.248",
		Mysql_port: 3306,
		Mysql_user: "root",
		Mysql_pass: "weiyigeek.top",
		Database:   "test",
	}
	// 数据库初始化
	err := conn.InitDB()
	if err != nil {
		panic(err)
	} else {
		fmt.Println("[INFO] - 已成功连接到数据库!")
	}
	// 关闭数据库对象
	defer conn.Db.Close()

	// 预处理查询
	fmt.Println("预处理查询示例函数 prepareQuery:")
	prepareQuery(conn.Db, 5)

	// 预处理插入
	fmt.Println("预处理插入示例函数 prepareInsert:")
	prepareInsert(conn.Db)
} 

执行结果:

[INFO] - 已成功连接到数据库!

-- 预处理查询示例函数 prepareQuery:
id:6 name:C age:25
id:7 name:C++ age:25
id:8 name:Python age:26
id:9 name:Golang age:16
id:12 name:WeiyiGeek age:18
id:13 name:插入示例 age:82
map[int]mypkg.Person{6:mypkg.Person{Uid:6, Name:"C", Age:25}, 7:mypkg.Person{Uid:7, Name:"C++", Age:25}, 8:mypkg.Person{Uid:8, Name:"Python", Age:26}, 9:mypkg.Person{Uid:9, Name:"Golang", Age:16}, 12:mypkg.Person{Uid:12, Name:"WeiyiGeek", Age:18}, 13:mypkg.Person{Uid:13, Name:"插入示例", Age:18}}

-- 预处理插入示例函数 prepareInsert:
insert success. 

6.MySQL事务处理
什么是事务?

事务:一个最小的不可再分的工作单元;通常一个事务对应一个完整的业务(例如银行账户转账业务,该业务就是一个最小的工作单元),同时这个完整的业务需要执行多次的DML(insert、update、delete)语句共同联合完成。A转账给B,这里面就需要执行两次update操作。

在MySQL中只有使用了Innodb数据库引擎的数据库或表才支持事务, 事务处理可以用来维护数据库的完整性,保证成批的SQL语句要么全部执行,要么全部不执行。

 

事务特性复习 ACID

描述: 通常事务必须满足4个条件(ACID):原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

原子性: 一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

一致性: 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

隔离性: 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。

持久性: 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

 

事务方法原型
描述:Go语言中使用以下三个方法实现MySQL中的事务操作。

func (db *DB) Begin() (*Tx, error) : 开始事务

func (tx *Tx) Commit() error : 提交事务

func (tx *Tx) Rollback() error : 回滚事务

 

实践示例
描述: 下面的代码演示了一个简单的事务操作,该事物操作能够确保两次更新操作要么同时成功要么同时失败,不会存在中间状态。
例如: A 转账给 B 50 RMB,即从A账号余额-50,B账号余额+50。

数据库表创建:

-- 测试表
create table `money` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(20) DEFAULT '',
  `balance` INT(16) DEFAULT '0',
  PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

-- 测试数据
insert into `test`.`money`(`name`,`balance`) values("WeiyiGeek",1200);
insert into `test`.`money`(`name`,`balance`) values("辛勤的小蜜蜂",3650);

-- 查看插入的测试数据
SELECT * from money;
1	WeiyiGeek	1200
2	辛勤的小蜜蜂	3650 

示例代码:

package main
import (
	"database/sql"
	"fmt"

	"weiyigeek.top/studygo/Day09/MySQL/mypkg"
)

// ## 事务操作示例
func transactionDemo(conn *sql.DB, money int) {
	// 开启事务
	tx, err := conn.Begin()
	if err != nil {
		if tx != nil {
			tx.Rollback() // 回滚
		}
		fmt.Printf("begin trans failed, err:%v\n", err)
		return
	}

	// (1) A 用户转账 50 给 B 则 - 50
	sqlStr1 := "UPDATE `money` SET balance=balance-? WHERE id=?;"
	ret1, err := tx.Exec(sqlStr1, money, 1)
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec sql1 failed, err:%v\n", err)
		return
	}
	affRow1, err := ret1.RowsAffected()
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
		return
	}

	// B 用户接收到 A 转账的 50 给 则 + 50
	sqlStr2 := "UPDATE `money` SET balance=balance+? WHERE id=?;"
	ret2, err := tx.Exec(sqlStr2, money, 2)
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec sql2 failed, err:%v\n", err)
		return
	}
	affRow2, err := ret2.RowsAffected()
	if err != nil {
		tx.Rollback() // 回滚
		fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
		return
	}

	// 事务处理影响行数判断是否修改成功
	fmt.Println("事务处理影响行数判断是否修改成功: ", affRow1, affRow2)
	if affRow1 == 1 && affRow2 == 1 {
		fmt.Println("事务正在提交啦...")
		tx.Commit() // 提交事务
	} else {
		tx.Rollback()
		fmt.Println("事务回滚啦...")
	}

	fmt.Println("[INFO] - 事务完成了 ,exec trans success!")
}

func main() {
	// (1) MysqlObj 结构体初始化
	conn := &mypkg.MysqlObj{
		Mysql_host: "10.20.172.248",
		Mysql_port: 3306,
		Mysql_user: "root",
		Mysql_pass: "weiyigeek.top",
		Database:   "test",
	}

	// (2) 数据库初始化
	err := conn.InitDB()
	if err != nil {
		panic(err)
	} else {
		fmt.Println("[INFO] - 已成功连接到数据库!")
	}
	// 关闭数据库对象
	defer conn.Db.Close()

	// (3) 简单的事务操作示例
	transactionDemo(conn.Db, 50)
} 

执行结果:

[INFO] - 已成功连接到数据库!
事务处理影响行数判断是否修改成功:  1 1
事务正在提交啦...
[INFO] - 事务完成了 ,exec trans success!

# 可以看到用户的在数据库中金额变化
1	WeiyiGeek	1150
2	辛勤的小蜜蜂	3700 

至此使用database/sql标准库操作MySQL数据库完毕!

原文连接: https://mp.weixin.qq.com/s/XfvbsppDMgn3EuN7A5R02Q

YFCMF-TP6

YFCMF-TP6 基于ThinkPHP6+FastAdmin的快速开发框架

主要特性

  • 基于Auth验证的权限管理系统
  • 支持无限级父子级权限继承,父级的管理员可任意增删改子级管理员及权限设置
  • 支持单管理员多角色
  • 支持管理子级数据或个人数据
  • 强大的一键生成功能
  • 一键生成CRUD,包括控制器、模型、视图、JS、语言包、菜单、回收站等
  • 一键压缩打包JS和CSS文件,一键CDN静态资源部署
  • 一键生成控制器菜单和规则
  • 一键生成API接口文档
  • 完善的前端功能组件开发
  • 基于AdminLTE二次开发
  • 基于Bootstrap开发,自适应手机、平板、PC
  • 基于RequireJS进行JS模块管理,按需加载
  • 基于Less进行样式开发
  • 基于Bower进行前端组件包管理
  • 强大的插件扩展功能,在线安装卸载升级插件
  • 通用的会员模块和API模块
  • 共用同一账号体系的Web端会员中心权限验证和API接口会员权限验证
  • 二级域名部署支持,同时域名支持绑定到插件
  • 多语言支持,服务端及客户端支持
  • 整合第三方短信接口(阿里云、腾讯云短信)
  • 无缝整合第三方云存储(七牛、阿里云OSS、又拍云)功能
  • 第三方富文本编辑器支持(Summernote、Kindeditor、百度编辑器)
  • 第三方登录(QQ、微信、微博)整合
  • 第三方支付(微信、支付宝)无缝整合,微信支持PC端扫码支付
  • 丰富的插件应用市场

各种设备自适应

响应式的网站设计能够对用户产生友好度,并且对于不同的分辨率能够灵活的进行操作应用。 简洁通俗表达就是页面宽度可以自适应屏幕大小,一个网站 PC、手机、PAD 通吃,页面地址一致。
一个字 “酷 “,可以用 PC 浏览器拉动窗口大小,网站内容显示依旧在设计之内,用户体验非常不错。 一个字 “省”,一个网站 PC、手机、PAD 通吃,这样就不用花那么多心思去维护多个网站,无论是制作还是数据内容。
基于 HTML5 技术
HTML5 对于用户来说,提高了用户体验,加强了视觉感受。HTML5 技术在移动端,能够让应用程序回归到网页,并对网页的功能进行扩展,操作更加简单,用户体验更好。
HTML5 技术跨平台,适配多终端。对于搜索引擎来说,HTML5 新增的标签,使搜索引擎更加容易抓取和索引网页,从而驱动网站获得更多的点击流量。
人性化的后台管理
传统的企业网站管理系统是以技术人员的角度出发,设计了很多复杂的功能,并且操作流程上也很复杂,对于最终要操控这个系统的管理员来说并不是很人性化,YFCMF 所做的只是简化不必要的功能,从操作习惯下合理地布局和设计界面,让最普通的用户,即使没有网站管理的经营,也能很容易上手我们的系统。

问题反馈

在使用中有任何问题,请使用以下联系方式联系我们

交流社区: https://bbs.iuok.cn

QQ群: 345183861

Email: (ice#sbing.vip, 把#换成@)

Github: https://github.com/0377/yfcmf-tp6

Gitee: https://gitee.com/nymondo/yfcmf-tp6

特别鸣谢

感谢以下的项目,排名不分先后

Fastadmin:https://www.fastadmin.net

ThinkPHP:http://www.thinkphp.cn

AdminLTE:https://adminlte.io

Bootstrap:http://getbootstrap.com

jQuery:http://jquery.com

Bootstrap-table:https://github.com/wenzhixin/bootstrap-table

Nice-validator: https://validator.niceue.com

SelectPage: https://github.com/TerryZ/SelectPage