pgears

package module
v0.0.0-...-df70bd3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 30, 2016 License: Apache-2.0 Imports: 10 Imported by: 3

README

PGears

Toolkits for easy to use PostgreSQL in Golang

说明一下……

首先,这个是给公司的项目自用的,所以不会坑。但是不保证符合正常的需要……我本来就是个不正常的程序员啊……

普通用户我推荐 xorm 或 beedb,前者功能很全,很强大,后者使用简单方便。一般的场景够用了。

PGears 顾名思义,着眼于为 Go 语言项目访问和操作 PostgreSQL 提供一组方便的工具。不一定全是 ORM , 也应该不是完整的 ORM 工具。实际上 ORM 工具我只喜欢 SQLAlchemy ……

通用性

通常来说,一个完整的数据库访问工具总是要考虑通用性。但是在这里,我希望尽可能实现对 Postgres 的支持。PG 的异步访问,丰富的数据类型,强大的编程能力。对应用层编程的支持可以带来很多提升。但是如果 要开发一个普适于多种数据库产品的访问工具,难免要在特异性的支持上有所牺牲。例如 MySQL 就缺少很多 Postgres 里好用的数据类型。

动态和静态化

在 Python 里我常常感到很不舒服的是,有些代码我们都知道它其实是确定的,但是在一个全动态的语言中, 它总会一遍又一遍的被执行。例如传统上的 SQLAlchemy 或 WxPython 的初始化逻辑(wx很久不用了,不过 SQLAlchemy 在这方面是有所改进的)。Go 本身是个静态化的语言,而且它的反射功能相当有限。加上没有模板 /宏这样的编译期代码推导能力。我们很难写出强类型的普适代码。

ADO.net 在早期 CLR 不提供范型的时代,就提供了一个代码生成工具,它非常好用,可以帮助程序员构造出尽 可能安全和高效的代码。

例如,解决对象到存储的映射时,一个常见的做法是对象隐含一个id属性,映射到数据库表的自增整型 id 主键。 这在大多数的中小型应用场景够用了,而且是最合理的做法。但是如果要面对一个分布(未必很大)的架构,自增 id 不是很好用,特别是我们需要切分数据库,迁移对象的时候,很麻烦。

但是要在一个静态类型的语言中,同时支持多种不同的 PK 类型,也确实不容易。Beedb 和 XORM 都在这方面下了功夫,有很多值得学习的地方。

PGears 中会结合运行时的类型检测,通过反射构造静态类型函数的功能,和一些代码生成的工具,尽可能提供便于 ……便于我自己使用的组合吧……如果我自己都用着不方便就不用跟别人说很好用了……

对象映射和 SQL

从编程语言的角度讲,我们需要将一些对象存储起来,需要的时候再找到它们,加载,处理,再保存。从数据库的 角度,我们有一些数据,需要管理,所以要在编程语言中表现成适于处理的形式。前者的话,我接触过的技术中, 当年 Mono 相关的有一个对象数据库 db4 ,算是走的比较远的。后者对应的是一些比较完整的 ORM 工具会提供的 SQL 表达式对象化功能。

类型映射

Go 的类型系统跟 Java 或 C# 不太一样,后两者有一个逻辑自洽的类型系统,有 Object 和 Type 类型( Type 是 Object 的子类),有高度一致性的 Equal、Compare和Disposable机制。这些 Python 也有, Go 没有。另一方面 Go 的对象机制也不像 C/C++ ,一切都可以追溯到比较原始的字节和指针。Go 的对象体系基于一组互相独立的类型:各种字节宽度的浮点数和有或无符号的整型,字符串、线性定长序列(数组 )和变长序列(Slice),Map,Struct,Func,Interface和interface{}以及它们的指针组成。相当的 不简洁。加上没有代码生成技术(目前的版本甚至也没有足够强的类型推导),这使得我们不太容易实现一个类 似 Java 中的 Hibernate (代码生成能力限制)或者 Python 的 SQLAlchemy(类型的动态程度限制) 的工具。

