您的位置:首页 > 编程语言 > Go语言

golang 构建数据服务

2017-11-22 12:39 387 查看

构建数据服务

本部分的目标是使用 golang database/sql 写出易于阅读、扩展和可维护的数据库服务。重点是掌握经典的 “entity - dao - service” 层次结构编程模型

构建数据服务
一安装数据库

二mysql 基本数据库操作

三数据库访问

四构建数据服务
1 数据库设定

2 数据实体定义

3 数据访问 CRUD

4 事务与服务

5 数据服务测试

6 web 数据服务包装

7 测试 web 服务

8 databasesql 库的问题

五 使用 ORMObject Relational Mapping 库

六进一步工作

一、安装数据库

golang 推荐的数据库是 postgresql ,中国程序员一般比较喜欢 mysql。

这里我们仅介绍用 docker 安装 mysql。

1.1 下载镜像

$ sudo docker pull mysql:5.7
$ sudo docker images
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
mysql                 5.7                 5709795eeffa        13 days ago         408MB


docker pull
从 docker hub 仓库取名字为
mysql:5.7
镜像。 镜像名称格式为
仓库名:标签(tag)
默认 tag 是
latest


docker images
本地仓库镜像列表

安装 docker centos 7 安装 Docker

1.2 启动 mysql 作为主机服务

主机服务(HOST Service)就是作为宿主机器的一个服务进程。

$ sudo docker run -p 3306:3306 --name mysql2 -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7
$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
c55170cbf764        mysql:5.7           "docker-entrypoint..."   8 minutes ago       Up 8 minutes        0.0.0.0:3306->3306/tcp     mysql2


容器可以看作自带文件系统和网络环境的进程。

docker run
就是运行容器镜像文件系统内部的一个命令(CLI)。

参数 1 是镜像
mysql:5.7


-p
是容器内部网络端口到主机端口的映射;

--name
进程的名称, ID 是短唯一标识

-e
设置进程(容器)的环境变量

-d
后台运行

1.3 启动 mysql client 访问服务器

mysql 镜像已自带了命令行客户端,启动 client 的命令:

$ sudo docker run -it --net host mysql:5.7 "sh"
# mysql -h127.0.0.1 -P3306 -uroot -proot
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 7
Server version: 5.7.20 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>


这里:

sudo docker run -it --rm --net host mysql:5.7 "sh"
启动了容器内 sh 进程。

-it
等价于
-i -t
,表示使用当前 stdin 和 stdout 作为该进程的 io

--rm
, 当该进程结束时,自动清理该容器的文件系统空间

--net host
,表示容器使用当前主机的网络环境

参数1 参数2,分别是镜像和在镜像中执行的命令

#
表示你处于容器的超级管理员的 shell

mysql -h127.0.0.1 -P3306 -uroot -proot
mysql 客户端的命令

1.4 一些问题与注意事项

1、使用 –rm 的重要性

对于 mysql 服务器,由于没有使用 –rm 参数,我们创建的数据库会留在该进程(容器)的文件系统上。即使该进程停止,也不会清理这些文件,使用
sudo docker ps  -a
可以查到这些容器的文件系统。

对于 mysql 客户端工具,由于没有值得保存的数据,所以需要进程结束后自动清理。

如果忘记加
--rm
选项,可使用
sudo docker rm $(sudo docker ps -a -q)
可以清理这些残留的文件。

2、如果服务器宕机

如果异常关闭机器,要重启 mysql ,请使用
sudo docker start ...


更多操作 一张图读懂 docker 命令

3、如果忘记使用 -it 选项

这时你无法与应用程序交互,请打开一个新的控制台,使用
docker kill
杀死进程

二、mysql 基本数据库操作

创建数据库 test :
create datebase test;
注意以
;
结束

查询数据库:
show databases;


设定 test 为当前数据库:
use test;
4000


删除数据库 test:
drop database test;


显示表:
show tables;


显示表 userinfo 结构:
describe userinfo


执行查询语句:
sql statement;


测试:

创建数据库test,用户表userinfo,关联用户信息表userdetail。

create database test;
use test;

CREATE TABLE `userinfo` (
`uid` INT(10) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(64) NULL DEFAULT NULL,
`departname` VARCHAR(64) NULL DEFAULT NULL,
`created` DATE NULL DEFAULT NULL,
PRIMARY KEY (`uid`)
);

CREATE TABLE `userdetail` (
`uid` INT(10) NOT NULL DEFAULT '0',
`intro` TEXT NULL,
`profile` TEXT NULL,
PRIMARY KEY (`uid`)
)


三、数据库访问

3.1 CRUD 的含义

对于每个数据表(实体 / Entity ),数据操作分为四个大类:

Create 创建数据实体

Retrieve 获取数据实体

Update 修改数据实体

