周边生态贡献者+1,一个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接口进行连接。以后的版本中,笔者将提供在Windows和Linux下的原生连接器接口可供配置使用。

项目地址:

https://github.com/machine-w/crown

安装

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

pip install crown

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

git clone https://github.com/machine-w/crown.gitcd crowmpython setup.py install

简单使用

1.连接数据库

使用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

2.建表删表操作

建好数据库对象后,就可以通过为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字段类型的对应关系:

定义好表模型后,即可调用类方法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()

3.数据插入

可以通过新建的数据表类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')

4.数据查询

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()

以下是使用的例子。

#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功能。

5.超级表定义

超级表定义与普通表的区别在于继承自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()

6.从超级表建立子表

对于数据插入操作,就需要从超级表中建立子表。可以使用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上查看使用文档。也欢迎在GitHub上多提宝贵意见与通报bug。笔者将持续维护该项目,努力提供更加丰富的功能和更加完备的文档供大家使用。

结语

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


如果你有好的idea想和TDengine一起实现,欢迎成为Contributor Family的一员。点击下方链接,加入我们!

https://www.taosdata.com/cn/contributor/

开源、高性能、云原生、极简的时序数据处理平台

87 声望
27 粉丝
0 条评论
推荐阅读
从 1000+ 参赛项目突围!涛思数据荣获 ITEC 2022 全球创业赛成长组二等奖
3 月 25 日,第十届朝阳国际人才创业大会(ITEC)创新峰会在京举办。本届大会由朝阳海外人才创业大会(OTEC)全新升级为朝阳国际人才创业大会(ITEC),服务范围从海外人才拓展至国际人才,功能从支持项目落地提...

TDengine涛思数据

封面图
数据结构与算法:二分查找
一、常见数据结构简单数据结构(必须理解和掌握)有序数据结构:栈、队列、链表。有序数据结构省空间(储存空间小)无序数据结构:集合、字典、散列表,无序数据结构省时间(读取时间快)复杂数据结构树、 堆图二...

白鲸鱼9阅读 5.3k

3 分钟上手,不用再找 Chatgpt 资源了,这里全都有
最近无论是打开社交网站,还是朋友圈,就连中午吃个饭都能听到大家都在聊 Chatgpt,仿佛如果这一刻你不懂这是个啥玩意儿,你就会觉得自己完全搭不上他们的话...

Postcat4阅读 767评论 5

封面图
滚蛋吧,正则表达式!
你是不是也有这样的操作,比如你需要使用「电子邮箱正则表达式」,首先想到的就是直接百度上搜索一个,然后采用 CV 大法神奇地接入到你的代码中?

良许3阅读 1.5k

搭个ChatGPT算法模型,从哪开始?
最近 ChatGPT 很火,火到了各行各业。记得去年更多的还是码农最新体验后拿它搜代码,现在各行各业都进来体验,问它咋理财、怎么写报告和给小孩起名。😂 也因此让小傅哥在头条的一篇关于 ChatGPT 的文章都有了26万...

小傅哥6阅读 1.2k

封面图
程序员适合创业吗?
大家好,我是良许。从去年 12 月开始,我已经在视频号、抖音等主流视频平台上连续更新视频到现在,并得到了不错的评价。每个视频都花了很多时间精力用心制作,欢迎大家关注哦~考虑到有些小伙伴没有看过我的视频,...

良许3阅读 1.3k

Ubuntu20.04 从源代码编译安装 python3.10
Ubuntu 22.04 Release DateUbuntu 22.04 Jammy Jellyfish is scheduled for release on April 21, 2022If you’re ready to use Ubuntu 22.04 Jammy Jellyfish, you can either upgrade your current Ubuntu syste...

ponponon1阅读 4.6k评论 1

开源、高性能、云原生、极简的时序数据处理平台

87 声望
27 粉丝
宣传栏