构造一组接受 interface{} 参数,通过反射获取并分析结构,再填充数据,是个可行的做法。目前看到 的工具主要是通过这样的方式工作的。甚至在数据库访问工具的底层,我们总是只能通过类似的做法处理—— 在数据库和应用层的边界上,我们接受字节流,然后分析内容,根据协议做转型。

但是另一方面,反射并不是一个高效的做法,何况重复的在运行期分析一个已经静态化的结构,很浪费。Go 是一 门静态编译型语言,我们使用它并不是为了把 Python 里要做的事情重复一遍。何况在静态类型语言里重复动态 语言擅长的事情其实很笨拙。例如在Python里我们可以 在运行期定义出新的类型,在 Go 里就没有办法运行时构造新的 struct——如果你有办法,请来教教我, 非常感谢:)。

PGrears 在这方面计划采取一个折衷的方式。一方面我们也应该提供 interface{} 方式的泛化接口,另一方 面提供可以将接口静态化的工具,这方面可以利用 reflect.MakeFunc 。官方文档提供了一个非常 好的范例。在函数生成和绑定的逻辑中,可以尽可能的完成一些能确定的逻辑。这样虽然还是要大量使用反射, 但是比起完全在业务函数调用的时候做分析,也能好很多。一方面可以减少一些参数传递时类型错误造成的panic, 另一方面结构分析逻辑可以在整个进程的生命期只执行一次。而且静态化之后的函数便于将来手工优化, 我认为这种风格值得鼓励,将来无论在文档还是工具上都应该尽量予以支持。

当然,我觉得根本解决方案还是要语言内置一个足够强的代码生成技术,但是目前没有。朋友推荐了一个类似 CodeSmith的 http://clipperhouse.github.io/gen 项目,可以尝试。不过 PGears 会尝试内置一个类似的东西。

在类似 sqlexpress 这一层上,会尽量的提供一些特有功能和语法的支持。无论如何,PostgreSQL 的 PL/SQL 开发,总是会遵循不同于 Golang 的思路,所以完全复现有困难,只能说尽量找一条中间道路。

PostgreSQL 特有类型和功能

目前在 pq 这一层上没有对 Postgres 有特别突出的支持,只实现了一个时间类型。不过沿着的设计思路拓展 下去还是不太难的。接下来会参考同行的实现对pq贡献一些代码。例如JSON类型的支持,是一个值得尝试的方向。

在pg中没有看到异步化的支持,事实上 Python 的 psycopg2 一直有异步能力,应该是在 c 层面带来的。 这是一个值得探索的点。另外 go 本身就有很好的异步模型,可以充分利用。

在 SQL Expressisons 的层面上,支持PG的全文搜索、正则表达式、服务器端函数等功能,是比较自然的方向。

PostgreSQL 的 SQL 编程能力非常的强大,甚至包括递归语法,当然这种东西是否需要支持,取决于我们项目 中是否会用到……

null 和 nil

某种角度上讲,关系型数据库的 null 值可以对应到应用层语言的 nil 。不过在 go 中使用 nil 并不像在 PostgreSQL 中使用 null 那么方便和安全——由于有 interface{} 存在,其实已经比其它很多语言都好了 ——,其实 go 的内置库已经提供了一组 NullXxxx 类型,可以方便的处理带空值的类型。考虑到只是在类型分 析时提供若干分支,我会尽量提供对这一组类型的支持。

路线图

其实路线图什么的真没有……

目前的目标就是尽快做出一个简单够用的工具,我手头好几个网站项目要用到……然后,剩下的,就看心情吧……

——刘鑫

版本

1.0.1(假设刘老师的版本定为1.0)

增加支持sqlite Debug测试,需要下载 https://github.com/mattn/go-sqlite3 组件

注意测试用的dbUrl ,必须用 sqlite://./test.db,你可以理解成sqlite:// 以后再扩展其他数据库也会如此考虑。

package auth