Delete 删除数据实体

对应的就是四类数据表操作的 sql 语句

3.2 数据驱动管理与数据操作抽象

对于 golang 基础库是 database/sql/driver 包完成。它定义了 数据驱动管理 和 数据操作的抽象接口。 如果你熟悉 java java/jdbc ( Java Database Connectivity API) ,golang 对应的就是 “godbc” (jdbc 精简版)。 主要对应关系如下:

JDBCGoDBC说明
DriverManager classsql functionsmakes a connection with a driver
Driver interfacedriver.Driver interfaceprovides the API for registering and connecting drivers based on JDBC technology (“JDBC drivers”); generally used only by the DriverManager class
Connection interfacedriver.Conn/Tx interfaceprovides methods for creating statements and managing connections and their properties
Statement interfacedriver.Stmt interfaceused to send basic SQL statements
ResultSet interfacedriver.Result/Rows interfaceRetrieving and updating the results of a query
mappings for SQL typesdriver.Value/ValueConverter interface
* JDBC 资源 JDBC 3.0 API

* golang 资源 database/sql接口

在驱动层,Conn 以及 Stmt 是不支持并发访问的有状态实体,所以需要进一步包装,以达到简化应用的目标。

3.3 database/sql 包

database/sql在database/sql/driver提供的接口基础上定义了一些更高阶的方法,用以简化数据库操作,同时内部还建议性地实现一个conn pool。

1、加载驱动:

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


_ "github.com/go-sql-driver/mysql"
启动包在 init() 阶段,自动调用 database.sql.Register(…) 注册驱动到应用中。

2、打开数据库:

db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test?charset=utf8")
if err != nil {
panic(err)
}


sql.Open()函数用来打开一个注册过的数据库驱动,go-sql-driver中注册了mysql这个数据库驱动,第二个参数是DSN(Data Source Name),它是go-sql-driver定义的一些数据库链接和配置信息。它支持如下格式:

DNS 格式:

[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]


例如:

user@unix(/path/to/socket)/dbname?charset=utf8
user:password@tcp(localhost:5555)/dbname?charset=utf8
user:password@/dbname
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname


得到的 db 实体的定义是:

type DB struct {
driver   driver.Driver
dsn      string
mu       sync.Mutex // protects freeConn and closed
freeConn []driver.Conn
closed   bool
...
}


没有任何导出属性,内置连接池,支持多线程操作!!!

3、执行 sql 语句

db 有以下方法执行 sql 语句:

func (db *DB) Exec(query string, args ...interface{}) (Result, error)
func (db *DB) Prepare(query string) (*Stmt, error)
func (db *DB) Query(query string, args ...interface{}) (*Rows, error)
func (db *DB) QueryRow(query string, args ...interface{}) *Row


其中:

*
Exec
执行不返回结果行的语句。如
create, update,delete


*
Query
返回多行结果

*
QueryRow
返回一行结果

*
Prepare
返回编译完成的带参数模板语句

演示代码,使用MySQL数据库

注意: 这样的代码毫无实战价值!

四、构建数据服务

为了编写易于阅读、扩展和维护的程序,让我们沿着 java jdbc 编程风格一路到黑!

编程中,我们继续追寻 “简单、使用原生库” 的原则! 同时采用 java 经典的 “entity - dao - service” 层次结构模型:

+---------------+------------------+
| xxx-service   |提供原子交易服务   |
+---------------+------------------+
| xxx-dao       |提供xxx的数据存取  |
+---------------+------------------+
| xxx-entity    |定义xxx的实体数据  |
+---------------+------------------+


代码与文件结构::
github.com/pmlpml/golang-learning/web/cloudgo-data


4.1 数据库设定

entities/initial.go

package entities

