一个TDengine的python ORM库: crown

本文介绍了一个用于操作TDengine的 python ORM库。本文的预期读者是,需要使用python语言操作TDengine数据库的开发人员。

项目地址: https://github.com/machine-w/crown

什么是ORM? ORM就是对象关系映射(Object Relational Mapping),是一种程序设计技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。从效果上说,它其实是创建了一个可在编程语言里使用的“虚拟对象数据库”。简单说来就是,通过建立类与数据库表,对象与数据库数据条目的对应关系。从而可以通过编程语言的数据类型操作数据库。

为TDengine开发ORM库的动因

作为一个使用python作为主力编程语言的开发者,笔者经常要编写操作各种数据库的代码。对于键值对类型(如:redis)或者文档类型(如:mongodb)的数据库,python生态都提供了很好的第三方连接库。而对于最常使用的关系型数据( 如:mysql、PostgreSQL),python则提供了sqlalchemy、peewee等ORM第三方库的解决方案。所以,笔者日常工作中,需要手工拼接sql查询语句的场景非常少 ( 甚至慢慢忘记了这项技能)。

近来,笔者需要带领团队完成一个智能电力系统的项目。在技术选型过程中发现了优秀的物联网大数据平台TDengine 。经过测试和评估发现,无论从超高性能和稳定性、还是简洁的设计、开源的理念。TDengine都非常适合作为智能电力系统的基础平台使用。但是,在使用过程中,我们发现了一个比较棘手的问题。那就是:由于 TDengine 诞生不久,相比较其他已经发展很多年的其他数据库平台,周边的相关生态软件还略少一些。特别是,苹果操作系统OS X下暂时没有原生连接器可用,写好的程序需要拿到Linux上去调试。这对于被“宠坏”的python程序员来讲真的没法适应。而且考虑到笔者团队中其他程序员都习惯了ORM的操作方式,对原始sql并不熟悉。所以,笔者意识到:如果使用原生的连接器进行开发,将会遇到很多困难。于是就开始了TDengine的开源ORM库的开发。
一方面,可以帮助团队更高效的完成系统开发工作。另外一方面,也可以为帮助TDengine更好的完善生态工具链。

如何安装和使用

简介

  • 需要python 3.0版本以上
  • 在tdengine 2.0.8版本测试通过
  • 解决mac操作系统下没有原生python连接器的问题
  • 极大的降低了python程序员使用TDengine技术门槛
  • 可以方便的将数据转换到numpy与pandas

由于目前TDengine没有提供mac操作系统下的原生client, 为保证库的兼容性,目前crown库底层使用的restful接口进行连接。以后的版本中,笔者将提供在window和Linux下的原生连接器接口可供配置使用。

项目地址: https://github.com/machine-w/crown

安装

crown库像其他python第三方库一样,可以通过pip,轻松安装最新版本:

pip install crown

还可以通过git安装, 使用方法:

git clone https://github.com/machine-w/crown.git
cd crowm
python setup.py install

简单使用

连接数据库

使用crown连接TDengine,只需要提供taos restful服务的地址、端口号、以及需要操作的数据库名。然后即可使用TdEngineDatabase类新建一个数据库对象。以下为连接数据库的例子代码:

from crown import * #导入库
DATABASENAME = 'taos_test'  #数据库名
HOST = 'localhost'
PORT = 6041 
db = TdEngineDatabase(DATABASENAME) # 默认端口 6041,默认用户名:root,默认密码:taosdata
#如不使用默认参数,可以使用下面的方法提供参数
# db = TdEngineDatabase(DATABASENAME,host=HOST,port=PORT,user='yourusername',passwd='yourpassword') 

 # 一般情况我们使用connect方法尝试连接数据库,如果当前数据库不存在,则会自动建库。
db.connect() 
# 连接数据库后,db对象后会自动获取全部数据库信息,以字典的形式保存在属性databases中。
print(db.databases) 

#当然也可以使用手动建库方法建立数据库。
db.create_database(safe=True)   #参数safe为True表示:如果库存在,则跳过建库指令。
#可选字段:建库时配置数据库参数,具体字段含义请参考tdengine文档。
# db.create_database(safe=True,keep= 100,comp=0,replica=1,quorum=2,blocks=115) 

#可以通过调用alter_database方法修改数据库参数。
db.alter_database(keep= 120,comp=1,replica=1,quorum=1,blocks=156) 

#删除当前数据库方法drop_database
db.drop_database(safe=True) #参数safe:如果库不存在,则跳过删库指令。

理论上使用crown库操作TDengine,所有的数据库操作都无需手工拼装sql语句,但是为了应对比较特殊的应用场景,crown库也提供了执行原始sql语句的功能。