import (
	"你的model目录"
	_"github.com/Dwarfartisan/pgears/exp"
	"testing"
)

func TestServer2(t * testing.T){
	models.Engine.CreateTable("你的model表,注意此处需要填写完成路径")
	models...等单元测试的编码。
}

1.0.2

增加了tag里面对db fieldtype 的支持定义,具体可以参见example里面的例子。

Documentation

Overview

Package pgears 的 Engine 包中包含了对外的基本接口。核心是 Engine 类型,它可以用于单个对象的CURD操作,也可以 生成预备好类型的查询集。或者通过 Engine 更方便的访问 pg 组件。

extractor.go 中提供了提取字段值的逻辑,主要封装了对 JSON 字段的转换

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ExtractField

func ExtractField(val reflect.Value, field reflect.StructField) interface{}

这个是动态选择的封装接口。除了内置类型的extract,它还提供了对json类型的提取。

Types

type DbField

type DbField struct {
	GoName      string
	DbName      string
	DbFieldType string
	IsPK        bool
	DbGen       bool
	NotNull     bool
	Extract     func(reflect.Value) (interface{}, func() error)
}

DbField 结构用 Go 语言表述一个数据库字段的结构。 stype 指结构字段的类型,这个类型总是指的值类型,如果该字段为指针,就取其 Elem() 判断原始类型是否是指针,看它是不是 NotNull 就可以了。

func NewDbField

func NewDbField(fieldStruct *reflect.StructField) *DbField

NewDbField 是 DbField 的内部构造函数,通常由其它pgears内部类型调用

type DbTable

type DbTable struct {
	Fields *FieldMap
	Pk     *FieldMap
	NPk    *FieldMap
	DbGen  *FieldMap
	NDbGen *FieldMap
	// contains filtered or unexported fields
}

DbTable 用 Go 语言描述了数据表的结构

func NewDbTable

func NewDbTable(typ *reflect.Type, tablename string) *DbTable

NewDbTable 构造一个数据表映射结构

func (*DbTable) AllInsertExpr

func (dbt *DbTable) AllInsertExpr() (exp.Exp, []string)

AllInsertExpr 方法生成一个用于 Insert 的表达式,包含所有的字段,包括dbgen

func (*DbTable) AllInsertGears

func (dbt *DbTable) AllInsertGears() (t *exp.Table,
	fields []exp.Exp, values []exp.Exp, names []string)

AllInsertGears 方法生成用于 Insert 的表达式组件,包含所有的字段,包括dbgen

func (*DbTable) DropTable

func (dbt *DbTable) DropTable() string

func (*DbTable) Extract

func (dbt *DbTable) Extract() (t *exp.Table, pk []exp.Exp, other []exp.Exp, cond exp.Exp)

Extract 方法从 DbTable 结构中得到分别表示主键、字段表达式和条件表达式的部分,便于拼接

func (*DbTable) FetchExpr

func (dbt *DbTable) FetchExpr() (expr exp.Exp, names []string)

FetchExpr 方法生成一个用于 Select 包含所有主键的给定对象的 SQL 表达式

func (*DbTable) GetCreateTableSQL

func (dbt *DbTable) GetCreateTableSQL() string

把当前表对象直接转换成建表语句

func (*DbTable) MergeInsertExpr

func (dbt *DbTable) MergeInsertExpr() (exp.Exp, []string)

MergeInsertExpr 方法生成一个用于 Insert 的表达式,其中不包括在数据库端自动生成的字段,这些字段包含在 returning 中

func (*DbTable) MergeInsertGears

func (dbt *DbTable) MergeInsertGears() (t *exp.Table,
	fields []exp.Exp, values []exp.Exp, returning []exp.Exp,
	names []string)

MergeInsertGears 方法生成用于 Insert 的表达式组件,其中不包括在数据库端自动生成的字段, 这些字段包含在 returning 中

func (*DbTable) UpdateExpr

func (dbt *DbTable) UpdateExpr(sets []string) (expr exp.Exp, names []string)