import (
"database/sql"

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

var mydb *sql.DB

func init() {
//https://stackoverflow.com/questions/45040319/unsupported-scan-storing-driver-value-type-uint8-into-type-time-time
db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test?charset=utf8&parseTime=true")
if err != nil {
panic(err)
}
mydb = db
}

// SQLExecer interface for supporting sql.DB and sql.Tx to do sql statement
type SQLExecer interface {
Exec(query string, args ...interface{}) (sql.Result, error)
Prepare(query string) (*sql.Stmt, error)
Query(query string, args ...interface{}) (*sql.Rows, error)
QueryRow(query string, args ...interface{}) *sql.Row
}

// DaoSource Data Access Object Source
type DaoSource struct {
// if DB, each statement execute sql with random conn.
// if Tx, all statements use the same conn as the Tx's connection
SQLExecer
}

func checkErr(err error) {
if err != nil {
panic(err)
}
}


在程序包的 init() 函数中加载数据库驱动

SQLExecer interface 和 DaoSource 使得数据存取不需要考虑 transaction,从而使得事务交给服务层决定!

checkErr 用于简化程序处理错误

4.2 数据实体定义

定义数据,并完成一些数据约束处理。

entities/userinfo-entity.go

package entities

import (
"time"
)

// UserInfo .
type UserInfo struct {
UID        int   `orm:"id,auto-inc"` //语义标签
UserName   string
DepartName string
CreateAt   *time.Time
}

// NewUserInfo .
func NewUserInfo(u UserInfo) *UserInfo {
if len(u.UserName) == 0 {
panic("UserName shold not null!")
}
if u.CreateAt == nil {
t := time.Now()
u.CreateAt = &t
}
return &u
}


4.3 数据访问 CRUD

entities/userinfo-dao.go

package entities

type userInfoDao DaoSource

var userInfoInsertStmt = "INSERT userinfo SET username=?,departname=?,created=?"

// Save .
func (dao *userInfoDao) Save(u *UserInfo) error {
stmt, err := dao.Prepare(userInfoInsertStmt)
checkErr(err)
defer stmt.Close()

res, err := stmt.Exec(u.UserName, u.DepartName, u.CreateAt)
checkErr(err)
if err != nil {
return err
}
id, err := res.LastInsertId()
if err != nil {
return err
}
u.UID = int(id)
return nil
}

var userInfoQueryAll = "SELECT * FROM userinfo"
var userInfoQueryByID = "SELECT * FROM userinfo where uid = ?"

// FindAll .
func (dao *userInfoDao) FindAll() []UserInfo {
rows, err := dao.Query(userInfoQueryAll)
checkErr(err)
defer rows.Close()

ulist := make([]UserInfo, 0, 0)
for rows.Next() {
u := UserInfo{}
err := rows.Scan(&u.UID, &u.UserName, &u.DepartName, &u.CreateAt)
checkErr(err)
ulist = append(ulist, u)
}
return ulist
}

// FindByID .
func (dao *userInfoDao) FindByID(id int) *UserInfo {
stmt, err := dao.Prepare(userInfoQueryByID)
checkErr(err)
defer stmt.Close()

row := stmt.QueryRow(id)
u := UserInfo{}
err = row.Scan(&u.UID, &u.UserName, &u.DepartName, &u.CreateAt)
checkErr(err)

return &u
}


这里仅给出保存数据和查询数据的程序。要点:

定义了 userInfoDao

建在 DaoSource 基础上

第一个字母小写,仅允许包内访问

在 userInfoDao 基础上定义了 CRUD 相关方法

每个方法有固定的编程风格

首先,定义 SQL 语句

然后,Prepare 语句

执行语句

将输出结果映射的对应实体或实体列表

这里最要注意:

的是 sql.Stmt 和 sql.Rows 必须 close(), 务必正确使用 defer 语句!

几乎每个操作都有 error 要处理啊,写出健壮的程序是件不容易的事情

你需要确保程序是多线程安全的!

4.4 事务与服务

entities/userinfo-service.go

package entities

//UserInfoAtomicService .
type UserInfoAtomicService struct{}

//UserInfoService .
var UserInfoService = UserInfoAtomicService{}

// Save .
func (*UserInfoAtomicService) Save(u *UserInfo) error {
tx, err := mydb.Begin()
checkErr(err)

dao := userInfoDao{tx}
err = dao.Save(u)

if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
return nil
}

// FindAll .
func (*UserInfoAtomicService) FindAll() []UserInfo {
dao := userInfoDao{mydb}
return dao.FindAll()
}

// FindByID .
func (*UserInfoAtomicService) FindByID(id int) *UserInfo {
dao := userInfoDao{mydb}
return dao.FindByID(id)
}


创建对象我们使用了事务,查询没有使用事务。

4.5 数据服务测试

编写集成测试脚本是程序开发最重要的工作。请自己编写 entities/userinfo_test.go 测试我们编写的服务!

问题:如何用 travis 测试你的数据服务?

4.6 web 数据服务包装

首先编写 HTTP handler 然后加入 HTTP Router 。

service/userinfo-handler.go

package service

import (
"net/http"
"strconv"

"github.com/pmlpml/golang-learning/web/cloudgo-data/entities"

"github.com/unrolled/render"
)

func postUserInfoHandler(formatter *render.Render) http.HandlerFunc {

return func(w http.ResponseWriter, req *http.Request) {
req.ParseForm()
if len(req.Form["username"][0]) == 0 {
formatter.JSON(w, http.StatusBadRequest, struct{ ErrorIndo string }{"Bad Input!"})
return
}
u := entities.NewUserInfo(entities.UserInfo{UserName: req.Form["username"][0]})
u.DepartName = req.Form["departname"][0]
entities.UserInfoService.Save(u)
formatter.JSON(w, http.StatusOK, u)
}
}

func getUserInfoHandler(formatter *render.Render) http.HandlerFunc {

return func(w http.ResponseWriter, req *http.Request) {
req.ParseForm()
if len(req.Form["userid"][0]) != 0 {
i, _ := strconv.ParseInt(req.Form["userid"][0], 10, 32)

u := entities.UserInfoService.FindByID(int(i))
formatter.JSON(w, http.StatusBadRequest, u)
return
}
ulist := entities.UserInfoService.FindAll()
formatter.JSON(w, http.StatusOK, ulist)
}
}


service/server.go

package service

import (
"net/http"

"github.com/codegangsta/negroni"
"github.com/gorilla/mux"
"github.com/unrolled/render"
)

// NewServer configures and returns a Server.
func NewServer() *negroni.Negroni {

formatter := render.New(render.Options{
IndentJSON: true,
})

n := negroni.Classic()
mx := mux.NewRouter()

initRoutes(mx, formatter)

n.UseHandler(mx)
return n
}

func initRoutes(mx *mux.Router, formatter *render.Render) {
mx.HandleFunc("/hello/{id}", testHandler(formatter)).Methods("GET")
mx.HandleFunc("/service/userinfo", postUserInfoHandler(formatter)).Methods("POST")
mx.HandleFunc("/service/userinfo", getUserInfoHandler(formatter)).Methods("GET")

}

func testHandler(formatter *render.Render) http.HandlerFunc {

return func(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
id := vars["id"]
formatter.JSON(w, http.StatusOK, struct{ Test string }{"Hello " + id})
}
}


4.7 测试 web 服务

添加数据

首先,用 curl POST 一些数据到网站。

$ curl -d "username=ooo&departname=1" http://localhost:8080/service/userinfo[/code] 
-d 参数是 POST 到服务器的数据

查询数据

然后,我们查询上传的数据。

$ curl http://localhost:8080/service/userinfo?userid=[/code] 

4.8 database/sql 库的问题

使用原生 database/sql 必读! Go database/sql 教程。即使你认真阅读了,也不一定能在大并发量下搞定 go sql,直观感觉用 go sql 编程好 辣鸡(和 java 比) !

1. 数据库支持

除了 postgresql 、mysql 、 sqlite3 等开源库有较好支持外, 不是所有数据库都有 golang 驱动。有驱动也不一定完美支持 database/sql 库的标准

2. 数据库迁移

使用 数据库 raw sql 语句操作最大问题就是数据库迁移。好在 golang 仅用于互联网应用开发,与 Oracle,DB2 等关系不大!

教程 4.4 必须注意,占位符不一样

3. Null 处理

golang 是静态语言, nil 都很少, 处理 null 就痛苦万分。 建议不用 null。

4. 顺序敏感

scan 字段是有顺序的

5. error 处理

我们没有用 panic/recover 捕获错误。直接使用 database/sql 处理数据,错误处理不是容易的工作

五、 使用 ORM(Object Relational Mapping) 库

在 java 的世界中, jdbc pk orm 是最常见的,目前 ORM 占上风。 golang 中存在同样问题!

5.1 golang ORM 常见模块

ORM 工具推荐:

xorm 推荐,有完善中文文档:http://xorm.io/ | https://github.com/go-xorm/xorm

gorm 推荐,:https://github.com/jinzhu/gorm

beego orm 有较完善的中支持: https://beego.me/docs/mvc/model/overview.md

太简单了,自学!

5.2 是否需要学习 database/sql

和 java 世界一样,不会 jdbc 只知道 Hibernate 是不会开发数据库应用的。

5.3 orm or not

这是个复杂的话题,orm 是用的反射技术、牺牲性能获得易用性。

如果你做面向程序猿的系统应用而不是面向客户的应用,database/sql 是你的第一选择;相反,orm 可以让你获得开发效率,orm 使得你不需要编写 dao 服务!

六、进一步工作

使用 xorm 或 gorm 实现本文的程序,从编程效率、程序结构、服务性能等角度对比 database/sql 与 orm 实现的异同!

orm 是否就是实现了 dao 的自动化?

使用 ab 测试性能

参考 Java JdbcTemplate 的设计思想,设计 GoSqlTemplate 的原型, 使得 sql 操作对于爱写 sql 的程序猿操作数据库更容易。

轻量级别的扩展,程序员的最爱

程序猿不怕写 sql ,怕的是线程安全处理和错误处理

sql 的 CRUD 操作 database/sql 具有强烈的模板特征,适当的回调可以让程序员自己编写 sql 语句和处理 RowMapping

建立在本文 SQLExecer 接口之上做包装,直观上是有利的选择

暂时不用考虑占位符等数据库移植问题,方便使用 mysql 或 sqlite3 就可以

参考资源:github.com/jmoiron/sqlx

工作 2 有相当难度!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  golang mysql 数据服务