#通过数据库对象的raw_sql方法直接执行sql语句,语句规则与TDengine restful接口要求一致。
res = db.raw_sql('select c1,c2 from taos_test.member1')
print(res)
print(res.head)
print(res.rowcount) 
#返回的对象为二维数据。res.head属性为数组对象,保存每一行数据的代表的列名。res.rowcount属性保存返回行数。
# res: [[1.2,2.2],[1.3,2.1],[1.5,2.0],[1.6,2.1]]
# res.head: ['c1','c2']
# res.rowcount: 4

建表删表操作

建好数据库对象后,就可以通过为model类建立子类的方式定义并新建数据库表(使用方法和python中常用的ORM库类似),新建的一个类对应数据库中的一张表,类的一个对象对应表中一条数据。以下示例创建一个简单的数据库表:

# 表模型类继承自Model类,每个模型类对应数据库中的一张表,模型类中定义的每个Field,对应表中的一列,
# 如果不明确定义主键类型字段, 会默认添加一个主键,主键名为 “ts”
class Meter1(Model):
    cur = FloatField()  #如果省略列名参数,则使用属性名作为列名
    curInt = IntegerField(db_column='c2')
    curDouble = DoubleField(db_column='c3')
    desc = BinaryField(db_column='des')
    # custom_ts = PrimaryKeyField() # 如果定义了主键列,则使用主键列名作为主键

    class Meta: # Meta子类中定义模型类的配置信息
        database = db # 指定之前建的数据库对象
        db_table = 'meter1' # 指定表名

crown支持的字段类型与TDengine字段类型的对应关系:

crown Field TDengine类型
DateTimeField TIMESTAMP
IntegerField INT
BigIntegerField BIGINT
FloatField FLOAT
DoubleField DOUBLE
BinaryField BINARY
SmallIntegerField SMALLINT
TinyIntegerField TINYINT
BooleanField BOOL
NCharField NCHAR

定义好表模型后,即可调用类方法create_table进行建表操作:

# create_table运行成功返回True,失败则raise错误
Meter1.create_table(safe=True) #safe:如果表存在,则跳过建表指令
#也可以通过数据库对象的create_table方法进行建表。
# db.create_table(Meter1,safe=True) 

# drop_table方法进行删除表操作,运行成功返回True,失败则raise错误
Meter1.drop_table(safe=True) #safe:如果表不存在,则跳过删表指令
#同样可以通过数据库对象删表,功能同上
# db.drop_table(Meter1,safe=True) 

#table_exists方法查看表是否存在,存在返回True,不存在返回:False
Meter1.table_exists() 

数据插入

可以通过新建的数据表类Meter1新建数据对象并传入具体字段的数值,然后使用对象的save方法插入数据。
也可以直接使用Meter1类的类方法insert直接插入数据。下面的例子分别演示了这两种方法:

import time
#方法一
for i in range(1,101):
    #使用模型类实例化的每个对象对应数据表中的每一行,可以通过传入属性参数的方式给每一列赋值
    m = Meter1(cur = 1/i,curInt=i,curDouble=1/i+10,desc='g1',ts= datetime.datetime.now())
    time.sleep(1)
    #使用对象的save方法将数据存入数据库
    m.save()
print(Meter1.select().count()) # 结果:100

#方法二
for i in range(1,101):
    #也可以直接使用模型类的insert方法插入数据。
    Meter1.insert(cur = 1/i,curInt=i,curDouble=1/i+10,desc='g1',ts= datetime.datetime.now() - datetime.timedelta(seconds=(102-i)))
print(Meter1.select().count()) # 结果:101

如果不传入时间属性ts,则会以当前时刻为默认值传入

Meter1.insert(cur = 1/i,curInt=i,curDouble=1/i+10,desc='g1')
m = Meter1(cur = 1/i,curInt=i,curDouble=1/i+10,desc='g1')

数据查询

crown提供了丰富的数据查询功能,由于篇幅的原因,这里只介绍笔者项目中比较常用的几种查询。了解更多的查询使用方法,请查看项目文档:https://github.com/machine-w/crown/blob/main/README.rst

单条数据查询

使用Meter1类的select()方法可以获取表的查询对象,查询对象的one方法可以获取满足条件的第一条数据。

#获取一条数据:使用select()类方法获取查询字段(参数留空表示取全部字段),然后可以链式使用one方法获取第一条数据
res = Meter1.select().one()
print(res.desc,res.curDouble,res.curInt,res.cur,res.ts)

#select函数中可以选择要读取的字段
res = Meter1.select(Meter1.cur,Meter1.desc).one()
print(res.desc,res.curDouble,res.curInt,res.cur,res.ts)

多条数据查询