UpdateExpr 方法生成一个用于 Update 的表达式,这里需要调用者给出准备Update的字段名, 函数生成形如 Update XXX Set ... Where cond 的 SQL 表达式, update 语句中包含主键字段列表,所以虽然它的sets由用户指定,仍然返回参数命名表

func (*DbTable) UpdateGears

func (dbt *DbTable) UpdateGears(s []string) (t *exp.Table,
	sets []exp.Exp, cond exp.Exp, names []string)

UpdateGears 方法生成用于 Update 的表达式组件,这里需要调用者传入 sets 的字段列表

type Engine

type Engine struct {
	*sql.DB
	// contains filtered or unexported fields
}

Engine 类型是管理数据源的业务类型

func CreateEngine

func CreateEngine(url string) (*Engine, error)

CreateEngine 方法构造一个新的 Engine 对象,error 不为空的话表示构造过程出错。

func (*Engine) AutoTran

func (engine *Engine) AutoTran(fun func(*Engine, *Tran) (interface{}, error)) (interface{}, error)

AutoTran 是一个简单的事务封装,只要传入一个函数,其函数体会在一个封闭的事务环境中执行, 并且根据返回的错误信息决定是否Commit

func (*Engine) Begin

func (engine *Engine) Begin() (*Tran, error)

Begin 返回一个封装后的事务对象

func (*Engine) CreateTable

func (e *Engine) CreateTable(typeName string) error

增加建表功能 add by zhaonf 2015.12.11 5:16 主要提供脚本进行测试使用

func (*Engine) Delete

func (e *Engine) Delete(obj interface{}) error

Delete 当前的设定是根据pk删除,所以无返回,但是—— TODO:如果返回的受影响数据为0,记一个warning ,发一个error 如果大于1,应该log一个Fail,发一个error,必要的话panic也是可以的……

func (*Engine) DropTable

func (e *Engine) DropTable(typeName string) error

add by zhaonf 2015.12.14 10:29 主要提供脚本测试,不要随意在生产和测试环境使用,只可以在脚本测试中玩哦!

func (*Engine) Fetch

func (e *Engine) Fetch(obj interface{}) error

这里要验证传入的obj的类型是否已经注册,但是应该允许匿名类型,这个接口要另外设计 目前操作匿名类型可以先拼接一个 Exp ,然后让Engine 去 prepare 出对应的 Query, 然后用 Query 和 Result 操作

func (*Engine) FinaToCona

func (e *Engine) FinaToCona(typename string, fieldname string) string

Struct Field Name to Table Column Name

func (*Engine) Insert

func (e *Engine) Insert(obj interface{}) error

insert 的设定是 insert 插入所有字段,包括主键,有时候我们需要在应用层生成主键值,就使用这个逻辑

func (*Engine) InsertMerge

func (e *Engine) InsertMerge(obj interface{}) error

insert merge 的设定是insert仅插入非dbgen数据,所有dbgen字段从数据库加载load后的 这个逻辑用于那些主键在数据库层生成的场合,例如自增 id 主键,服务端uuid,时间戳等

func (*Engine) MapStructTo

func (e *Engine) MapStructTo(s interface{}, tablename string)

将类型映射到明确指定的表,遵循一个简单的规则: - tag 可以指定类型,不过一般不用,int/int64 对应 integer, double 对应 float64, text 对应 string。 - 如果 int/int64 的字段,tag 指定了 serial, 就是 serial - time.Time 类型对应到 timestamp,其实其它时间日期类型也可以,pq支持就可以 - tag 包含 PK:"true" 的是主键,可以有复合主键,无关类型 - tag 包含 jsonto:"map" 的 映射到 map[string]interface{} - tag 包含 jsonto:"struct" 的映射到结构,具体的结构类型是一个 reflect.Type, 保存在 DbField 类型的 gotype 字段 - 如果字段定义为值类型,表示对应的是 not null - 如果定义为指针类型,表示对应的是可以为null的字段,读取后的使用应该谨慎 - tag 中的 field:"xxxx" 指定了对应的数据库子段名,这个不能省,一定要写。 没做自动转换真的不是因为懒……相信我……

