本文介紹了一個用於操做TDengine的 Python ORM庫。本文的預期讀者是,須要使用Python語言操做TDengine數據庫的開發人員。python
項目地址:
https://github.com/machine-w/crowngit
ORM就是對象關係映射(Object Relational Mapping),是一種程序設計技術,用於實現面向對象編程語言裏不一樣類型系統的數據之間的轉換。從效果上說,它實際上是建立了一個可在編程語言裏使用的「虛擬對象數據庫」。簡單說來就是,經過創建類與數據庫表,對象與數據庫數據條目的對應關係。從而能夠經過編程語言的數據類型操做數據庫。程序員
做爲一個使用Python做爲主力編程語言的開發者,筆者常常要編寫操做各類數據庫的代碼。對於鍵值對類型(如:Redis)或者文檔類型(如:MongoDB)的數據庫,Python生態都提供了很好的第三方鏈接庫。而對於最常使用的關係型數據( 如:MySQL、PostgreSQL),Python則提供了SQLAlchemy、Peewee等ORM第三方庫的解決方案。因此,筆者平常工做中,須要手工拼接SQL查詢語句的場景很是少 ( 甚至慢慢忘記了這項技能)。github
近來,筆者須要帶領團隊完成一個智能電力系統的項目。在技術選型過程當中發現了優秀的物聯網大數據平臺TDengine 。通過測試和評估發現,不管從超高性能和穩定性、仍是簡潔的設計、開源的理念。TDengine都很是適合做爲智能電力系統的基礎平臺使用。可是,在使用過程當中,咱們發現了一個比較棘手的問題。那就是:因爲 TDengine 誕生不久,相比較其餘已經發展不少年的其餘數據庫平臺,周邊的相關生態軟件還略少一些。特別是,蘋果操做系統OS X下暫時沒有原生鏈接器可用,寫好的程序須要拿到Linux上去調試。這對於被「寵壞」的python程序員來說真得無法適應。並且考慮到筆者團隊中其餘程序員都習慣了ORM的操做方式,對原始SQL並不熟悉。因此,筆者意識到:若是使用原生的鏈接器進行開發,將會遇到不少困難。因而就開始了TDengine的開源ORM庫的開發。一方面,能夠幫助團隊更高效的完成系統開發工做。另一方面,也能夠爲幫助TDengine更好的完善生態工具鏈。sql
因爲目前TDengine沒有提供Mac操做系統下的原生client, 爲保證庫的兼容性,目前crown庫底層使用的RESTful接口進行鏈接。之後的版本中,筆者將提供在Windows和Linux下的原生鏈接器接口可供配置使用。
項目地址:數據庫
https://github.com/machine-w/crown編程
crown庫像其餘Python第三方庫同樣,能夠經過pip,輕鬆安裝最新版本:數組
pip install crown
還能夠經過git安裝,使用方法:restful
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的一員。點擊下方連接,加入咱們!