扭矩達230牛米!這輛前衛車型有思域、卡羅拉強嗎?

而懸架具有彈性,對於大小震動都做出了不錯的過濾。只是懸挂行程比起兩廂的標緻308S更長,所以過彎時的車身擺動稍大些,不過支撐性已經不錯了。油耗表現如何。1。2T自動擋車型的車主口碑油耗:7。5L/100km。1。2T發動機的排量小,而且變速箱在D擋時的經濟性不錯。

最近有很多粉絲提議我們多聊一下靈活好開的家用轎車,這讓編者想起前段時間試駕過朋友的標緻308 1.2T車型。

標緻308 1.2T車型售價:10.97-13.57萬

308的外觀設計是標緻最新設計語言的縮影,這個設計年輕、時尚,車尾的線條流暢,比較大氣。兩個尾燈之間還採用了黑色裝飾板來點綴,讓它給人一種過目不忘的感覺。

它靈活好開,有駕駛樂趣的同時也給乘客不錯的舒適性,下面我們就來聊聊編者對這款車的感受!

動力總成採用了新穎的1.2T(三缸)渦輪增壓發動機,最大功率136馬力、峰值扭矩還達到了230牛米/1750-3500轉!動力參數漂亮,而編者的朋友買這款車也是衝著這發動機去的。

傳動方面搭配的是6擋手自一體變速箱,而同級別的日系對手,如卡羅拉1.2T車型採用的則是CVT變速箱。

上到車內,前衛的內飾讓年輕人覺得它前衛和很有設計感。尺寸很小的方向盤充滿玩味!

但問題是,儀錶盤的位置設計在中控的頂部,容易出現方向盤頂端擋住部分儀錶盤的情況。這與方向盤的高度調節和駕駛人的身高有關,所以在駕駛前盡量調好方向盤的位置。

駕駛起來,在D擋模式下,它的調校側重於降低油耗,所以總是积極升擋、降擋出現一點點“猶豫”的情況,在市區中代步動力夠用也平順。

而打開運動模式,變速箱的降擋直接乾脆!發動機在1900轉以後的動力爆發比較猛,提速能力強,而且在時速超過100km/h后,再加速能力也不錯,這套動力總成實際的表現已經接近1.8L的自然吸氣發動機了,所以在動力方面不用擔心。

小尺寸方向盤握感出眾,指向性精準,所以轉動起來充滿着玩味。而懸架具有彈性,對於大小震動都做出了不錯的過濾!只是懸挂行程比起兩廂的標緻308S更長,所以過彎時的車身擺動稍大些,不過支撐性已經不錯了!

油耗表現如何?

1.2T自動擋車型的車主口碑油耗:7.5L/100km。

1.2T發動機的排量小,而且變速箱在D擋時的經濟性不錯!所以油耗是較低的。

競爭對手

東風本田-思域

1.0T車型售價:11.59-12.79萬

在駕駛體驗上,標緻308 1.2T車型表現更好、也更有樂趣,而思域的空間表現更佳,不過購買思域的買家大多會選擇1.5T車型,所以兩者在進行着差異化競爭!

一汽豐田-卡羅拉

1.2T車型售價:10.98-14.38萬

卡羅拉採用的1.2T發動機最大功率為116馬力,數據沒有標緻308的漂亮,但是卡羅拉普遍優惠較大、車型選擇豐富,所以性價比較高些。

最後總結:

其實標緻308在駕駛樂趣和舒適性方面達到了不錯的平衡,如果你想要一款開起來有樂趣、不乏味的家用車,它是不錯的選擇。而它的優惠沒有對手車型多,所以性價比不算太高。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

要大,要硬,要安全,才是一輛好車

總結:一直以來,物流用途的商用車輛一味地追求大空間和低排放,注重燃油經濟性和裝載能力的提高卻忽視了一輛汽車最應該擁有的安全性能,而不斷在設計、品質、智能、安全方面超越自身的新全順卻從未被對手超越,擁有好品牌、大空間、高安全的福特新全順,是真正意義上的商用車佼佼者,也是創業者和物流運輸業的絕佳選擇。
老陳是物流公司創始人
因為業務擴本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

PL/SQL編程急速上手

  結構化查詢語言(SQL)是第四代編程語言的典型,這種命令式的語言更像一種指令,使用它,你只需要告訴計算機“做什麼”,而不用告訴計算機“怎麼做”。第四代編程語言普遍具有簡單、易學、能更快的投入生產等優點,但也失去了部分第三代編程語言(C,C++,Java等)的靈活性。PL/SQL 在 SQL 的基礎上,保留了部分第三代編程語言特性,它主要運行在 Oracle 數據庫上,它同時兼備了第四代語言簡單、易用的特點,又保留了高級程序設計語言的靈活性與完整性,這使得開發人員可以只使用 PL/SQL 就能進行複雜業務邏輯的編寫。

 

一  PL/SQL 簡介

 

  1,簡介

  標準 SQL 提供了定義和操縱數據庫對象的能力,但與傳統高級編程語言相比,由於其具有更高的抽象性,所以註定缺乏諸多高級編程語言的特性,比如封裝函數、流程控制、進行錯誤檢測和處理等。

  PL/SQL 是 Oracle 在標準 SQL 的基礎上進行功能擴充后的一門編程語言,這使它保留了部分第三代編程語言的部分特性,比如變量聲明、流程控制、錯誤處理等。

  PL/SQL 的全稱是 Procedural Language/SQL,即過程化結構查詢語言,正如其名所示,PL/SQL 增加了過程性語言中的結構,以對標準 SQL 進行擴充。在PL/SQL 中,最基本的程序單元是語句塊(block),所有的程序都應該由各種塊構成,塊與塊之間可以相互嵌套。在塊中,可以定義變量,執行條件判斷,循環等。

 

  2,開發工具

  Oracle 官方提供了兩款開發工具:SQL*Plus 和 Oracle SQL Developer。前者是一款命令行開發工具,後者則擁有方便的圖形化操作界面(類似SQL Server 的 SSMS)。

  除了官方提供的兩款工具外,PL/SQL Develpoer 是一款由第三方公司開發的,非常流行的 Oracle 數據庫集成開發環境。除此之外,市面上還有很多其他工具也具備 Oracle 數據庫開發的能力,大家可以根據需要選擇合適的開發工具。

  3,基本概念

  如果你對 SQL Server 有一定了解,你應該知道,它裏面可以創建很多的數據庫,用於存儲不同業務或系統的數據,每個數據庫都有單獨的數據文件(物理磁盤存儲的)。Oracle 中有點不同,嚴格來說 Oracle 通常只有一個數據庫,然後通過表空間和方案來實現多業務的數據分離。

  Oracle 通過表空間來存儲不同的內容,表空間是數據庫的邏輯劃分,每個數據庫至少有一個表空間(SYSTEM 表空間)。為了便於管理和提高運行效率,可以使用一些附加表空間來劃分用戶和應用程序。例如:USER 表空間供一般用戶使用,RBS 表空間供回滾段使用,一個表空間只能屬於一個數據庫。

  方案是另一個 Oracle 中特有的概念,方案是比表空間更細粒度的另一種單元,方案用於存放用戶相關的信息。通常每個用戶都對應一個方案,並且名稱一樣。多個用戶可以共用一個表空間,但不能通用一個方案,正是由於這種方案-用戶一一對應的關係,所以我們通常也把方案理解成用戶權限所能及的對象的集合。

 

二  PL/SQL基礎

 

  1,SQL 與 PL/SQL

  前面提到,PL/SQL 是對標準 SQL 的擴展,所以,在 PL/SQL 中不僅可以執行 SQL 語句,還支持很多增強的特性,比如在 SQL 語句中使用變量、使用 PL/SQL 定義的函數等。在 PL/SQL 語句塊中,可以使用 SQL 語句操作數據庫,它支持所有的 SQL 數據操作、游標和事務處理命令,支持所有的 SQL 函數、操作符,完全支持 SQL 數據類型。

  需要注意的是:在 PL/SQL 語句塊中,不能直接使用 DDL 語句,這是因為 PL/SQL 引擎在編譯時會檢測語句塊中所涉及的對象,如果其不存在,通常都會引發錯誤,導致 DDL 語句執行失敗。

  為了解決這類綁定性錯誤,可以使用動態SQL,即把需要執行的 DDL 操作存儲在字符串中,並通過 execute immediate 來執行這個字符串,從而達到間接執行 DDL 操作的目的。

  

  2,數據定義

  數據管理主要使用 DDL 數據定義語言:create、alter、drop。

  創建表和約束:

 1 --在列后添加約束
 2 create table table_name  3 (  4     col1 type constraint,  5  ...  6 )  7 --單獨添加約束
 8 create table table_name  9 ( 10  col1 type, 11  ...., 12  constration cons_name cons_type 13 ) 14 --在Oracle中創建表和約束與標準SQL相同

  創建索引和視圖:

 1 --創建索引(非唯一)
 2 --默認系統會在具有unique和primary key的列上創建唯一約束
 3 create index index_name on (col1...);  4 --當提供多個列時,即創建複合索引
 5 --創建視圖
 6 create or replace view view_name  7 as
 8 select ...;  9 --創建,如果已存在則修改視圖
10 create view ...
11 as
12 ... 13 with read only; 14 --創建只讀的視圖(推薦)

  修改表或視圖:

1 --為表增加新的列
2 alter table table_name 3 add col_name type constration; 4 --移除表中已有的列
5 alter table table_name 6 drop column col_name;

  刪除數據庫對象:

1 --刪除表
2 drop table table_name; 3 --刪除視圖
4 drop view view_name; 5 ...

  

  3,數據查詢

  A:標準查詢

    Oracle 中的數據查詢遵循 SQL 標準,常規查詢請移步我的《SQL入門,就這麼簡單》。

  B:dual 表

    dual 是 Oracle 系統中對所有用戶可用的一個實際存在的表,它不能用來存儲信息,在實際開發中只能用來執行 SELECT 語句,我們可以用它來獲取系統信息,比如獲取當前系統日期,或輸出一些測試信息。

1 --獲取系統日期
2 select sysdate from dual; 3 --轉換日期格式
4 select to_char(sysdate,'yyyy-mm-dd');
5 ...

  C: 偽列

    常用的偽列有兩個:rownum、rowid。

    在 Oracle 中沒有類似 SQL Server 中 TOP 這樣可以提取結果集前幾條記錄的關鍵字,但 Oracle 提供了一個更方便的方法,rownum 偽列。rownum 是一個動態的序號,從 1 開始,為所有查詢到的數據編號。

1 --查詢員工表中前10位員工相關信息
2 select rownum,ename,sal from emp 3 where rownum<=10; 4 -- 測試數據庫 Oracle 11g 

    使用 rownum 偽列時需要注意:rownum 是在基礎查詢之後動態添加上去的序號,所以,如果你想通過一條查詢語句實現提取結果集中間的部分記錄是不能成功的,必須使用子查詢,把 rownum 當做普通列才能實現。

1 select row_num,empno,ename,sal from ( 2     select rownum as row_num,empno,ename,sal from emp 3 )a 4 where row_num >5 and row_num <=10; 5 -- 別名是為了防止服務器把外層的rownum再次當做偽列

    同理,提取使用 order by 排序后的記錄,也需要使用子查詢。

    和 rownum 不同,rowid 偽列是和表中的數據一樣實際存在的列,它是一種數據類型,是基於 64 位編碼的 18 個字符,用來唯一的表示一條記錄物理位置的一個id。我們可以通過 rowidtochar 函數把它轉換成字符串進行显示,還可以通過它來刪除表中重複的記錄。

1 --查看rowid
2 select rowidtochar(rowid) ename,sal from emp; 3 --基於rowid刪除表中形同的記錄
4 delete from emp 5 where rowid not in ( 6     select min(rowid) from emp group by empno 7 );

  

  4,數據操縱

  數據操縱主要包含以下操作:insert、update、delete、merge。

  A:insert 插入

 1 --方式一
 2 insert into table_name(column list)--如果不提供字段列表,下面的值列表需要提供每個字段的值,即使可以為空或有默認值
 3 values 
 4 (value list),  5 (value list),  6 ....  7 --方式二
 8 insert into table_name  9 select ... 10 --從其他查詢獲取數據,並插入表,數據必須符合表的約束

  B:update 更新

1 --方式一
2 update table_name 3 set col=newValue 4 where ...--如果不提供過濾條件,則更新表中所有的列
5 --方式二
6 update table_name 7 set (column list)=
8 (select ...) 9 --通過子查詢更新表,如果只更新一列,則可以省略column list 的括號,需要注意子查詢的字段順序需要和更新的字段順序一致

  C:delete

1 --方式一
2 delete from table_name 3 where ...--如果不提供過濾條件,則會刪除所有記錄

  

  5,序列

  Oracle 中沒有 SQL Server 中 identity() 標識函數,也沒有 MySQL 中 auto_increnent 這樣的選項來實現自增的列。但 Oracle 提供了更有用的“序列”。類似一個封裝好的函數,每次執行會返回一個按指定步長增長或減小的数字。常用來為表設置自增的主鍵。

1 create sequence seq_name 2 increment by n --自增的步長,(省略該選項則)默認為1,負數表示遞減
3 start with n --序列的初始值,默認為1
4 max value n | nomaxvalue --指定最大值或沒有最大值(無限增長)
5 min value n  | nominvalue --指定最小值或沒有最小值(無限減小)
6 cycle | nocycle --規定設置的序列到達最大或最小時是否從開頭循環
7 cache n | nocache --規定是否在內存中緩存序列值,以改善性能

  通常情況下,我們只需要指定初始值,最大值和循環三項,即可創建一個序列。