func (*Engine) PrepareFor

func (e *Engine) PrepareFor(typeName string, exp exp.Exp) (*Query, error)

PrepareFor 方法使得 SQL 表达式可以预先 Prepare

func (*Engine) PrepareSQL

func (e *Engine) PrepareSQL(exp exp.Exp) (*sql.Stmt, error)

PrepareSQL 不做预设的fetch等功夫,如果我们只需要做简单的查询,或者要自己手动静态化, 就可以走这个接口

func (*Engine) RegistStruct

func (e *Engine) RegistStruct(s interface{}, tablename string)

将类型注册到指定的表上,这个操作不要求类型完全匹配表结构,只要部分符合,主键完整即可 适用于部分内容的填充、更新等操作 走这个接口注册类型表示它不是完整的对应数据库中的表。所以可以用表里的数据填充结构,但是 不能反过来依赖其中的结构去推断和维护表 这个接口显然可以将类型注册到不存在的表,这超出了我最初的设计。建议使用这种表名的时候, 起一个跟业务有关的,容易记忆的名字

func (*Engine) Scalar

func (engine *Engine) Scalar(expr exp.Exp, args ...interface{}) (interface{}, error)

用于类似 select count(*) from table where cond 这种只需要获取单个结果的查询 程序逻辑直接获取单行的第一列,如果查询实际返回的结果集格式不匹配……大概会出错……吧……

func (*Engine) TynaToTana

func (e *Engine) TynaToTana(typename string) string

Type Name to Table Name 暂时不支持schema NOTE: 需要注意的是当前使用type的Name(),其中包含packages名

func (*Engine) Update

func (e *Engine) Update(obj interface{}) error

update 当前的设定是直接更新,所以无返回,但是—— TODO:如果返回的受影响数据不为一,记一个warning ,发一个error

type FieldMap

type FieldMap struct {
	// contains filtered or unexported fields
}

FieldMap 结构用于管理字段组的双键 map,这样就可以根据结构或表字段名找到对应的字段

func NewFieldMap

func NewFieldMap() *FieldMap

NewFieldMap 函数构造一个go 结构到 数据表结构的映射关系

func (*FieldMap) DbGet

func (fm *FieldMap) DbGet(goname string) (*DbField, bool)

DbGet 方法获取数据库结构中给定字段名对应的那个 Dbfield ,如果该成员不存在,状态码返回 false

func (*FieldMap) DbKeys

func (fm *FieldMap) DbKeys() []string

DbKeys 方法给出所有注册的数据库字段名

func (*FieldMap) GoGet

func (fm *FieldMap) GoGet(goname string) (*DbField, bool)

GoGet 方法获取 go 结构中给定字段名对应的那个 Dbfield ,如果该成员不存在,状态码返回 false

func (*FieldMap) GoKeys

func (fm *FieldMap) GoKeys() []string

GoKeys 方法给出所有注册的 Go 结构字段名

func (*FieldMap) Length

func (fm *FieldMap) Length() int

Length 返回 gomap 的长度,即 go 结构的字段数目

func (*FieldMap) Set

func (fm *FieldMap) Set(field *DbField)

Set 方法在映射集中注册一个新的 DbField

type NotFound

type NotFound struct {
	// contains filtered or unexported fields
}

func NewNotFound

func NewNotFound(object interface{}) NotFound

func (NotFound) Error

func (e NotFound) Error() string

type Parser

type Parser struct {
	*Engine
	// contains filtered or unexported fields
}

Parser 是用于解析参数的独立环境,这样不同语句的 Prepare 可以安全的异步化。将来可 能会通过这个组件实现命名参数。

func NewParser

func NewParser(engine *Engine) *Parser

NewParser 方法构造一个新的 Parser

func (*Parser) Scope

func (p *Parser) Scope() exp.Exp