使用Meter1类的select()方法可以获取表的查询对象,查询对象的all方法可以获取满足条件的全部数据。

#使用select()类方法获取查询字段(参数留空表示取全部字段),然后可以链式使用all方法获取全部数据
res_all = Meter1.select().all()
for res in res_all:
    print(res.desc,res.curDouble,res.curInt,res.cur,res.ts)

#select函数中可以选择要读取的字段
res_all = Meter1.select(Meter1.cur,Meter1.desc).all()
for res in res_all:
    print(res.desc,res.curDouble,res.curInt,res.cur,res.ts)

读取数据导入numpy和pandas

虽然TDengine提供了很多聚合和统计函数,但是把时序数据导入numpy或pandas等数据分析组件中进行处理的情况也是很常见的操作。
下面演示如何通过crown把结果数据导入numpy和pandas

#导入numpy
#通过all_raw函数可以获取二维数组格式的数据查询结果。结果每列代表的标题保存在结果对象的head属性中。
raw_results = Meter1.select(Meter1.cur,Meter1.curInt,Meter1.curDouble).all_raw()
#可以很方便的将结果转换为numpy数组对象
np_data = np.array(raw_results)
print(np_data)
print(raw_results.head)

#导入pandas
raw_results = Meter1.select().all_raw()
#使用以下方法,可以轻松的将数据导入pandas,并且使用时间点作为index,使用返回的数据标题作为列名。
pd_data = pd.DataFrame(raw_results,columns=raw_results.head).set_index('ts')
print(pd_data)

选择列四则运算

#使用select()类方法获取查询字段时,可以返回某列或多列间的值加、减、乘、除、取余计算结果(+ - * / %)
res_all = Meter1.select((Meter1.curDouble+Meter1.cur),Meter1.ts).all()
for res in res_all:
    #返回的结果对象可以用get方法获取原始计算式结果
    print(res.get(Meter1.curDouble+Meter1.cur),res.ts) 

#字段别名
#给运算式起别名(不仅运算式,其他放在select函数中的任何属性都可以使用别名)
res_all = Meter1.select(((Meter1.curDouble+Meter1.cur)*Meter1.curDouble).alias('new_name'),Meter1.ts).all() 
for res in res_all:
    #使用别名获取运算结果
    print(res.new_name,res.ts) 

where函数

#可以在select函数后链式调用where函数进行条件限
one_time =datetime.datetime.now() - datetime.timedelta(hours=10)
ress = Meter1.select().where(Meter1.ts > one_time).all()
#限定条件可以使用 > < == >= <= != and or ! 等。字符类型的字段可以使用 % 作为模糊查询(相当于like)
ress = Meter1.select().where(Meter1.cur > 0 or Meter1.desc % 'g%').all()
#where函数可以接收任意多参数,每个参数为一个限定条件,参数条件之间为"与"的关系。
ress = Meter1.select().where(Meter1.cur > 0, Meter1.ts > one_time, Meter1.desc % '%1').all()

分页与limit

#可以在select函数后链式调用paginate函数进行分页操作,以下例子为取第6页 每页5条数据。
ress_1 = Meter1.select().paginate(6,page_size=5).all()
ress_2 = Meter1.select().paginate(6).all() #默认page_size为20
#可以在select函数后链式调用limit函数和offset函数条数限制和定位操作。
ress_3 = Meter1.select().limit(2).offset(5).all()
ress_4 = Meter1.select().limit(2).all()

排序

目前tdengine只支持主键排序

#可以在select函数后链式调用desc或者asc函数进行时间轴的正序或者倒序查询
res = Meter1.select().desc().one()

聚合函数

TDengine提供了许多聚合函数可供使用,可以直接返回聚合结果,极大的提高数据聚合效率。crown几乎完全兼容TDengine全部的聚合函数,只需要调用对应的方法即可使用。以下是使用的例子:

#count
count = Meter1.select().count() #统计行数
print(count) # 结果: 100
count = Meter1.select().count(Meter1.desc) #统计指定列非空行数
print(count) # 结果: 90

#avg(sum,stddev,min,max,first,last,last_row,spread使用方法与avg相同)
avg1 = Meter1.select().avg(Meter1.cur,Meter1.curDouble.alias('aa')) #可以同时获取多列,并且可以使用别名
print(avg1.get(Meter1.cur.avg()),avg1.aa) #打印统计结果

#twa 必须配合where函数,且必须选择时间段
twa1 = Meter1.select().where(Meter1.ts > datetime.datetime(2020, 11, 19, 15, 9, 12, 946118),Meter1.ts < datetime.datetime.now()).twa(Meter1.cur,Meter1.curDouble.alias('aa'))
print(twa1.get(Meter1.cur.twa()),avg1.aa) #打印统计结果

#diff
diffs = Meter1.select().diff(Meter1.curInt.alias('aa')) #diff目前只可以聚合一个属性。
for diff1 in diffs:
    print(diff1.aa,diff1.ts) # 时间点数据同时返回

#top(bottom函数使用方式相同)
# top函数需要提供要统计的属性,行数,以及别名
tops = Meter1.select().top(Meter1.cur,3,alias='aa') 
for top1 in tops:
    print(top1.aa,top1.ts) # 时间点数据同时返回
tops = Meter1.select().top(Meter1.cur,3) # 可以不指定别名
for top1 in tops:
    #不指定别名,需用使用get方法获取属性
    print(top1.get(Meter1.cur.top(3))) 

#percentile (apercentile函数使用方式相同) 
#每个属性参数为一个元组(数组),分别定义要统计的属性,P值(P值取值范围0≤P≤100),可选别名。
percentile1 = Meter1.select().percentile((Meter1.cur,1,'aa'),(Meter1.curDouble,2)) 
print(percentile1.aa)
#不指定别名,需用使用get方法获取属性
print(percentile1.get(Meter1.curDouble.percentile(2)))

#leastsquares
#每个属性参数为一个元组(数组),分别定义要统计的属性,start_val(自变量初始值),step_val(自变量的步长值),可选别名。
leastsquares1 = Meter1.select().leastsquares((Meter1.cur,1,1,'aa'),(Meter1.curDouble,2,2)) 
print(leastsquares1.aa) # 结果: {slop:-0.001595, intercept:0.212111}
#不指定别名,需用使用get方法获取属性
print(leastsquares1.get(Meter1.curDouble.leastsquares(2,2))) 

注意:当前版本并不支持多表join查询,需要多表查询的情况请使用raw_sql函数,执行原始sql语句。后期版本会补充join功能。

超级表定义

超级表定义与普通表的区别在于继承自SuperModel。而且,在Meta类中,可以定义标签。
超级表与普通表的查询操作使用方式相同,以上介绍的所有方法也可以在超级表类中使用,查询操作时标签字段也可以当作普通字段一样操作。

# 超级表模型类继承自SuperModel类
class Meters(SuperModel):
    cur = FloatField(db_column='c1')
    curInt = IntegerField(db_column='c2')
    curDouble = DoubleField(db_column='c3')
    desc = BinaryField(db_column='des')
    class Meta:
        database = db
        db_table = 'meters'
        # Meta类中定义的Field,为超级表的标签
        location = BinaryField(max_length=30)
        groupid = IntegerField(db_column='gid')

#建立超级表
Meters.create_table(safe=True) 
#删除超级表
Meters.drop_table(safe=True) 
#查看超级表是否存在
Meters.supertable_exists() 

从超级表建立子表

对于数据插入操作,就需要从超级表中建立子表。可以使用Meters类的create_son_table方法创建子表。该方法返回一个子表对应的类对象。该对象可以和以上介绍的普通表类对象(Meter1)一样使用。

 #生成字表模型类的同时,自动在数据库中建表。
SonTable_d3 = Meters.create_son_table('d3',location='beijing',groupid=3)

# SonTable_d3的使用方法和继承自Modle类的模型类一样。可以进行插入与查询操作
SonTable_d3.table_exists() 

#子表中插入数据
m = SonTable_d3(cur = 65.8,curInt=10,curDouble=1.1,desc='g1',ts = datetime.datetime.now())
m.save()

上面介绍了TDengine的python ORM连接库crown的基本安装和使用方法。除了上面介绍的内容,crown还提供了很多非常实用的功能,比如动态建表、根据表名获取模型类、分组查询等。有兴趣的读者可以前往github上的项目地址(https://github.com/machine-w/crown)查看使用文档。也欢迎在github上多提宝贵意见与通报bug。笔者将持续维护该项目,努力提供更加丰富的功能和更加完备的文档供大家使用。

结语

TDengine作为优秀的国产开源软件,拥有优雅的软件设计和出色的性能表现。非常适合在物联网大数据应用场景下作为数据基础平台使用。未来随着物联网行业的蓬勃发展,TDengine也必将成为物联网大数据基础架构的一部分而备受全世界相关领域从业者的广泛关注。笔者作为一个物联网行业的一名普通开发人员,非常荣幸有机会开发和维护这样一个TDengine周边的开源小项目。希望通过这个项目可以让更多的开发人员可以更加方便便捷的使用TDengine,提高工作效率。也希望能够起到抛砖引玉的作用,鼓励更多的开发者加入到开源项目开发中来,大家一起来为丰富TDengine的周边生态做贡献。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容