1 create sequence my_seq 2 start with 1
3 nomaxvalue 4 nocycle;

  序列也是 Oracle 數據庫對象之一,序列有兩個常用的屬性:nextval、currval。

1 select my_seq.nextval from dual;--獲取下一個序列值
2 select my_seq.currval from dual;--查看當前序列值
3 --在插入數據是使用序列
4 insert into table_name 5 values
6 (my_seq.nextval,...) 7 --使用循環批量插入時非常方便

  我們可以為每個表創建單獨的序列,從而為每個表提供沒有間隙(無刪除數據或回滾等操作干擾)的自增字段作為主鍵。

  修改和刪除序列:

1 alter sequence seq_name 2 ... 3 --為了保證主鍵的變化有相同的規律可循,一般不建議修改已創建的序列
4 drop sequence seq_name

 

三  Oracle 內置函數

 

  1,字符串函數

 1 --把二進制轉換成字符
 2 select CHR(0101) from dual;  3 --連接字符串
 4 select concat(111,'aaa') from dual;  5 select 111 || 'aaa' from dual;  6 --首字母大寫
 7 select INITCAP('char') from dual;  8 --全大/小寫轉換
 9 select lower('ABC'),upper('abc') from dual; 10 --左/右填充
11 select lpad('aa',5,'*'),rpad('aa',5,'*') from dual; 12 --刪除字符串左/右指定字符(第二個參數中包含的字符都會被刪除)
13 select ltrim('aaa123aaa','1a'),rtrim('aa123aa','a') from dual; 14 --刪除左右空格
15 select trim(' aaa ') from dual; 16 --從左邊開始刪除指定字符(單個),可選參數還包括:trailing(從右邊開始),both(兩邊一起)
17 select trim(leading 'a' from 'aa123aa') from dual; 18 --從指定位置開始截取指定長度的字符串
19 select substr('abcdefg',2,3) from dual; 20 --字符替換(第二個參數中包含的字符都會被替換)
21 select translate('11aa22aa11', 'a2', 'bb') from dual; 22 --替換 NULL 值
23 select nvl(NULL,'aha') from dual;

  

  2,數學函數

 1 --絕對值
 2 select abs(-123) from dual;  3 --向上取整
 4 select ceil(1.2),ceil(-1.2) from dual;  5 --向下取整
 6 select floor(1.8),floor(-1.8) from dual;  7 --返回自然常數 e 的 n 次方
 8 select exp(5) from dual;  9 --返回以第一個參數為底的第二個參數的對數
10 select log(3,10) from dual; 11 --求模,如果第二個參數為0,則返回第一個參數
12 select mod(10,3) from dual; 13 --返回第一個參數的第二個參數次方
14 select power(23) from dual; 15 --保留指定小數位,最後一位小數四舍五入得來
16 select round(1.2345,3) from dual; 17 --保留指定小數位,其餘直接截斷
18 select trunc(1.2345,3) from dual; 19 
20 --格式化数字(格式位數應該與数字位數相同)
21 
22 --用0格式化時,如果数字位數不夠,結果會用0補齊位數
23 select to_char(123456789000,'000,000,000,000,000') from dual; 24 --用9格式化時,如果数字位數不夠,結果會用空格補齊位數
25 select to_char(123456789000,'999,999,999,999,999') from dual; 26 --使用fm格式化小數
27 select to_char(123456.258,'fm999,999,999.99') from dual; 28 --使用 $(美元) 或 L(當地) 添加貨幣符號
29 select to_char(123.456,'L999.999') from dual; 30 /* 注意貨幣符號和小數不能一起使用 */

 

  3,時間和日期函數

 1 --返回操作系統日期
 2 select sysdate from dual;  3 --返回日期部分
 4 select current_date from dual;  5 --返回日期+時間
 6 select current_timestamp from dual;  7 --返回操作系統日期—+時間(包含時區信息)
 8 select systimestamp from dual;  9 --按格式化日期為字符串
10 select to_char(sysdate,'YYYY-MM-DD HH:MM:SS') from dual; 11 --把字符串表示的日期轉換成日期類型的值返回(前後格式需保持一致)
12 select to_date('2020-05-28 17:02:00','YYYY-MM-DD HH24:MI:SS') from dual; 13 --把字符串表示的日期轉換成日期 + 時間類型的值返回(前後格式需保持一致)
14 select to_timestamp('2020-05-28 17:02:00','YYYY-MM-DD HH24:MI:SS') from dual; 15 --返回指定日期後幾個月的日期
16 select add_months(sysdate,1) from dual; 17 --返回兩個日期間間隔月數(注意正負)
18 select months_between(sysdate,to_date('2020-07-01','YYYY-MM-DD')) from dual; 19 --把日期按指定精度截斷,可選參數有yyyy(精確到年,返回當年的第一天的日期),mm(精確到月,返回當月第一天的日期),rr(精確到日,返回當天的日期)
20 select trunc(sysdate,'mm') from dual; 21 
22 /* ----------------------日期可選格式--------------------- */
23 TO_CHAR(sysdate, 'DD-MON-YYYY HH24:MI:SS') 24 TO_CHAR(sysdate, 'DD-MON-YYYY HH12:MI:SS PM') 25 TO_CHAR(systimestamp, 'DD-MON-YYYY HH24:MI:SS.FF') 26 TO_CHAR(sysdate, 'DY, DD-MON-YYYY') 27 TO_CHAR(sysdate,'Month DDth, YYYY') 28 TO_CHAR(systimestamp, 'DD-MON-YYYY HH24:MI:SS TZH:TZM') 29 TO_CHAR(sysdate, 'MM/DD/YYYY HH24:MI:SS') 30 TO_CHAR(sysdate, 'MM/DD/YY HH24:MI:SS') 31 TO_CHAR(sysdate, 'MM/DD/RRRR HH12:MI:SS PM') 32 TO_CHAR(sysdate, 'MM/DD/RR HH12:MI:SS PM')

 

  4,聚合函數

 1 --計算行數(不計算空值)
 2 select count(*) from emp;--根據所有列計算
 3 select count(comm) from emp;--根據某一列計算(注意該列是否有空值)
 4 select count(distinct deptno) from emp;--計算deptno中不同值的個數
 5 --計算列的最大/小值
 6 select max(sal),min(sal) from emp;  7 --返回中間值
 8 select median(sal) from emp;  9 --返回標準差
10 select stddev(sal) from emp; 11 --求和
12 select sum(sal) from emp; 13 --計算方差
14 select variance(sal) from emp; 15 --偽列 rownum,每條數據的序號
16 select rownum,empno,ename,sal from emp;

 

四  變量和類型

 

  1,PL/SQL 基礎

  如果想通過 PL/SQL 程序輸出內容,需要先執行以下命令,以打開輸出功能,否則即使 PL/SQL 程序正常執行,也不會有任何信息輸出。

1 set serveroutput on--可以不需要語句結束標記';',這是開發工具的命令
2 dbms_output.enable;--這是 Pl/SQL 提供的

  PL/SQL 程序由不同的 block(程序塊)組成,塊是 PL/SQL 程序的基本組成單位,塊又可以分為匿名塊和命名塊。

  一個完整的 PL/SQL 程序一般包含 3 部分:declare(聲明),execution code(執行代碼,即業務邏輯代碼),exception(異常處理),聲明和異常處理不是必須的。

1 declare
2 --... 包括變量、游標等
3 begin
4 --... 業務代碼
5 exception 6 --... 異常處理
7 end;

  讓我們來看一個最簡單的 PL/SQL 程序:

1 --注意,PL/SQL業務代碼必須運行在 begin...end 中
2 begin
3 dbms_output.put_line('hello world'); 4 end; 5 --沒有聲明和異常部分

  塊與塊之間可以相互嵌套,PL/SQL 中程序塊可以限制變量的作用域(變量的作用域問題稍後的章節將會詳細講解),另外,使用<<name>>為塊命名可以讓整個程序可讀性更好:

1 <<outer>>--oracle 11g 不允許給最外層塊命名
2 begin
3 dbms_output.put_line('outer block'); 4   <<inner>>
5   begin
6   dbms_output.put_line('inner block'); 7   end; 8 end;

  

  2,變量

  PL/SQL 中的變量在 declare 區域聲明,不需要額外的標識符,只需要提供變量名和值類型即可。

1 declare
2   v_name emp.ename%type;--通過動態獲取表中列的數據類型,來確定變量的數據類型
3   v_job  varchar(50);--直接指定具體的數據類型
4 begin
5   v_name:='&name';--通過:=為變量賦值
6 end;

  &name,這種形式是 SQL Developer 工具提供的一種變量形式:替換變量,在執行程序時,你可以手動指定變量的值,提升程序的交互性,測試程序時非常有用。需要注意的是,它並不是 PL/SQL 提供的功能,當使用 & 標識變量時,每次執行該程序都需要提供值,如果使用 && 標識,則只需要在第一次執行時提供,後續執行都默認為第一次提供的值。

  給變量賦值除了通過 := 的方式,還可以使用 select…into 的方式,直接從查詢中獲取值並賦給變量。

1 declare
2     v_job emp.job%type; 3 begin
4     select job into v_job from emp where ename=v_name;--通過select...into 為變量賦值
5     dbms_output.put_line(v_job);--輸出變量值
6 end;

  

  3,記錄類型

  當有多個邏輯相關的變量需要聲明時,我們可以使用記錄類型來封裝他們,封裝好這個東西就是記錄類型(record)。

 1 declare
 2   type emp_record is record(--這裏相當於定義了一種新的數據類型,類型名稱是emp_record,和varcahr,int等類型一樣
 3   v_name emp.ename%type,  4   v_job emp.job%type,  5   v_sal emp.sal%type  6  );  7   --記錄類型類似其他編程語言中的類
 8   v_emp_record emp_record;--聲明一個emp_record類型的變量,相當於創建一個類的實例
 9 begin
10   select ename,job,sal into v_emp_record from emp where ename='ALLEN';--注意查詢的順序必須和記錄類型中定義的順序一致
11   dbms_output.put_line(v_emp_record.v_name||' '||v_emp_record.v_job||' '||v_emp_record.v_sal); 12   --通過實例訪問相關屬性
13 end;

  %rowtype:

1 declare
2   v_emp_record emp%rowtype;--聲明一個包含指定表中所有列的rowtype變量,使用上和記錄類型完全一致,但它本質上並不是記錄類型
3 begin
4   select * into v_emp_record from emp where ename='ALLEN';--把所有的列都查詢出來賦值給該變量
5   dbms_output.put_line(v_emp_record.ename||' '||v_emp_record.sal); 6   --該變量中的屬性和表的列名完全一致,可以根據需要,只使用部分數據
7 end;

  

  4,集合

  集合類似其他編程語言中的數組,也可以通過下標來訪問數據。

  如果把它和記錄類型、變量相比教,你會發現,標量標量是用來處理單行單列數據的,記錄類型適合處理單行多列的數據,而集合則是用來處理單列多行數據的。

  Oracle 提供了三種類型的集合:索引表(又稱關聯數組)、嵌套表、可變長度數組。

  索引表可以通過数字或字符串來作為下標存儲數據,下標可以不連續,索引表的容量即是数字的最大值,但它只能存儲在內存中。

1 declare
2   type idx_table is table of varchar(20) index by pls_integer;--創建索引表類型
3   -- index by 后可選的參數有pls_integer、binary_integer、varcahr(size)和使用%type 指定的varchar2類型
4   v_idx_table idx_table;--聲明索引表類型的變量
5 begin
6   v_idx_table(1):='hello';--插入值
7   v_idx_table(2):='world'; 8   dbms_output.put_line(v_idx_table(1)||' '||v_idx_table(2)); 9 end;

  嵌套表只能使用数字作為下標,数字必須是有序的,嵌套表的容量沒有限制,可以保存到數據庫中。

 1 declare 
 2   type nest_table is table of varchar(20);--創建嵌套表類型
 3   v_nest_table nest_table:=nest_table('x');--聲明嵌套表類型的變量並初始化
 4   --未初始化的嵌套表類型實際上是一個null,如果試圖為其賦值會報錯。初始化就是調用一個和創建的嵌套表類型同名的函數,
 5   --函數的參數值類型需要和嵌套表類型定義的存儲值類型(of 后的類型)相同,並且參數的個數默認就是這個嵌套表類型變量的初始容量
 6 begin
 7   v_nest_table.extend(5);--擴充嵌套表類型變量的容量
 8   --如果要增加嵌套表的容量,需要調用extend方法(該方法將在稍後詳細說明)
 9   v_nest_table(1):='hello';--插入值
10   v_nest_table(2):='world'; 11   dbms_output.put_line(v_nest_table(1)||' '||v_nest_table(2)); 12   dbms_output.put_line(nvl(v_nest_table(3),'it is null'));--沒被使用的位置為null
13 end;

  可變長度數組和嵌套表類似,都只能使用有序的数字作為下標,可變數組在定義時必須指定容量,但在運行時可以手動的擴充其容量,所以,可變數組的真實容量也可以是無限的,可變數組也可以存儲到數據庫中。

 1 declare
 2   type varr is varray(5) of int;--創建可變數組類型
 3   v_varr varr:=varr();--聲明可變數組類型的變量並初始化
 4   --和嵌套表一樣的原因,必須初始化
 5 begin
 6   for i in 1..5 loop--循環插入值
 7  v_varr.extend();  8     v_varr(i):=i;  9     end loop; dbms_output.put_line(v_varr(1)||','||v_varr(2)||','||v_varr(3)||','||v_varr(4)||','||v_varr(5)); 10 end;

  嵌套表和可變數組能存入數據庫是指:他們可以和普通數據類型一樣,用來定義表的列。

 1 --第一步,創建一個保存在數據庫中的嵌套表類型
 2 create or replace type nest is table of varchar(20);  3 --第二步,創建一個帶有嵌套表類型列的數據表
 4 create table x(  5   x_id int,  6  x_nest nest  7 )nested table x_nest store as y;--使用nest table 指定這是一個包含嵌套表類型值的數據表,並通過 store as 創建一個關聯表來專門存儲嵌套表  8 --插入一條數據(包含初始化的嵌套表類型值)
 9 insert into x values(1,nest('x','y','z')); 10 --第三步,在PL/SQL中讀取嵌套表類型的值(多行操作使用游標)。數據表並沒有直接存儲嵌套表,所以不能直接使用select查詢,而應該在PL/SQL程序塊中查詢
11 declare
12  v_nest nest; 13 begin
14   select x_nest into v_nest from x; 15   for i in 1..3 loop 16  dbms_output.put_line(v_nest(i)); 17   end loop; 18 end;

  把可變長度數組存放到數據庫就不需要使用 nested table 和 store as 指定相關信息,而且可以直接使用 select 查詢存儲了可變長度數組的數據表。所以,通常的建議是,當只是臨時使用集合,那麼索引表是最好的選擇,如果需要把集合存入數據庫,可變數組更操作起來更簡單。

  

  5,集合常用方法

  集合的方法通過“.”點的形式調用:集合.方法。

1 集合.exists(n)--判斷是否存在某個集合的值
2 集合.count--統計集合中值的個數
3 集合.limit--查詢集合容量(長度)
4 集合.first/集合.last--集合中第一個/最後一個值的索引
5 集合.prior(n)/集合.nest(n)--指定索引位置前一個/下一個值的索引(一般用在索引表中,因為其下標可能不連續)
6 集合.extend/集合.extend(n)--為集合增加1個/n個容量(一般用在嵌套表和可變數組中)
7 集合.trim/集合.trim(n)--從集合末尾刪除1個/n個元素(一般用在嵌套表和可變數組中)
8 集合.delete/集合.delete(n)--從集合中刪除所有元素/第n個元素(一般用在索引表和嵌套表中)

   

  6,變量的作用域

 1 declare
 2  v1 int default 1;--外層塊變量v1
 3 begin
 4  dbms_output.put_line(v1);  5  --dbms_output.put_line(v2);error 必須聲明v2
 6  declare
 7   v2 int default 2;---內層塊變量
 8   v1 int default 3;  9  begin
10   dbms_output.put_line(v1);--返回3
11  end; 12 end;

  變量只在聲明的塊中起作用,內層塊可以訪問外層塊的變量,但外層塊無法訪問內層塊的變量,如果內外塊聲明的相同的變量,那麼 PL/SQL 採用就近原則。

 

五  流程控制

  

  1,case

  case 語句有兩種語法,簡單 case 語法只做等值匹配,搜索 case 語法可以做區間匹配。

  先來看簡單 case 語法:

 1 declare
 2   v_sal int;  3 begin
 4   select sal into v_sal from emp where empno=&empno;  5   case v_sal  6     when 800 then dbms_output.put_line('太少了吧');  7     when 1600 then dbms_output.put_line('這還差不多');  8     when 3000 then dbms_output.put_line('這樣更好');  9     when 5000 then dbms_output.put_line('這樣最好'); 10     else dbms_output.put_line('隨緣吧'); 11     end case; 12 end;

  搜索 case 語法:

 1 begin
 2   select sal into v_sal from emp where empno=&empno;  3   case 
 4     when v_sal<=1000 then dbms_output.put_line('太少了吧');  5     when v_sal<=1600 then dbms_output.put_line('這還差不多');  6     when v_sal<=3000 then dbms_output.put_line('這樣更好');  7     when v_sal<=5000 then dbms_output.put_line('這樣最好');  8     else dbms_output.put_line('隨緣吧');  9     end case; 10 end;

  請仔細觀察兩種語法的區別。

  

  2,if…elsif…else

 1 declare
 2   v_sal int;  3 begin
 4   select sal into v_sal from emp where empno=&empno;  5   if v_sal>=5000
 6     then dbms_output.put_line('還有頭髮嗎');  7   elsif v_sal>=3000
 8     then dbms_output.put_line('還有一半嗎');  9   else
10     dbms_output.put_line('好好珍惜頭髮啊,少年'); 11   end if; 12 end;

  請注意,PL/SQL 中的多分支結構 elsif 關鍵字與其他語言相比,少了一個字母 e,且 els 和 if 之間沒有空格。

  

  3,循環

  PL/SQL 提供了 3 種循環:loop、while、for(集合部分已經見過了)。

  在正式介紹循環之前,首先要介紹 PL/SQL 中的循環控制語句:exit,無條件結束整個循環(類似其他語言中的 break)。continue,結束本次循環,繼續下一次循環。接下里讓我們通過例子來詳細說明每個循環的使用方法。

  loop 循環:

 1 declare
 2   i int default 1;--定義,初始化循環控制變量
 3 begin
 4  loop  5     if i=5 then
 6       i:=i+1;  7       continue;--當n等於5時,直接結束本次循環,不輸出
 8     end if;  9  dbms_output.put_line(i); 10     i:=i+1;--修改循環控制變量 
11     exit when i>10;--根據循環控制比變量,判斷是否退出循環
12   end loop;--結束循環
13 end;

  while 循環:

1 declare
2   i int default 1;--定義,初始化循環控制變量
3 begin
4   while i<=10 loop--根據循環控制變量,判斷是否進入循環體
5     dbms_output.put_line(i);--循環體
6     i:=i+1;--修改循環控制變量
7   end loop;--結束循環
8 end;

  for 循環:

1 begin
2   --在for循環中,初始化循環控制變量,只需指明變量名即可,類型系統默認為数字,min..max指明控制變量的變化範圍,從min開始,到max結束
3   for i in reverse 1..10 loop--i可以被循環體引用,但不能被賦值
4   dbms_output.put_line(i);--循環體
5   --注意,因為初始化循環變量時已經指定了變化範圍,這相當於限定了循環條件,當變量從min變化到max時將自動結束循環
6   end loop;  --結束循環
7   --最後說明,reverse是可選的參數,表示循環變量從max開始,到min結束
8 end;

  

  4,雜項

  這裏要介紹兩個東西,null 語句(不是null 值)和 goto 語句。null 語句表示什麼也不做,goto 可以無條件跳轉到程序指定位置。

1 begin
2   if ... then
3  ... 4   else
5     null;--什麼也不做,但使整個語句塊更豐滿,可讀性更高
6   end if; 7 end;
 1 declare
 2   i int:=0;  3 begin
 4   <<outer>>--定義一個標籤
 5   i:=i+1;  6  dbms_output.put_line(i);  7   if i<10 then
 8     goto outer;--通過goto實現類似循環的結構
 9   else
10     null;--通過使用null讓語句塊更易讀
11   end if; 12 end;

  使用 goto 語句會破壞程序常規的執行流程,它是有別於順序、分支、循壞的另一種執行流程,如無特別需求,建議不要使用。

 

六  異常處理

  

  1,異常簡介

  無論何時何地何人,在編程的領域,總是無法避開異常。為了保證程序的健壯性,多數語言都提供了異常處理機制,PL/SQL 也不例外。

  在 PL/SQL 中,異常大致可分為兩大類:

    編譯時錯誤:程序在編寫過程中的錯誤,例如語法錯誤,訪問不存在的對象等,這類錯誤在編譯時 PL/SQL 引擎就會發現,並通知用戶。

    執行時錯誤:這類錯誤會順利通過程序的編譯環節,只能等到執行時才能被發現,比如除數是 0 。這類錯誤也是最要命的。

  

  2,異常處理語法

  我們知道,PL/SQL 程序分為三個部分:聲明區,執行區,異常處理區。基本的異常處理也包含此三個步驟:

    A:在定義區,定義異常。

    B:在執行區,觸發異常。

    C:只要執行區觸發了異常,那麼執行區後續的業務代碼都會立即停止執行,執行流程跳轉至異常處理區。

 1 declare
 2  異常變量名 exception;  3 begin
 4  ...  5  raise 異常變量名;  6  ...  7  exception  8       when 異常變量名  9         then ... 10 end;

  如果有多個異常,可以定義多個變量,並在合適的時候觸發他們,並在異常處理區通過多個 when…then 來捕獲他們,並執行特定操作。

  

  3,預定義異常

  大多數編譯時的異常,Oracle 都在內部隱式的定義好了,並且不需要在執行區手動的觸發,這類異常的處理最為簡單:

declare v_tmp varchar(10); begin v_tmp:='超過10字節的長度了'; exception when value_error then dbms_output.put_line('出現value_error錯誤!' || '錯誤編號:'|| sqlcode || '錯誤名稱' || sqlerrm); end;

  PL/SQL 中出現的錯誤,都一個錯誤號,一個錯誤編碼(sqlcode),一個錯誤名稱(sqlerrm)。在錯誤處理區通過在 when 後面指定錯誤名稱,既可捕獲到指定錯誤了。常見的預定義錯誤如下:

錯誤號 異常編碼 異常名稱 描述
ora-01012 -1017 not_logged_on 在沒有連接數據庫時訪問數據
ora-01403 100 no_date_found select…into沒有返回值
ora-01422 -1422 too_many_rows select…into結果集超過一行
ora-01476 -1476 zero_divide 除數為0
ora-01722 -1722 invalid_number 字符串和数字相加時,字符串轉換失敗
ora-06502 -6502 value-error 賦值時,變量長度不足
ora-06530 -6530 access_into_null 向null值對象賦值
ora-06592 -06592 case_not_found case語句中沒有任何匹配的值並且沒有else選項

  更多預定義異常請查詢 Oracle 11g 《Oracle 在線文檔》。

  

  4,自定義錯誤

 1 declare
 2   e_nocomm exception;--定義一個異常名稱
 3   v_comm number(10,2);  4 begin
 5   select comm into v_comm from emp where empno=&empno;  6   if v_comm is null
 7     then raise e_nocomm;--觸發自定義異常
 8   end if;  9  exception 10     when e_nocomm--捕獲自定義異常
11       then dbms_output.put_lne('該員工沒有提成'); 12     when others--捕獲未定義的錯誤 13       then dbms_output.put_line('未知錯誤 !'); 14 end;

  同一個塊中不能同時聲明一個異常多次,但不同的塊中可以定義相同的異常,在各自的塊中使用不會相互影響。

 

七  編程對象

  

  1,事務

  在 SQL Server 中,每一條 DML 語句都是一個隱式的事務,除非显示的開始一個事務,否則,這些語句執行完就立即向數據庫提交了這些更改。而在 Oracle 中,每一條 SQL 語句開始都會自動開啟一個事務,除非显示的使用 commit 提交,或退出某個開發工具而斷開連接,才會提交到數據庫,否則這些操作都只會保存在內存中。

 1 --在Oracle SQL Developer中
 2 begin
 3   insert into dept values(88,'開發部','cd');  4   savepoint a;--設置保存點a
 5   insert into dept values(88,'設計部','cd');  6  exception  7     when dup_val_on_index then
 8     dbms_output.put_line('插入出錯');  9     rollback to a;--回滾到a
10 end; 11 --這裏我們人為的製造了一個違反唯一約束的插入操作,在錯誤區捕獲該錯誤,然後回滾到保存點a
12 select * from dept;--只能查詢到開發部被插入
1 /* 在 SQL*Plus 中 */
2 SQL>select * from dept; 3 /* 連開發部都沒有被插入 */ 
1 -- 在 Oracle SQL Developer中
2 commit; 3 --現在插入已經被提交到數據庫,在SQL*Plus 中也可以查詢到了

  在多個事務併發執行時,大概率會發生:一個事務讀取到另一個事務還未提交的數據(臟讀);一個事務中不同時間點執行的同一個查詢,由於其他事務對涉及的數據進行了修改或刪除(不可重複讀)或插入(幻讀),而導致出現不一樣的結果。

  為了解決這樣的問題,Oracle 允許對事務設立隔離級別:

1 begin
2   commit; 3   set transaction read only;--只讀的事務
4   --settransaction read write;--可讀寫的事務
5   --set transaction isolation level [serializable | read commited];
6   --serializable:整個事務只能讀到當前事務開始前就以提交的數據
7   --read commited:當前事務中的查詢,只能讀到該查詢前以提交的數據(不是整個事務,而是該查詢語句。這也是 Oracle 默認的隔離級別)
8 end;

  由於一個事務中有且只能存在一條 set transaction 語句,且必須是事務的第一條語句,所以通常先使用 commit 結束前一個事務(理論上rollback也可以),以保證該語句是事務的第一條語句。

  

  2,子程序

  Oracle 中子程序事實上就是 SQL Server 中對存儲過程和用戶自定義函數的總稱。過程和函數本質上是一個命名塊,可以被存儲在數據庫中,並在合適的時候調用,這樣可以解決代碼重用的問題,並且由於它是已編譯好的代碼,所以執行起來也更快。

  過程和函數相比,過程不會返回值,常用來做數據的增刪改。而函數必須有返回值,通常用來嚮應用程序返回值。其他方面,過程和函數幾無區別。

  存儲過程:

1 --無參過程
2 create or replace procedure p2 as
3 begin
4   dbms_output.put_line('hello world'); 5 end p2; 6 --or replace:如果存在則替換存儲過程,建議使用
7 --p1:過程名
8 --as:不能省略,也可以用is代替
9 --end p2:創建完成時也要跟上過程名
 1 --帶參數的過程
 2 create or replace procedure p2(p_deptno in int)--使用括號添加過程需要的形參
 3 as
 4   v_empcount number;--定義過程中需要使用的變量,只需指定數據類型,不能添加類型所佔字節長度
 5 begin
 6   select count(ename) into v_empcount from emp where deptno=p_deptno;  7   if v_empcount>0 then
 8     dbms_output.put_line('有人');  9   else
10     dbms_output.put_line('沒人'); 11   end if; 12 end p2;--不要忘了過程名
1 --調用存儲過程
2 begin
3   p2(20);--通過()傳遞實參
4 end; 5 --call p2(20);

  函數:

 1 --創建函數
 2 create or replace function f1  3 return number--需要指定返回值類型,不需要長度
 4 as
 5 begin
 6   return 1;--需要使用return指定返回值
 7 end f1;  8 --調用函數
 9 declare 
10   v_f1 number(10); 11 begin
12   v_f1:=f1();--調用函數,並把返回值賦值給變量
13  dbms_output.put_line(v_f1); 14 end;

  在上面帶參數存儲過程中,指定形參時使用關鍵字 in,該關鍵字表示參數的模式是輸入型,可選的還有 out 輸出型,in out 輸入輸出型。如果不提供,默認是輸入型參數。

  in 模式的參數被用作輸入參數,在過程內部只能被訪問,不能被賦值。

  out  模式的參數被當做輸出參數使用,在過程內部可以被賦值,不能訪問。使用 out 類型參數時,必須在過程外部定義一個變量,用於接收過程在內部需要輸出的值,然後在調用子程序時把該變量當做形參傳入。待過程執行完畢,直接訪問外部定義的這個變量即可得到過程希望輸出的值了。

  in out 模式的參數既可以被當做輸入參數,也可以被當做輸出參數。使用方式和 out 型參數一致,但可以給這個變量一個初始化值,一併傳入過程內部。out 型參數即使傳入了初始值,也會被過程忽略。

  過程的參數模式和 MySQL 完全一致,例子可以參考我的《MySQL 編程》。函數本身就需要使用 return 返回值,所以不使用 in 或 out 指定參數模式,這樣毫無意義。

  

  3,觸發器

  Oracle 中的觸發器本質上也是一個命名的語句塊,定義的方式和 PL/SQL 語句塊差不多,但它和過程或函數不同,它只能被隱式的調用。並且不能接受任何參數。

  定義觸發器的語法:

1 create or replace trigger trigger_name--觸發器名稱
2 [before | after | instead of]--在事件之前還是之後執行觸發器中的代碼
3 trigger_event--觸發事件
4 [referenceing_caluse]--通過新的名稱引用當前正在更新的數據
5 [when trigger_condition]--指定觸發條件
6 [for each row]--指定行級觸發器(每一條記錄都觸發一次)
7 trigger_body--觸發體(程序塊)

  一個簡單例子:

 1 create test(--創建測試表
 2     id int primary key,  3     name varchar(20)  4 )  5 create or replace trigger t_test--創建觸發器
 6 after insert or update or delete--觸發操作(也可以是其中一種)
 7 on test--在表test上
 8 for each row--行級觸發器
 9 begin
10     if inserting then--在插入數據時
11         dbms_output.put_line('插入了數據,name:'||:new.name); 12     end if; 13     if updating then--在更新數據時
14         dbms_output.put_line('更新了數據,oldname:'||:old.name||',newname:'||:new.name); 15     end if; 16     if deleting then--在刪除數據時
17         dbms_output.put_line('刪除了數據,name:'||:old.name); 18     end if; 19 end;

  謂詞:new 表示引用新的數據(更新后或插入的數據),:old 引用舊的數據(被刪除的或更新前的數據)。可以在創建觸發器時通過 referencing(操作類型之後,for each row 之前) 指定新的謂詞。

1 ... 2 referencing old as test_old new as test_new 3 ... 4 --下面通過:test_old 引用修改前的數據,:test_new引用修改后的數據

  測試代碼:

1 insert into test values(1,'r'); 2 update test set name='e' where id=1; 3 delete from test where id=1; 4 --注意觀察輸出結果

  

  4,游標

  Oracle 中的游標用來處理多行多列的數據集合,包含四個步驟:定義,打開,遍歷,關閉。游標的語法如下:

1 cursor cursor_name [形參]--形參可以用來在where子句中限定游標記錄
2 [return type]--可選的指定游標返回的值類型
3 is query--通過is指定查詢(在這裏使用形參)
4 [for update[of column_list]]--允許在游標中修改表中的數據,並在游標打開期間鎖定選中的記錄

  下面是一個通過游標遍歷輸出 dept 部門信息的例子:

 1 declare
 2     deptrow dept%rowtype;--定義一個存儲記錄的變量
 3     cursor dept_cur is--通過cursor定義游標,is指定需要遍歷的結果集(一個查詢語句)
 4     select * from dept;  5 begin
 6     open dept_cur;--打開游標
 7     loop--通過循環遍歷游標中的記錄
 8         fetch dept_cur into deptrow;--通過fetch提取游標中記錄(每次一條)賦值給變量
 9         dbms_output.put_line(deptrow.deptno||':'||deptrow.dname); 10         exit when dept_cur%notfound;--通過%notfound判斷游標中是否還有記錄
11     end loop; 12     close dept_cur;--關閉游標
13 end;

  游標除了 %notfound 還有以下常用的的屬性:

1 cursor%isopen;--檢測游標是否已打開,打開返回ture,否則返回false
2 cursor%found;--檢測是否提取到值,提取到返回true,否者返回false
3 cursor%notfound;--與%found相反
4 cursor%rowcount;--統計到目前為止已提取的記錄數

  PL/SQL 中的三種循環都可以用來循環遍歷游標中的記錄,while 和 loop 相似,這裏不再舉例,for 循環專門對遍歷游標做了強化,工作中使用最多,也最方便:

1 delcare 2     cursor dept_cur is
3     select * from dept; 4 begin
5     for dept_row in dept_cur loop 6         dbms_output.put_line(deptrow.deptno||':'||deptrow.dname); 7     end loop; 8 end;

  dept_row 不需要顯式的聲明為記錄類型,PL/SQL 引擎自動隱式的聲明為 %rowtype。for 循環開始,自動打開游標,並自動提取記錄,然後賦值給dept_row,不用顯式的使用 fetch 提取記錄,循環完畢自動關閉游標並退出循環。

  

  5,包

  Oracle 中包(package)是一個工程化和面向對象的概念,它就像一個容器或命名空間,把邏輯相關的變量、類型、子程序或異常等組合起來一起存放,形成一個有序的組織單元或模塊,當我們編寫大型的複雜的應用程序時,我們就可以通過包來方便的歸類和管理各個功能模塊。

  完整的包由包規範和包體組成,但 Oracle 分開編譯的存儲包規範和包體,這又使得我們可以脫離包體使用包規範(反向不行)。包規範中主要是一些定義信息(也可以看成是 PL/SQL 提供的 API),比如記錄類型、變量、游標、異常和子程序的聲明。包體則負責實現包規範中定義的子程序。

  包規範簡單應用:

 1 create or replace package pkg1--創建包規範  2 as
 3   i int := 1;--標量變量
 4   dept_record dept%rowtype;--rowtype類型
 5   type dept_tab is table of varchar(20) index by pls_integer;--集合類型  6 end pkg1;  7 
 8 declare
 9  mydept pkg1.dept_tab;--創建一個包中集合類型的變量(通過"包.內容"的方式訪問包中的內容) 10 begin
11   select * into pkg1.dept_record from dept where deptno=10;--給包中定義的rowtype類型變量賦值 12  dbms_output.put_line(pkg1.dept_record.dname);--訪問包中的rowtype類型變量 13   dbms_output.put_line('-------------------------------------------');--分割線 14   for deptrow in (select * from dept) loop--使用游標給包中的集合賦值 15     mydept(pkg1.i) := deptrow.dname; 16     pkg1.i := pkg1.i+1;--修改包中的標量變量 17   end loop; 18   for j in 1..mydept.count loop--使用循環訪問集合 19  dbms_output.put_line(mydept(j)); 20   end loop; 21   pkg1.i := 1;--初始化包中的標量變量(防止下一次游標讀取不到數據) 22 end;

   在這個例子中,我們只創建了包規範,沒有包體,並且在包中定義了標量變量,rowtype類型(記錄類型同理),集合這些基本的數據類型,然後在 PL/SQL 程序塊中使用了他們。

  包規範中只有聲明,沒有具體的實現,事實上,包規範中的聲明的內容是公共的,對於一個方案來說,相當於一個全局的對象,在包內任何地方都能訪問他們。包規範和包體分別進行獨立的編譯和存儲,所以沒有包體,上訴例子任然能正常運行。

  另一個例子:

 1 create or replace package pkg2--創建包規範  2 as
 3   cursor dept_cur return dept%rowtype;--定義游標類型  4   procedure dept_ins(p_deptno int,p_dname varchar);--定義存儲過程  5   function f2 return varchar;--定義函數  6 end pkg2;  7 
 8 create or replace package body pkg2--創建包體  9 as
10   cursor dept_cur return dept%rowtype--創建游標 11   is
12     select * from dept; 13   procedure dept_ins(p_deptno in int,p_dname in varchar)--創建存儲過程 14   as
15   begin
16     insert into dept(deptno,dname) values(p_deptno,p_dname); 17     dbms_output.put_line('新增了部門:'|| p_deptno||','||p_dname); 18   end dept_ins; 19   function f2 return varchar--創建函數
20   is
21   begin
22     return '這是個函數'; 23   end f2; 24 end pkg2; 25 
26 
27 begin
28   for deptrow in pkg2.dept_cur loop--讀取游標 29  dbms_output.put_line(deptrow.dname); 30   end loop; 31   pkg2.dept_ins(99,'TI');--執行存儲過程 32  dbms_output.put_line(pkg2.f2());--執行函數 33 end;

  上面的例子在包體中定義了游標,存儲過程和函數,並且在包規範中也聲明了他們,這時候,存儲過程和函數、游標都是公開的了,如果在包體中創建的內容並未在包規範中定義,那麼我們說,這些內容是包私有的,不能在其他地方調用,而只能在包體內部使用。

  合理的使用包,有助於我們進行模塊化的程序開發;把邏輯相關的東西放在一個包中進行開發和管理,可以使我們的程序更加規範化;把一些重要的東西定義成包的私有內容,可以大大加強數據的安全性;另外,由於在使用包時, PL/SQL 會把整個包都加載到內存中,所以還可以提高程序運行效率。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

SwiftUI – iOS10本地推送通知教程UserNotifications在Swift中的實現方式

簡介

消息推送相信在很多人的眼裡都不陌生了吧?像即時聊天微信,好友發信息給你時會在頂部彈下小窗口提醒你。也像是在影院APP預訂了電影票,在開場前一小時你也會收到提醒。這類推送是需要經過後端發送請求的,需要服務器發送推送請求,又或者使用如極光推送等第三方渠道。

那麼如果我們的APP不需要連網呢?這是不是就不能使用消息推送了?不是的,蘋果還提供給我們本地消息通知服務,即便APP不連網也能使用,功能也很強大可靠。本地時鐘的應用場景很廣泛,例如手機上的時鐘、日曆等。

那麼你知道如何去實現它嗎?這篇文章將告知你答案,同時以兩個小案例作為例子,以便更好地去理解它。

筆者環境

Xcode – Version 11.5 (11E608c)

Swift – version 5.2.4 (swiftlang-1103.0.32.9 clang-1103.0.32.53).

權限獲取

UserNotifications 是 iOS10 推出來的框架,因此你只能在 10 或以上的版本使用它。推送服務和以往一樣,也是需要用戶授權的,當用戶同意后才能正常註冊消息通知,當用戶拒絕時應該引導用戶去打開APP的通知權限。利用requestAuthorization方法彈出並獲取通知權限,接收的參數options是具體的授權選項,一般有彈窗、未讀數量圖標和聲音即可,並在回調閉包中可以獲取授權結果和錯誤。

UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (status, err) in
    if !status {
    		print("用戶不同意授權通知權限")
        return
    }
}

status 為布爾類型,true 表示用戶同意,false 即拒絕。在此種情況下,我們可以使用彈窗去引導用戶去打開通知權限,需要明確告知用戶打開後有什麼好處,如果關閉會造成什麼影響等等。如果讓用戶手動打開設置,找到APP,為APP開啟權限,這樣未免太過複雜,所幸的是可以通過以下代碼為用戶直接跳轉至該應用的權限設置中心。

guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
if UIApplication.shared.canOpenURL(url) {
    UIApplication.shared.open(url, completionHandler: nil)
}

應彈窗提示用戶,待用戶同意后才跳轉至設置,不然容易引起用戶的不滿心理。

觸發器

本地消息通知一般有以下三種類型的觸發器,它們都是繼承於類UNNotificationTrigger

  1. UNTimeIntervalNotificationTrigger – 在經過特定的時間后觸發本地消息推送;
  2. UNCalendarNotificationTrigger – 在特定的時間點觸發本地消息推送;
  3. UNLocationNotificationTrigger – 在進入或離開特定的地理位置時觸發本地消息推送。

UNTimeIntervalNotificationTrigger

手機上的時鐘用過吧,裏面的計時器功能就可以用UNTimeIntervalNotificationTrigger實現,比如開始計時30分鐘,那麼在計時器完成的時候就是使用通知提醒。

那麼設置在經過特定的時間后觸發本地消息推送,一般都經由以下幾個步驟:

  1. 首先創建UNMutableNotificationContent類,設定標題和內容,如果你有子標題還可以設置子標題,一般很少見到會設置子標題的應用。
  2. 創建觸發器,這裏就是UNTimeIntervalNotificationTrigger,設定執行秒數和是否循環通知。
  3. 創建通知請求UNNotificationRequest,這裏需要指定通知的identifier,內容和觸發器,至於identifier,你可以隨意定義。
  4. 最後將通知請求添加到系統的通知中心UNUserNotificationCenter即可。

例子,創建一個通知,在5秒后執行消息推送。實例代碼展示如下:

let content = UNMutableNotificationContent()
content.title = "添加朋友 對着月亮敲代碼"
//content.subtitle = "子標題"
content.body = "公眾號 gh_6a83a7c19315"
content.badge = 1

let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
let request = UNNotificationRequest(identifier: "Notification", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { err in
    err != nil ? print("添加本地通知錯誤", err!.localizedDescription) : print("添加本地通知成功")
}

有一處小 Tips,UNTimeIntervalNotificationTrigger創建時的repeats選項,如果你設定為循環通知時,即需要每隔N秒觸發一次通知,那麼你必須至少設置為60秒的時間間隔,如若低於60秒,你將會得到這樣一條錯誤。

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'time interval must be at least 60 if repeating'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff23c7127e __exceptionPreprocess + 350
	1   libobjc.A.dylib                     0x00007fff513fbb20 objc_exception_throw + 48
	2   CoreFoundation                      0x00007fff23c70ff8 +[NSException raise:format:arguments:] + 88
	3   Foundation                          0x00007fff256e9b51 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 191
	4   UserNotifications                   0x00007fff2c7dfc7c -[UNTimeIntervalNotificationTrigger _initWithTimeInterval:repeats:] + 277

UNCalendarNotificationTrigger

手機上的日曆用過吧,在新建日程的時候,你可以選擇一個提醒時間,這樣它就會在你設定的提醒時間提醒你,這種情況就很適合用UNCalendarNotificationTrigger去實現。

舉個例子,我們要在每晚7點提醒用戶看公眾號。

let content = UNMutableNotificationContent()
content.title = "添加朋友 對着月亮敲代碼"
//content.subtitle = "子標題"
content.body = "公眾號 gh_6a83a7c19315"
content.badge = 1

let dateComponents = DateComponents(hour: 19) // 1
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true) // 2
let request = UNNotificationRequest(identifier: "Notification", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { err in
    err != nil ? print("添加本地通知錯誤", err!.localizedDescription) : print("添加本地通知成功")
}

1 – 創建時間元件,19點即為晚上7點

2 – 創建UNCalendarNotificationTrigger對象,並將dateComponents賦值到dateMatching,repeats為true,重複在每天19點收到通知提醒。

UNLocationNotificationTrigger

這個觸發器不在此篇文章講述,留給你們自己去實現和測試結果。

圖標

還記得剛剛設置的屬性badge嗎,我們設置值為1,這意味着在iPhone桌面上的應用圖標在收到通知時,右上角圓點內所展示的数字就是badge的值。

這個屬性值是applicationIconBadgeNumber,它是UIApplication的屬性,設置為0即為隱藏,默認也是0。

UIApplication.shared.applicationIconBadgeNumber = 0

消息推送回調代理

接收用戶對消息推送的反饋事件,比如說應用在後台收到了通知,用戶點擊了這條通知進入到了APP裏面,我們需要獲取這個事件去做一些處理,比如跳去某個界面,這裏例子不講這麼複雜,只通過簡單地判斷用戶是通過哪個通知進來的。

接收回調代理事件前,需要遵循UNUserNotificationCenterDelegate協議,並設置delegate接收的對象。

extension AppDelegate: UNUserNotificationCenterDelegate {}

UNUserNotificationCenter.current().delegate = self

Swift語言中,可以通過extension擴展類遵循的協議,並在extension

當應用在前台運行時,收到的是這個-userNotificationCenter:willPresentNotification:withCompletionHandler:代理方法。UNNotification對象存儲了傳遞到應用的一些數據,通過此對象可以拿到此條通知關聯的觸發器notification.request.trigger,從而判斷其類型。

func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    guard let trigger = notification.request.trigger else { return; }
    if trigger.isKind(of: UNTimeIntervalNotificationTrigger.classForCoder()) {
        print("Notification did receive, Is class UNTimeIntervalNotificationTrigger")
    } else if trigger.isKind(of: UNCalendarNotificationTrigger.classForCoder()) {
        print("Notification did receive, Is class UNCalendarNotificationTrigger")
    }
}

當應用在後台,或者被殺死的狀態下,收到的是這個-userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:代理方法。此方法接收UNNotificationResponse類型的參數,它裡面包含notification屬性,因此可以參考上面的代碼進行觸發器的判斷。

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
    guard let trigger = response.notification.request.trigger else { return; }
    if trigger.isKind(of: UNTimeIntervalNotificationTrigger.classForCoder()) {
        print("Notification did receive, Is class UNTimeIntervalNotificationTrigger")
    } else if trigger.isKind(of: UNCalendarNotificationTrigger.classForCoder()) {
        print("Notification did receive, Is class UNCalendarNotificationTrigger")
    }
}

總結

  1. 本地通知有三種類型的觸發器,分別是UNTimeIntervalNotificationTrigger、UNCalendarNotificationTrigger和UNLocationNotificationTrigger。
  2. UNTimeIntervalNotificationTrigger在設置循環通知時,所設定的時間隔不能低於60秒,否則會報運行時錯誤。

往期回顧

  1. SwiftUI – 一起來仿寫微信APP之一首頁列表視圖
  2. SwiftUI – 一步一步教你使用UIViewRepresentable封裝網絡加載視圖(UIActivityIndicatorView)

Demo 源碼下載

我已經把 Demo 上傳至 GitHub 上面,項目名字是 SwiftUI-Tutorials,目錄名為GCLocalUserNotification,有需要的朋友可以去下載運行一下,當然你也可以跟着文章去做一遍,這樣更有利於你掌握此方面的知識。

如果本文章對你有幫助,請關注我,你的關注就是我後續寫文章的動力,下期會更精彩噢!

關於作者

博文作者:GarveyCalvin

微博:https://weibo.com/feiyueharia

博客園:https://www.cnblogs.com/GarveyCalvin

本文版權歸作者,歡迎轉載,但必須保留此段聲明,並給出原文鏈接,謝謝合作!

公眾號

作者第一次運營公眾號,請你們一定要關注我的公眾號,給我點動力,後期主要運營公眾號為主。這是第三篇發布的文章,需要你們的支持,謝謝你們!

微信群

佛系等待你們的到來,若二維碼過期,請加我QQ:1147626297,記得寫備註,我重新發鏈接給你。快來加入我的“億”個人群吧!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

Hook踩坑記:React Hook react-unity-webgl

  自公司前後分離上手React以來,一個坑一個坑的踩,Class的全生命周期雲里霧裡,還么屢明白,就抱上了Hook的大腿不鬆手,確實爽到飛起。修改到Hook的過程基本比較順暢,直接少了三分之一的代碼,組件更容易封裝,調試更方便,諸多優點在此不再贅述,已有各路大佬紛紛評價,此處貼上中文官方地址:React-Hook文檔。這裏主要講講修改到一塊關於 Unity 3D模型加載的踩坑記。

  背景:React 加載 Unity 3D模型 ,使用到一個插件 react-unity-webgl,感興趣的盆友可以自行查閱。

  因為Class改Hook處理語法變動,邏輯代碼基本不用怎麼改動,所以基本沒有阻力,但是當我把這塊業務代碼改成Hook時,跟模型交互時通信失敗,無法驅動模型動作。百思不得其解,弄了倆測試頁面,test_hook、test_class,只能debugger,一步一步調,發現一些端倪。

  Class 有些初始化的代碼 都寫在了constructor(props){},這個大家都明白,第一次加載頁面的時候會走。hook呢,最外層是一個大方法,之前遷移的時候就寫在方法里最頂部了,也沒什麼問題。加載模型第一句是 const unityContent = new UnityContent(參數1,參數2);兩個頁面都能加載出來模型,但是跟斷點發現hook頁面的 unityContent 對象比class的缺少了一個重要的屬性:unityInstance,通信的方法就是靠它 Send() 的,而且發現同一個對象,屬性id一直在變,原來每次修改state時,都會走聲明的這段方法,導致每次都 new 一個新的對象,導致unityInstance屬性沒有正確掛在unityContent對象上。

  在知道大概原理的情況下,搞成全局變量,在加載時判斷是否已經初始化,問題就迎刃而解了(其實費了九牛二虎之力)。

  寫過hook的盆友第一反應會想到聲明寫到useEffect,然後 [] 只執行一次才是正確的寫法。

  之所以沒有呢,是因為模型加載跟其他的業務沒什麼關係,我並不需要渲染完整個DOM在來加載,並且加載模型很費時間,必須要剛加載頁面就同時加載模型,所以才有此次踩坑記。

  總結:Hook寫在useEffect之外的代碼會多次加載(包括刷新狀態),要做好判斷,否則很容易產生bug。更推薦(官方推薦)按業務按順序把初始化方法寫到useEffect。

  本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

Pytest單元測試框架——Pytest+Allure+Jenkins的應用

一、簡介

  pytest+allure+jenkins進行接口測試、生成測試報告、結合jenkins進行集成。

  pytest是python的一種單元測試框架,與python自帶的unittest測試框架類似,但是比unittest框架使用起來更簡潔,效率更高

  allure-pytest是python的一個第三方庫。用於連接pytest和allure,使它們可以配合在一起使用。

  allure-pytest基於pytest的原始執行結果生成適用於allure的json格式結果。該json格式結果可以用於後續適用allure生成html結果。

二、安裝  

  1、安裝pytest,命令行或終端中輸入

1 pip install pytest

  2、安裝allure-pytest,安裝成功

1 pip install allure-pytest

  allure-pytest安裝成功后截圖如下。

  3、下載安裝JDK

  官方下載:https://www.oracle.com/java/technologies/javase-jdk11-downloads.html

  安裝與配置不作闡述請諒解  

  4、下載安裝Jenkins

  官方下載:https://www.jenkins.io/

  安裝與配置不作闡述請諒解

三、下載Allure並配置

  下載allure並配置

  1、allure官網下載:https://github.com/allure-framework/allure2/releases

  如下圖所示

  2、allure2下載下來是一個zip的壓縮包,我們要解壓至自己的文件目錄下(可解壓放至項目的測試用例下或python安裝目錄下),自己可找到文件即可。

  3、打開allure2目錄,找到bin目錄,複製bin文件目錄, 然後進行環境變量的配置,設置環境變量的目的就是讓系統無論在哪個目錄下都可以運行allure2。

  4、環境變量設置:(桌面——我的電腦——右鍵屬性——高級系統配置——環境變量——系統變量——Path——編輯環境變量——把我們上面複製的目錄路徑新增至環境變量中即可)

  設置環境變量,如下圖所示。

 

  5、配置好后,打開cmd終端,輸入allure,出現以下幫助文檔,就說明配置成功了。

 四、Allure裝飾器描述

  Allure裝飾器

 五、Pytest+Allure的應用

  上述我們講了一些理論的知識,下面我們就來實戰練習一下吧。進一步理解Pytest+allure如何結合應用的。

  1、新建testcase文件夾,用來存放測試用例,新建test_Demo.py文件,作為pytest的具體測試用例文件。在test_Demo.py文件中輸入以下代碼。

 1 # test_Demo.py
 2 # Creator:wuwei
 3 # Date:2020-06-09
 4 
 5 import pytest
 6 import requests
 7 import allure
 8 import sys
 9 sys.dont_write_bytecode = True
10 
11 @allure.epic('測試描述'.center(30, '*'))
12 @allure.feature('測試模塊')
13 @allure.suite('測試套件')
14 class TestPytestOne():
15     @allure.story('用戶故事描述:用例一')
16     @allure.title('測試標題:用例一')
17     @allure.description('測試用例描述:用例一')
18     @allure.testcase('測試用例地址:https://www.baidu.com/')
19     @allure.tag('測試用例標籤:用例一')
20     def test_one(self):
21         print('執行第一個用例')
22         assert 1 == 1
23 
24     @allure.story('用戶故事描述:用例二')
25     @allure.title('測試標題:用例二')
26     @allure.description('測試用例描述:用例二')
27     @allure.testcase('測試用例地址:https://www.sogou.com/')
28     @allure.tag('測試用例標籤:用例二')
29     def test_two(self,action):
30         print('執行第二個用例')
31         assert True == True
32 
33 # pytest運行
34 if __name__ == "__main__":
35     pytest.main(['-s', '-v', 'test_Demo.py', '-q', '--alluredir', '../reports'])

  2、我們再來創建一個conftest.py,conftest用來共享數據及不同層次之間共享使用的文件,測試用例的前置和後置中一般都可以用到的。

 1 # conftest.py
 2 # Creator:wuwei
 3 # Date:2020-06-09
 4 
 5 import pytest
 6 import sys
 7 sys.dont_write_bytecode = True
 8 
 9 @pytest.fixture()
10 def action():
11     print("測試用例開始".center(30, '*'))
12     yield
13     print("測試用例結束".center(30, '*'))

  3、運行test_Demo.py文件,test_Demo文件中已經pytest+allure的結合,可查看allure的運行結果,可看出在根目錄中生成了一個reports文件夾,其中生成了測試報告的json文件,這裏面的json文件可通過allure生成html的測試報告。
  運行test_Demo.py,終端显示如下圖所示。

   生成的Json格式的測試報告,如下圖所示。

   4、使用allure將json文件生成html的測試報告,定位至項目文件根目錄下,運行以下命令,會在項目根目錄下生成一個名為allure_reports的文件夾,用來存放html測試報告。命令下如所示。

1 allure generate reports -o allure_reports/

  成功運行allure,結果如下圖所示。

  項目根目錄下的allure_reports文件,存放的是allure生成的測試報告。可看出文件下有一個HTML文件,可通過Python的編輯器Pycharm來打開該HTML文件(測試報告),或可通過allure命令來打開該HTML,展示HTML測試報告。如下所示。

  測試報告文件,HTML測試報告如下。

  allure命令打開HTML測試報告。命令如下所示。

1 allure open allure_reports/

  如下圖所示。

   打開生成的HTML測試報告如下圖所示。

 六、Pytest+Allure+Jenkins的應用

  1、Jenkins插件網站上下載allure插件最新版本:

    http://mirrors.jenkins-ci.org/plugins/allure-jenkins-plugin/

  2、Jenkins的安裝我已經在Postman+Newman+Git+Jenkins的篇章中講過了,沒看小夥伴可以看一下那篇文章。確認Jenkins服務是否開啟。確認開啟后,在瀏覽器中輸入:http://localhost:8080/,進入Jenkins配置頁面。

  3、http://localhost:8080/,登錄Jenkins的頁面,在管理Jenkins——插件管理——高級中找到上傳插件。將(1)步驟中下載的.hpi的文件上傳至jenkins上。

  上傳安裝好的allure-jenkins-plugin的插件,安裝完成並成功,是藍色圓點显示,因我已經安裝過一次,會提示已經安裝,重啟Jenkins即可生效。(注意:不是關閉瀏覽器重新打開,而是重啟Jenkins服務

  4、全局變量中配置allure路徑與JDK的路徑,

  配置JDK安裝的路徑,如下圖所示。

  配置allure安裝的路徑,如下圖所示。

  5、新建Item,配置構建后的allure測試報告生成。這裏配置Pytest執行完成之後,生成的allure文件所在的目錄位置。

  項目中生成allure的json測試報告的位置。需與下面構建后操作中的Results的Path文件一致。

  構建后操作的allure生成測試報告的配置,如下圖所示

  6、配置構建命令。就是上述在cmd中運行項目時的命令。如下圖所示。

注意:運行后發現有報錯。“Build step ‘Execute Windows batch command’ marked build as failure”,解決方案,在運行項目的命令后添加exit 0。如下圖所示。

  7、修改運行命令后我們再來運行一下。我們可發現運行后,allure裏面沒任務數據。因為我們還沒設置運行的項目路徑。設置工作空間,打開工作空間目錄,將我們的項目複製到jenkins的工作目錄中。

  我們可將代碼傳至GitHub上,在Jenkins中設置相關Github項目的配置,也可進行Jenkins部署。我在Postman+Newman+Git+Jenkins這篇博客里就應用到了。有興趣的可參考看看這篇Jenkins如何Git項目。在這裏我們使用本地項目來部署。

  測試報告無數據因為工作空間裏面沒有項目配置。

  複製項目至Jenkins工作空間的目錄中。

  8、添加項目后,我們再運行一下,藍點則為運行成功,可看到後面已經生成了allure的測試報告了。可直接點擊後面的alluree圖標跳轉至HTML的測試報告。如下圖所示。

  allure生成的HTML測試報告

八、總結

  上述我們聊了下pytest+allure+jenkins如何結合集成一起使用的,本地啟動jenkins,運行項目,調用allure生成測試報告。也簡單的做了一個小Demo。後期我將結合Requests接口測試和seleniumWeb測試應用至具體項目中。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

※回頭車貨運收費標準

源碼分析 | 手寫mybait-spring核心功能(乾貨好文一次學會工廠bean、類代理、bean註冊的使用)

作者:小傅哥
博客:https://bugstack.cn – 匯總系列原創專題文章

沉澱、分享、成長,讓自己和他人都能有所收穫!

一、前言介紹

一個知識點的學習過程基本分為;運行helloworld、熟練使用api、源碼分析、核心專家。在分析mybaits以及mybatis-spring源碼之前,我也只是簡單的使用,因為它好用。但是他是怎麼做的多半是憑自己的經驗去分析,但始終覺得這樣的感覺缺少點什麼,在幾次夙興夜寐,靡有朝矣之後決定徹底的研究一下,之後在去仿照着寫一版核心功能。依次來補全自己的技術棧的空缺。在現在技術知識像爆炸一樣迸發,而我們多半又忙於工作業務開發。就像一個不會修車的老司機,只能一腳油門,一腳剎車的奔波。車速很快,但經不起壞,累覺不愛。好!為了解決這樣問題,也為了錢程似錦(形容錢多的想家裡的棉布一樣),努力!

開動之前先慶祝下我的iPhone4s又活了,還是那麼好用(嗯!有點卡);

二、以往章節

關於mybaits & spring 源碼分析以及demo功能的章節匯總,可以通過下列內容進行系統的學習,同時以下章節會有部分內容涉及到demo版本的mybaits;

  • 源碼分析 | Mybatis接口沒有實現類為什麼可以執行增刪改查
  • 源碼分析 | 像盜墓一樣分析Spring是怎麼初始化xml並註冊bean的
  • 源碼分析 | 基於jdbc實現一個Demo版的Mybatis

三、一碟小菜類代理

往往從最簡單的內容才有抓手。先看一個接口到實現類的使用,在將這部分內容轉換為代理類。

1. 定義一個 IUserDao 接口並實現這個接口類

public interface IUserDao {

    String queryUserInfo();

}

public class UserDao implements IUserDao {

    @Override
    public String queryUserInfo() {
        return "實現類";
    }

}

2. new() 方式實例化

IUserDao userDao = new UserDao();
userDao.queryUserInfo();

這是最簡單的也是最常用的使用方式,new 個對象。

3. proxy 方式實例化

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class<?>[] classes = {IUserDao.class};
InvocationHandler handler = (proxy, method, args) -> "你被代理了 " + method.getName();

IUserDao userDao = (IUserDao) Proxy.newProxyInstance(classLoader, classes, handler);

String res = userDao.queryUserInfo();
logger.info("測試結果:{}", res);
  • Proxy.newProxyInstance 代理類實例化方式,對應傳入類的參數即可
  • ClassLoader,是這個類加載器,我們可以獲取當前線程的類加載器
  • InvocationHandler 是代理后實際操作方法執行的內容,在這裏可以添加自己業務場景需要的邏輯,在這裏我們只返回方法名

測試結果:

23:20:18.841 [main] INFO  org.itstack.demo.test.ApiTest - 測試結果:你被代理了 queryUserInfo

Process finished with exit code 0

四、盛宴來自Bean工廠

在使用Spring的時候,我們會採用註冊或配置文件的方式,將我們的類交給Spring管理。例如;

<bean id="userDao" class="org.itstack.demo.UserDao" scope="singleton"/>

UserDao是接口IUserDao的實現類,通過上面配置,就可以實例化一個類供我們使用,但如果IUserDao沒有實現類或者我們希望去動態改變他的實現類比如掛載到別的地方(像mybaits一樣),並且是由spring bean工廠管理的,該怎麼做呢?

1. FactoryBean的使用

FactoryBean 在spring起到着二當家的地位,它將近有70多個小弟(實現它的接口定義),那麼它有三個方法;

  • T getObject() throws Exception; 返回bean實例對象
  • Class<?> getObjectType(); 返回實例類類型
  • boolean isSingleton(); 判斷是否單例,單例會放到Spring容器中單實例緩存池中

那麼我們現在就將上面用到的代理類交給spring的FactoryBean進行管理,代碼如下;

ProxyBeanFactory.java & bean工廠實現類

public class ProxyBeanFactory implements FactoryBean<IUserDao> {

    @Override
    public IUserDao getObject() throws Exception {

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Class<?>[] classes = {IUserDao.class};
        InvocationHandler handler = (proxy, method, args) -> "你被代理了 " + method.getName();

        return (IUserDao) Proxy.newProxyInstance(classLoader, classes, handler);
    }

    @Override
    public Class<?> getObjectType() {
        return IUserDao.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

}

spring-config.xml & 配置bean類信息

<bean id="userDao" class="org.itstack.demo.bean.ProxyBeanFactory"/>

ApiTest.test_IUserDao() & 單元測試

@Test
public void test_IUserDao() {
    BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
    IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);
    String res = userDao.queryUserInfo();
    logger.info("測試結果:{}", res);
}

測試結果:

一月 20, 2020 23:43:35 上午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [spring-config.xml]
23:43:35.440 [main] INFO  org.itstack.demo.test.ApiTest - 測試結果:你被代理了 queryUserInfo

Process finished with exit code 0

咋樣,神奇不!你的接口都不需要實現類,就被安排的明明白白的。記住這個方法FactoryBean和動態代理。

2. BeanDefinitionRegistryPostProcessor 類註冊

你是否有懷疑過你媳婦把你錢沒收了之後都存放到哪去了,為啥你每次get都那麼費勁,像垃圾回收了一樣,不可達。

好嘞,媳婦那就別想了,研究下你的bean都被註冊到哪了就可以了。在spring的bean管理中,所有的bean最終都會被註冊到類DefaultListableBeanFactory中,接下來我們就主動註冊一個被我們代理了的bean。

RegisterBeanFactory.java & 註冊bean的實現類

public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(ProxyBeanFactory.class);

        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, "userDao");
        registry.registerBeanDefinition(definitionHolder.getBeanName(), definitionHolder.getBeanDefinition());
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // left intentionally blank
    }

}
  • 這裏包含4塊主要內容,分別是;
    • 實現BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry方法,獲取bean註冊對象
    • 定義bean,GenericBeanDefinition,這裏主要設置了我們的代理類工廠。我們已經測試過他獲取一個代理類
    • 創建bean定義處理類,BeanDefinitionHolder,這裏需要的主要參數;定義bean、bean名稱
    • 最後將我們自己的bean註冊到spring容器中去,registry.registerBeanDefinition()

spring-config.xml & 配置bean類信息

<bean id="userDao" class="org.itstack.demo.bean.RegisterBeanFactory"/>

ApiTest.test_IUserDao() & 單元測試

@Test
public void test_IUserDao() {
    BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
    IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);
    String res = userDao.queryUserInfo();
    logger.info("測試結果:{}", res);
}

測試結果:

信息: Loading XML bean definitions from class path resource [spring-config.xml]
一月 20, 2020 23:42:29 上午 org.springframework.beans.factory.support.DefaultListableBeanFactory registerBeanDefinition
信息: Overriding bean definition for bean 'userDao' with a different definition: replacing [Generic bean: class [org.itstack.demo.bean.RegisterBeanFactory]; scope=; abstract=false; lazyInit=false; autowireMode=1; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in class path resource [spring-config.xml]] with [Generic bean: class [org.itstack.demo.bean.ProxyBeanFactory]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null]
23:42:29.754 [main] INFO  org.itstack.demo.test.ApiTest - 測試結果:你被代理了 queryUserInfo

Process finished with exit code 0

納尼?是不有一種滿腦子都是騷操作的感覺,自己註冊的bean自己知道在哪了,咋回事了。

五、老闆郎上主食呀(mybaits-spring)

如果通過上面的知識點;代理類、bean工廠、bean註冊,將我們一個沒有實現類的接口安排的明明白白,讓他執行啥就執行啥,那麼你是否可以想到,這個沒有實現類的接口,可以通過我們的折騰,去調用到我們的mybaits呢!

如下圖,通過mybatis使用的配置,我們可以看到數據源DataSource交給SqlSessionFactoryBean,SqlSessionFactoryBean實例化出的SqlSessionFactory,再交給MapperScannerConfigurer。而我們要實現的就是MapperScannerConfigurer這部分;

1. 需要實現哪些核心類

為了更易理解也更易於對照,我們將實現mybatis-spring中的流程核心類,如下;

  • MapperFactoryBean {給每一個沒有實現類的接口都代理一個這樣的類,用於操作數據庫執行crud}
  • MapperScannerConfigurer {掃描包下接口類,免去配置。這樣是上圖中核心配置類}
  • SimpleMetadataReader {這個類完全和mybaits-spring中的類一樣,為了解析class文件。如果你對類加載處理很好奇,可以閱讀我的《用java實現jvm虛擬機》}
  • SqlSessionFactoryBean {這個類核心內容就一件事,將我們寫的demo版的mybaits結合進來}

在分析之前先看下我們實現主食是怎麼食用的,如下;

<bean id="sqlSessionFactory" class="org.itstack.demo.like.spring.SqlSessionFactoryBean">
    <property name="resource" value="spring/mybatis-config-datasource.xml"/>
</bean>

<bean class="org.itstack.demo.like.spring.MapperScannerConfigurer">
    <!-- 注入sqlSessionFactory -->
    <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
    <!-- 給出需要掃描Dao接口包 -->
    <property name="basePackage" value="org.itstack.demo.dao"/>
</bean>

2. (類介紹)SqlSessionFactoryBean

這類本身比較簡單,主要實現了FactoryBean , InitializingBean用於幫我們處理mybaits核心流程類的加載處理。(關於demo版的mybaits已經在上文中提供學習鏈接)

SqlSessionFactoryBean.java

public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean {

    private String resource;
    private SqlSessionFactory sqlSessionFactory;

    @Override
    public void afterPropertiesSet() throws Exception {
        try (Reader reader = Resources.getResourceAsReader(resource)) {
            this.sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public SqlSessionFactory getObject() throws Exception {
        return sqlSessionFactory;
    }

    @Override
    public Class<?> getObjectType() {
        return sqlSessionFactory.getClass();
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    public void setResource(String resource) {
        this.resource = resource;
    }

}
  • 實現InitializingBean主要用於加載mybatis相關內容;解析xml、構造SqlSession、鏈接數據庫等
  • FactoryBean,這個類我們介紹過,主要三個方法;getObject()、getObjectType()、isSingleton()

3. (類介紹)MapperScannerConfigurer

這類的內容看上去可能有點多,但是核心事情也就是將我們的dao層接口掃描、註冊

public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor {

    private String basePackage;
    private SqlSessionFactory sqlSessionFactory;

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        try {
            // classpath*:org/itstack/demo/dao/**/*.class
            String packageSearchPath = "classpath*:" + basePackage.replace('.', '/') + "/**/*.class";

            ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
            Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);

            for (Resource resource : resources) {
                MetadataReader metadataReader = new SimpleMetadataReader(resource, ClassUtils.getDefaultClassLoader());

                ScannedGenericBeanDefinition beanDefinition = new ScannedGenericBeanDefinition(metadataReader);
                String beanName = Introspector.decapitalize(ClassUtils.getShortName(beanDefinition.getBeanClassName()));
                
                beanDefinition.setResource(resource);
                beanDefinition.setSource(resource);
                beanDefinition.setScope("singleton");
                beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
                beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(sqlSessionFactory);
                beanDefinition.setBeanClass(MapperFactoryBean.class);

                BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, beanName);
                registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // left intentionally blank
    }

    public void setBasePackage(String basePackage) {
        this.basePackage = basePackage;
    }

    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }
}

  • 類的掃描註冊,classpath:org/itstack/demo/dao/**/.class,解析calss文件獲取資源信息;Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
  • 遍歷Resource,這裏就你的class信息,用於註冊bean。ScannedGenericBeanDefinition
  • 這裡有一點,bean的定義設置時候,是把beanDefinition.setBeanClass(MapperFactoryBean.class);設置進去的。同時在前面給他設置了構造參數。(細細品味)
  • 最後執行註冊registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

4. (類介紹)MapperFactoryBean

這個類就非常有意思了,因為你所有的dao接口類,實際就是他。他這裏幫你執行你對sql的所有操作的分發處理。為了更加簡化清晰,目前這裏只實現了查詢部分,在mybatis-spring源碼中分別對select、update、insert、delete、其他等做了操作。

public class MapperFactoryBean<T> implements FactoryBean<T> {

    private Class<T> mapperInterface;
    private SqlSessionFactory sqlSessionFactory;

    public MapperFactoryBean(Class<T> mapperInterface, SqlSessionFactory sqlSessionFactory) {
        this.mapperInterface = mapperInterface;
        this.sqlSessionFactory = sqlSessionFactory;
    }

    @Override
    public T getObject() throws Exception {
        InvocationHandler handler = (proxy, method, args) -> {
            System.out.println("你被代理了,執行SQL操作!" + method.getName());
            try {
                SqlSession session = sqlSessionFactory.openSession();
                try {
                    return session.selectOne(mapperInterface.getName() + "." + method.getName(), args[0]);
                } finally {
                    session.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

            return method.getReturnType().newInstance();
        };
        return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{mapperInterface}, handler);
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterface;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

}
  • T getObject(),中是一個java代理類的實現,這個代理類對象會被掛到你的注入中。真正調用方法內容時會執行到代理類的實現部分,也就是“你被代理了,執行SQL操作!”

  • InvocationHandler,代理類的實現部分非常簡單,主要開啟SqlSession,並通過固定的key;“org.itstack.demo.dao.IUserDao.queryUserInfoById”執行SQL操作;

    session.selectOne(mapperInterface.getName() + “.” + method.getName(), args[0]);

    <mapper namespace="org.itstack.demo.dao.IUserDao">
    
    	<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User">
    		SELECT id, name, age, createTime, updateTime
    		FROM user
    		where id = #{id}
    	</select>
    	
    </mapper>
    
  • 最終返回了執行結果,關於查詢到結果信息會反射操作成對象類,這部分內容可以遇到demo版本的mybatis

六、倒滿走一個

好!到這一切開發內容就完成了,測試走一個。

mybatis-config-datasource.xml & 數據源配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack_demo_ddd?useUnicode=true"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
        <mapper resource="mapper/School_Mapper.xml"/>
    </mappers>

</configuration>

test-config.xml & 配置xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd     http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"
       default-autowire="byName">
    <context:component-scan base-package="org.itstack"/>

    <aop:aspectj-autoproxy/>

    <bean id="sqlSessionFactory" class="org.itstack.demo.like.spring.SqlSessionFactoryBean">
        <property name="resource" value="spring/mybatis-config-datasource.xml"/>
    </bean>

    <bean class="org.itstack.demo.like.spring.MapperScannerConfigurer">
        <!-- 注入sqlSessionFactory -->
        <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
        <!-- 給出需要掃描Dao接口包 -->
        <property name="basePackage" value="org.itstack.demo.dao"/>
    </bean>

</beans>

SpringTest.java & 單元測試

public class SpringTest {

    private Logger logger = LoggerFactory.getLogger(SpringTest.class);

    @Test
    public void test_ClassPathXmlApplicationContext() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("test-config.xml");
        IUserDao userDao = beanFactory.getBean("IUserDao", IUserDao.class);
        User user = userDao.queryUserInfoById(1L);
        logger.info("測試結果:{}", JSON.toJSONString(user));
    }

}

測試結果;

一月 20, 2020 23:51:43 上午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@30b8a058: startup date [Mon Jan 20 23:51:43 CST 2020]; root of context hierarchy
一月 20, 2020 23:51:43 上午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [test-config.xml]
你被代理了,執行SQL操作!queryUserInfoById
2020-01-20 23:51:45.592 [main] INFO  org.itstack.demo.SpringTest[26] - 測試結果:{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}

Process finished with exit code 0

酒乾熱火笑紅塵,春秋幾載年輪,不問。回首皆是Spring!Gun!變心!你被代理了!

七、綜上總結

  • 通過這些核心關鍵類的實現;SqlSessionFactoryBean、MapperScannerConfigurer、SqlSessionFactoryBean,我們將spring與mybaits集合起來使用,解決了沒有實現類的接口怎麼處理數據庫CRUD操作
  • 那麼這個知識點可以用到哪裡,不要只想着面試!在我們業務開發中是不會有很多其他數據源操作,比如ES、Hadoop、數據中心等等,包括自建。那麼我們就可以做成一套統一數據源處理服務,以優化服務開發效率
  • 由於這次工程類是在itstack-demo-code-mybatis中繼續開發,如果需要獲取源碼可以關注公眾號:bugstack蟲洞棧,回復:源碼分析

八、推薦閱讀

  • 這麼折騰學習畢業進大廠不是問題
  • 工作兩年簡歷寫的差教你優化
  • 講一下我自己的學習路線,給你一些參考
  • 基於Springboot的中間件開發,了解一下

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

※回頭車貨運收費標準

變分(圖)自編碼器不能直接應用於下游任務(GAE, VGAE, AE, VAE and SAE)

    自編碼器是無監督學習領域中一個非常重要的工具。最近由於圖神經網絡的興起,圖自編碼器得到了廣泛的關注。筆者最近在做相關的工作,對科研工作中經常遇到的:自編碼器(AE),變分自編碼器(VAE),圖自編碼器(GAE)和圖變分自編碼器(VGAE)進行了總結。如有不對之處,請多多指正。
    另外,我必須要強調的一點是:很多文章在比較中將自編碼器和變分自編碼器視為一類,我個人認為,這二者的思想完全不同。自編碼器的目的不是為了得到latent representation(中間層),而是為了生成新的樣本。我自己的實驗得出的結論是,變分自編碼器和變分圖自編碼器生成的中間層不能直接用來做下游任務(聚類、分類等),這是一個坑。

自編碼器(AE)

    在解釋圖自編碼器之前,首先理解下什麼是自編碼器。自編碼器的思路來源於傳統的PCA,其目的可以理解為非線性降維。我們知道在傳統的PCA中,學習器學得一個子空間矩陣,將原始數據投影到一個低維子空間,從未達到數據降維的目的。自編碼器則是利用神經網絡將數據逐層降維,每層神經網絡之間的激活函數就起到了將”線性”轉化為”非線性”的作用。自編碼器的網絡結構可以是對稱的也可以是非對稱的。我們下面以一個簡單的四層對稱的自編碼器為例,全文代碼見最後。
   (嚴格的自編碼器是只有一個隱藏層,但是我在這裏做了個拓展,其最大的區別就是隱藏層以及神經元數量的多少,理解一個,其它的都就理解了。)

圖自編碼器(GAE)

    圖自編碼器和自編碼器最大的區別有兩點:一是圖自編碼器在encoder過程中使用了一個 \(n*n\) 的卷積核;另一個是圖自編碼器沒有數據解碼部分,轉而代之的是圖解碼(graph decoder),具體實現是前後鄰接矩陣的變化做loss。
   圖自編碼器可以像自編碼器那樣用來生成隱向量,也可以用來做鏈路預測(應用於推薦任務)。

變分自編碼器(VAE)

   變分自編碼是讓中間層Z服從一個分佈。這樣我們想要生成一個新的樣本的時候,就可以直接在特定分佈中隨機抽取一個樣本。另外,我初學時遇到的疑惑,就是中間層是怎麼符合分佈的。我的理解是:
      輸入樣本:\(\mathbf{X \in \mathcal{R}^{n * d}}\)
      中間層 :\(\mathbf{Z \in \mathcal{R}^{n * m}}\)
   所謂的正態分佈是讓\(Z\)的每一行\(z_i\)符合正態分佈,這樣才能隨機從正態分佈中抽一個新的\(z_i\)出來。但是正是這個原因,我認為\(Z\)不能直接用來處理下游任務(分類、聚類),我自己的實驗確實效果不好。

變分圖自編碼器(VGAE)

    如果你理解了變分比編碼器和圖自編碼器,那麼變分圖自編碼器你也就能理解了。第一個改動就是在VAE的基礎上把encoder過程換成了GCN的卷積過程,另一個改動就是把decoder過程換成了圖decoder過程。同樣生成的中間層隱向量不能直接應用下游任務。
   數據集和下游任務的代碼見: https://github.com/zyx423/GAE-and-VGAE.git

全文代碼如下:

class myAE(torch.nn.Module):
     
    def __init__(self, d_0, d_1, d_2, d_3, d_4):
        super(myAE, self).__init__()
        // 這裏的d0, d_1, d_2, d_3, d_4對應四層神經網絡的維度
    
        self.conv1 = torch.nn.Sequential(
            torch.nn.Linear(d_0, d_1, bias=False),
            torch.nn.ReLU(inplace=True)
        )

        self.conv2 = torch.nn.Sequential(
            torch.nn.Linear(d_1, d_2, bias=False),
            torch.nn.ReLU(inplace=True)
        )

        self.conv3 = torch.nn.Sequential(
            torch.nn.Linear(d_2, d_3, bias=False),
            torch.nn.ReLU(inplace=True)
        )

        self.conv4 = torch.nn.Sequential(
            torch.nn.Linear(d_3, d_4, bias=False),
            torch.nn.Sigmoid()
        )
    def Encoder(self, H_0):
        H_1 = self.conv1(H_0)
        H_2 = self.conv2(H_1)
        return H_2

    def Decoder(self, H_2):
        H_3 = self.conv3(H_2)
        H_4 = self.conv4(H_3)
        return H_4

    def forward(self, H_0):
        Latent_Representation = self.Encoder(H_0)
        Features_Reconstrction = self.Decoder(Latent_Representation)
        return Latent_Representation, Features_Reconstrction

class myGAE(torch.nn.Module):
    def __init__(self, d_0, d_1, d_2):
        super(myGAE, self).__init__()

        self.gconv1 = torch.nn.Sequential(
            torch.nn.Linear(d_0, d_1, bias=False),
            torch.nn.ReLU(inplace=True)
        )
        self.gconv1[0].weight.data = get_weight_initial(d_1, d_0)

        self.gconv2 = torch.nn.Sequential(
            torch.nn.Linear(d_1, d_2, bias=False)
        )
        self.gconv2[0].weight.data = get_weight_initial(d_2, d_1)

    def Encoder(self, Adjacency_Modified, H_0):
        H_1 = self.gconv1(torch.matmul(Adjacency_Modified, H_0))
        H_2 = self.gconv2(torch.matmul(Adjacency_Modified, H_1))
        return H_2

    def Graph_Decoder(self, H_2):
        graph_re = Graph_Construction(H_2)
        Graph_Reconstruction = graph_re.Middle()
        return Graph_Reconstruction


    def forward(self, Adjacency_Modified, H_0):
        Latent_Representation = self.Encoder(Adjacency_Modified, H_0)
        Graph_Reconstruction = self.Graph_Decoder(Latent_Representation)
        return Graph_Reconstruction, Latent_Representation

class myVAE(torch.nn.Module):
    def __init__(self, d_0, d_1, d_2, d_3, d_4, bias=False):
        super(myVAE, self).__init__()

        self.conv1 = torch.nn.Sequential\
        (
            torch.nn.Linear(d_0, d_1, bias= False),
            torch.nn.ReLU(inplace=True)
        )

        # VAE有兩個encoder,一個用來學均值,一個用來學方差
        self.conv2_mean = torch.nn.Sequential(
            torch.nn.Linear(d_1, d_2, bias=False)

        )
        self.conv2_std = torch.nn.Sequential(
            torch.nn.Linear(d_1, d_2, bias=False)
        )
        self.conv3 = torch.nn.Sequential(
            torch.nn.Linear(d_2, d_3, bias=False),
            torch.nn.ReLU(inplace=False)
        )
        self.conv4 = torch.nn.Sequential(
            torch.nn.Linear(d_3, d_4, bias=False),
            torch.nn.Sigmoid()
        )

    def Encoder(self, H_0):
        H_1 = self.conv1(H_0)
        H_2_mean = self.conv2_mean(H_1)
        H_2_std = self.conv2_std(H_1)
        return H_2_mean, H_2_std

    def Reparametrization(self, H_2_mean, H_2_std):
        # sigma = 0.5*exp(log(sigma^2))= 0.5*exp(log(var))
        std = 0.5 * torch.exp(H_2_std)
        # N(mu, std^2) = N(0, 1) * std + mu。
        # 數理統計中的正態分佈方差,剛學過, std是方差。
        # torch.randn 生成正態分佈
        Latent_Representation = torch.randn(std.size()) * std + H_2_mean
        return Latent_Representation

    # 解碼隱變量
    def Decoder(self, Latent_Representation):
        H_3 = self.conv3(Latent_Representation)
        Features_Reconstruction = self.conv4(H_3)
        return Features_Reconstruction

    # 計算重構值和隱變量z的分佈參數
    def forward(self, H_0):
        H_2_mean, H_2_std = self.Encoder(H_0)
        Latent_Representation = self.Reparametrization(H_2_mean, H_2_std)
        Features_Reconstruction = self.Decoder(Latent_Representation)
        return Latent_Representation, Features_Reconstruction, H_2_mean, H_2_std

class myVGAE(torch.nn.Module):
    def __init__(self, d_0, d_1, d_2):
        super(myVGAE, self).__init__()

        self.gconv1 = torch.nn.Sequential(
            torch.nn.Linear(d_0, d_1, bias=False),
            torch.nn.ReLU(inplace=True)
        )
        # self.gconv1[0].weight.data = get_weight_initial(d_1, d_0)

        self.gconv2_mean = torch.nn.Sequential(
            torch.nn.Linear(d_1, d_2, bias=False)
        )
        # self.gconv2_mean[0].weight.data = get_weight_initial(d_2, d_1)

        self.gconv2_std = torch.nn.Sequential(
            torch.nn.Linear(d_1, d_2, bias=False)
        )
        # self.gconv2_std[0].weight.data = get_weight_initial(d_2, d_1)

    def Encoder(self, Adjacency_Modified, H_0):
        H_1 = self.gconv1(torch.matmul(Adjacency_Modified, H_0))
        H_2_mean = self.gconv2_mean(torch.matmul(Adjacency_Modified, H_1))
        H_2_std = self.gconv2_std(torch.matmul(Adjacency_Modified, H_1))
        return H_2_mean, H_2_std

    def Reparametrization(self, H_2_mean, H_2_std):

        # sigma = 0.5*exp(log(sigma^2))= 0.5*exp(log(var))
        std = 0.5 * torch.exp(H_2_std)
        # N(mu, std^2) = N(0, 1) * std + mu。
        # 數理統計中的正態分佈方差,剛學過, std是方差。
        # torch.randn 生成正態分佈
        Latent_Representation = torch.randn(std.size()) * std + H_2_mean
        return Latent_Representation

    # 解碼隱變量
    def Graph_Decoder(self, Latent_Representation):
        graph_re = Graph_Construction(Latent_Representation)
        Graph_Reconstruction = graph_re.Middle()
        return Graph_Reconstruction

    def forward(self, Adjacency_Modified, H_0):
        H_2_mean, H_2_std = self.Encoder(Adjacency_Modified, H_0)
        Latent_Representation = self.Reparametrization(H_2_mean, H_2_std)
        Graph_Reconstruction = self.Graph_Decoder(Latent_Representation)
        return Latent_Representation, Graph_Reconstruction, H_2_mean, H_2_std

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

Python3 源碼閱讀-深入了解Python GIL

今日得到: 三人行,必有我師焉,擇其善者而從之,其不善者而改之。

現在已經是2020年了,而在2010年的時候,大佬David Beazley就做了講座講解Python GIL的設計相關問題,10年間相信也在不斷改善和優化,但是並沒有將GIL從CPython中移除,可想而知,GIL已經深入CPython,難以移除。就目前來看,工作中常用的還是協程,多線程來處理高併發的I/O密集型任務。CPU密集型的大型計算可以用其他語言來實現。

1. GIL

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.) —– Global Interpreter Lock

為了防止多線程共享內存出現競態問題,設置的防止多線程併發執行機器碼的一個Mutex。

2. python32 之前-基於opcode數量的調度方式

在python3.2版本之前,定義了一個tick計數器,表示當前線程在釋放gil之前連續執行的多少個字節碼(實際上有部分執行較快的字節碼並不會被計入計數器)。如果當前的線程正在執行一個 CPU 密集型的任務, 它會在 tick 計數器到達 100 之後就釋放 gil, 給其他線程一個獲得 gil 的機會。

(圖片來自 Understanding the Python GIL(youtube))

以opcode個數為基準來計數,如果有些opcode代碼複雜耗時較長,一些耗時較短,會導致同樣的100個tick,一些線程的執行時間總是執行的比另一些長。是不公平的調度策略。

(圖片來自Understanding-the-python-gil)

如果當前的線程正在執行一個 IO密集型的 的任務, 你執行 sleep/recv/send(...etc) 這些會阻塞的系統調用時, 即使 tick 計數器的值還沒到 100, gil 也會被主動地釋放。至於下次該執行哪一個線程這個是操作系統層面的,線程調度算法優先級調度,開發者沒辦法控制。

在多核機器上, 如果兩個線程都在執行 CPU 密集型的任務, 操作系統有可能讓這兩個線程在不同的核心上運行, 也許會出現以下的情況, 當一個擁有了 gil 的線程在一個核心上執行 100 次 tick 的過程中, 在另一個核心上運行的線程頻繁的進行搶佔 gil, 搶佔失敗的循環, 導致 CPU 瞎忙影響性能。 如下圖:綠色部分表示該線程在運行,且在執行有用的計算,紅色部分為線程被調度喚醒,但是無法獲取GIL導致無法進行有效運算等待的時間。

由圖可見,GIL的存在導致多線程無法很好的利用多核CPU的併發處理能力。

3. python3.2 之後-基於時間片的切換

由於在多核機器下可能導致性能下降, gil的實現在python3.2之後做了一些優化 。python在初始化解釋器的時候就會初始化一個gil,並設置一個DEFAULT_INTERVAL=5000, 單位是微妙,即0.005秒(在 C 裏面是用 微秒 為單位存儲, 在 python 解釋器中以秒來表示)這個間隔就是GIL切換的標誌。

// Python\ceval_gil.h
#define DEFAULT_INTERVAL 5000

static void _gil_initialize(struct _gil_runtime_state *gil)
{
    _Py_atomic_int uninitialized = {-1};
    gil->locked = uninitialized;
    gil->interval = DEFAULT_INTERVAL;
}

python中查看gil切換的時間

In [7]: import sys
In [8]: sys.getswitchinterval()
Out[8]: 0.005

如果當前有不止一個線程, 當前等待 gil 的線程在超過一定時間的等待后, 會把全局變量 gil_drop_request 的值設置為 1, 之後繼續等待相同的時間, 這時擁有 gil 的線程看到了 gil_drop_request 變為 1, 就會主動釋放 gil 並通過 condition variable 通知到在等待中的線程, 第一個被喚醒的等待中的線程會搶到 gil 並執行相應的任務, 將gil_drop_request設置為1的線程不一定能搶到gil

4 condition variable相關字段

  1. locked : locked 的類型是_Py_atomic_int, 值-1表示還未初始化,0表示當前的gil處於釋放狀態,1表示某個線程已經佔用了gil,這個值的類型設置為原子類型之後在 ceval.c 就可以不加鎖的對這個值進行讀取。
  2. interval:是線程在設置gil_drop_request這個變量之前需要等待的時長,默認是5000毫秒
  3. last_holder:存放了最後一個持有 gil 的線程的 C 中對應的 PyThreadState 結構的指針地址, 通過這個值我們可以知道當前線程釋放了 gil 后, 是否有其他線程獲得了 gil(可以採取措施避免被自己重新獲得)
  4. switch_number: 是一個計數器, 表示從解釋器運行到現在, gil 總共被釋放獲得多少次
  5. mutex:是一把互斥鎖, 用來保護 locked, last_holder, switch_number 還有 _gil_runtime_state 中的其他變量
  6. cond:是一個 condition variable, 和 mutex 結合起來一起使用, 當前線程釋放 gil 時用來給其他等待中的線程發送信號
  7. ** switch_cond and switch_mutex**

switch_cond 是另一個 condition variable, 和 switch_mutex 結合起來可以用來保證釋放后重新獲得 gil 的線程不是同一個前面釋放 gil 的線程, 避免 gil 切換時線程未切換浪費 cpu 時間

這個功能如果編譯時未定義 FORCE_SWITCHING 則不開啟

static void
drop_gil(struct _ceval_runtime_state *ceval, PyThreadState *tstate)
{
    ...

#ifdef FORCE_SWITCHING
    if (_Py_atomic_load_relaxed(&ceval->gil_drop_request) && tstate != NULL) {
        MUTEX_LOCK(gil->switch_mutex);
        /* Not switched yet => wait */
        if (((PyThreadState*)_Py_atomic_load_relaxed(&gil->last_holder)) == tstate)
        {   
            /* 如果 last_holder 是當前線程, 釋放 switch_mutex 這把互斥鎖, 等待 switch_cond 這個條件變量的信號 */
            RESET_GIL_DROP_REQUEST(ceval);
            /* NOTE: if COND_WAIT does not atomically start waiting when
               releasing the mutex, another thread can run through, take
               the GIL and drop it again, and reset the condition
               before we even had a chance to wait for it. */
            /* 注意, 如果 COND_WAIT 不在互斥鎖釋放后原子的啟動,
                另一個線程有可能會在這中間拿到 gil 並釋放,
            '並且重置這個條件變量, 這個過程發生在了 COND_WAIT 之前 */
            COND_WAIT(gil->switch_cond, gil->switch_mutex);
        }
        MUTEX_UNLOCK(gil->switch_mutex);
    }
#endif
}

4. gil在main_loop中的體現

//
main_loop:
for (;;) {
    /* 如果 gil_drop_request 被其他線程設置為 1 */
    /* 給其他線程一個獲得 gil 的機會 */
    if (_Py_atomic_load_relaxed(&ceval->gil_drop_request)) {
    /* Give another thread a chance */
    if (_PyThreadState_Swap(&runtime->gilstate, NULL) != tstate) {
        Py_FatalError("ceval: tstate mix-up");
    }
    drop_gil(ceval, tstate);

    /* Other threads may run now */

    take_gil(ceval, tstate);

    /* Check if we should make a quick exit. */
    exit_thread_if_finalizing(runtime, tstate);

    if (_PyThreadState_Swap(&runtime->gilstate, tstate) != NULL) {
        Py_FatalError("ceval: orphan tstate");
        }
    }
    /* Check for asynchronous exceptions. */
    /* 忽略 */
    fast_next_opcode:
    switch (opcode) {
        case TARGET(NOP): {
            FAST_DISPATCH();
        }
        /* 忽略 */
        case TARGET(UNARY_POSITIVE): {
            PyObject *value = TOP();
            PyObject *res = PyNumber_Positive(value);
            Py_DECREF(value);
            SET_TOP(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }
    	/* 忽略 */
    }
    /* 忽略 */
}

這個很大的 for loop 會按順序逐個的加載 opcode, 並委派給中間很大的 switch statement 去進行執行, switch statement 會根據不同的 opcode 跳轉到不同的位置執行

for loop在開始位置會檢查 gil_drop_request變量, 必要的時候會釋放 gil

不是所有的 opcode 執行之前都會檢查 gil_drop_request 的, 有一些 opcode 結束時的代碼為 FAST_DISPATCH(), 這部分 opcode 會直接跳轉到下一個 opcode 對應的代碼的部分進行執行

而另一些 DISPATCH() 結尾的作用和 continue 類似, 會跳轉到 for loop 頂端, 重新檢測 gil_drop_request, 必要時釋放 gil

5 如何解決GIL

GIL只會對CPU密集型的程序產生影響,規避GIL限制主要有兩種常用策略:一是使用多進程,二是使用C語言擴展,把計算密集型的任務轉移到C語言中,使其獨立於Python,在C代碼中釋放GIL。當然也可以使用其他語言編譯的解釋器如 JpythonPyPy

6.總結

  1. Python語言和GIL沒有半毛錢關係,僅僅是由於歷史原因在CPython解釋器中難以移除GIL
  2. GIL:全局解釋器鎖,每個線程在執行的過程都需要先獲取GIL,確保同一時刻僅有一個線程執行代碼,所以python的線程無法利用多核。
  3. 線程在I/O操作等可能引起阻塞的system call之前,可以暫時釋放GIL,執行完畢后重新獲取GIL,python3.2以後使用時間片來切換線程,時間閾值是0.005秒,而python3.2之前是使用opcode執行的數量(tick=100)來切換的。
  4. Python的多線程在多核CPU上,只對於IO密集型計算產生正面效果;而當有至少有一個CPU密集型線程存在,那麼多線程效率會由於GIL而大幅下降

參考

Cpython-gil講解-zpoint

Python的GIL是什麼鬼-盧鈞軼(cenalulu)

Youtube-Understanding the Python GIL

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※回頭車貨運收費標準

台中搬家公司費用怎麼算?

武漢肺炎讓日本人想起311 福島媽媽陷憂鬱:再一次面對「看不見的敵人」

文:宋瑞文(媽媽監督核電廠聯盟特約撰述)

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※回頭車貨運收費標準