Scope 方法返回 parser 的作用域

func (*Parser) SetScope

func (p *Parser) SetScope(exp exp.Exp)

SetScope 给 Parser 指定一个作用域,它是一个 Exp

type Query

type Query struct {
	*sql.Stmt
	// contains filtered or unexported fields
}

func (*Query) Q

func (q *Query) Q(args ...interface{}) (*ResultSet, error)

func (*Query) QBy

func (q *Query) QBy(arg interface{}) (*ResultSet, error)

如果有一个已经准备好的 struct ,可以用这个方法传入,会 根据反射得到的 accessable 字段拆解出参数传入 暂时只是根据顺序提取字段,将来有可能会增加根据字段名和参数名的对照进行传递的功能

type ResultSet

type ResultSet struct {
	*sql.Rows
	// contains filtered or unexported fields
}

func (*ResultSet) FetchOne

func (r *ResultSet) FetchOne(obj interface{})

Scan a row and fetch into the object 严格来说,这里传入的对象应该严格匹配prepare时使用的类型, 但是从理论来讲,似乎任何结构相同的都可以。有待测试 此处返回值应为error,但是fetcher构造的时候没有加入,这个将来应该补全

func (*ResultSet) LoadOne

func (r *ResultSet) LoadOne(obj interface{})

func (*ResultSet) Scalar

func (r *ResultSet) Scalar(slot interface{}) error

get the first column in current row, like scalar method in .net clr's ado.net this method don't close connect, need close it after used.

type Tran

type Tran struct {
	*sql.Tx
	// contains filtered or unexported fields
}

Tran 是事务对象的一个简单包装

func (*Tran) Insert

func (tran *Tran) Insert(obj interface{}) error

带有事务的版本 insert 的设定是 insert 插入所有字段,包括主键,有时候我们需要在应用层生成主键值,就使用这个逻辑

func (*Tran) InsertMerge

func (tran *Tran) InsertMerge(obj interface{}) error

insert merge 的设定是insert仅插入非dbgen数据,所有dbgen字段从数据库加载load后的 这个逻辑用于那些主键在数据库层生成的场合,例如自增 id 主键,服务端uuid,时间戳等

func (*Tran) Query

func (tran *Tran) Query(query *Query) *Query

Query 将一个给定的Query转为事务Query,作用类似 sql.Tx 的 Stmt 方法

Directories

Path Synopsis
exp 包中是用于 SQL 语句生成的各种组件 exp.go 中保存了 exp 包的一些未分类组件 TODO 普通的表达式没有处理字符串转义,需要接受外部传入的数据的话,请一定参数化 目前还没有对纯文本文法中的命名做映射,要享受转换的能力,请使用 table 和 field 类型 select.go 文件提供以 Sel 类型为核心的 Select 语句生成功能 NOTE Table 的 name 应该是模型的类型名而非表名,表名只在注册模型的时候显式出现 table 和 field 都是指应用层命名,也就是表和字段对应的类型名和字段名 暂时还不支持主子表的嵌套表示,这个应该尽快实现 在 Postgres 中,用双引号包围的命名大小写敏感,可以包含带有空格等特殊符号的字符 这里暂时没有计划支持。
exp 包中是用于 SQL 语句生成的各种组件 exp.go 中保存了 exp 包的一些未分类组件 TODO 普通的表达式没有处理字符串转义,需要接受外部传入的数据的话,请一定参数化 目前还没有对纯文本文法中的命名做映射,要享受转换的能力,请使用 table 和 field 类型 select.go 文件提供以 Sel 类型为核心的 Select 语句生成功能 NOTE Table 的 name 应该是模型的类型名而非表名,表名只在注册模型的时候显式出现 table 和 field 都是指应用层命名,也就是表和字段对应的类型名和字段名 暂时还不支持主子表的嵌套表示,这个应该尽快实现 在 Postgres 中,用双引号包围的命名大小写敏感,可以包含带有空格等特殊符号的字符 这里暂时没有计划支持。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL