hibernate實際開發中用到的東西,其缺點和優點
[color=blue][size=medium]spring在管理hibernate上有獨到的地方可以順手拿來用,我也是想在能不拋棄hibernate的基礎上儘可能多挖掘一下它的一些效能提升上的做法,總結大家的看法,基本得出一致結論:複雜查詢依靠jdbc的sql或者hibernate提供的本地化sql封裝,或者使用spring的管理,都可以提升效能和效率.我認為對於hibernate的效能優化應該是無止境的.基於大家對此的看法意見和網上的一些資料,加上自己的測試,收錄如下,望大家多提寶貴意見:
在專案中使用Hibernate進行大資料量的效能測試,有一些總結,
1) 在處理大資料量時,會有大量的資料緩衝儲存在Session的一級快取中,這快取大太時會嚴重顯示效能,所以在使用Hibernate處理大資料量的,可以使用session.clear()或者session. Evict(Object) 在處理過程中,清除全部的快取或者清除某個物件。
2) 對大資料量查詢時,慎用list()或者iterator()返回查詢結果,
1. 使用List()返回結果時,Hibernate會所有查詢結果初始化為持久化物件,結果集較大時,會佔用很多的處理時間。
2. 而使用iterator()返回結果時,在每次呼叫iterator.next()返回物件並使用物件時,Hibernate才呼叫查詢將對應的物件初始化,對於大資料量時,每呼叫一次查詢都會花費較多的時間。當結果集較大,但是含有較大量相同的資料,或者結果集不是全部都會使用時,使用iterator()才有優勢。
3. 對於大資料量,使用qry.scroll()可以得到較好的處理速度以及效能。而且直接對結果集向前向後滾動。
3) 對於關聯操作,Hibernate雖然可以表達複雜的資料關係,但請慎用,使資料關係較為簡單時會得到較好的效率,特別是較深層次的關聯時,效能會很差。
4) 對含有關聯的PO(持久化物件)時,若default-cascade= "all "或者 “save-update”,新增PO時,請注意對PO中的集合的賦值操作,因為有可能使得多執行一次update操作。
5) 在一對多、多對一的關係中,使用延遲載入機制,會使不少的物件在使用時方會初始化,這樣可使得節省記憶體空間以及減少的負荷,而且若PO中的集合沒有被使用時,就可減少互資料庫的互動從而減少處理時間。 資料庫
什麼叫n+1次select查詢問題?
在Session的快取中存放的是相互關聯的物件圖。預設情況下,當Hibernate從資料庫中載入Customer物件時,會同時載入所有關聯的Order物件。以Customer和Order類為例,假定ORDERS表的CUSTOMER_ID外來鍵允許為null,圖1列出了CUSTOMERS表和ORDERS表中的記錄。
以下Session的find()方法用於到資料庫中檢索所有的Customer物件:
List customerLists=session.find( "from Customer as c ");
執行以上find()方法時,Hibernate將先查詢CUSTOMERS表中所有的記錄,然後根據每條記錄的ID,到ORDERS表中查詢有參照關係的記錄,Hibernate將依次執行以下select語句:
select * from CUSTOMERS;
select * from ORDERS where CUSTOMER_ID=1;
select * from ORDERS where CUSTOMER_ID=2;
select * from ORDERS where CUSTOMER_ID=3;
select * from ORDERS where CUSTOMER_ID=4;
通過以上5條select語句,Hibernate最後載入了4個Customer物件和5個Order物件,在記憶體中形成了一幅關聯的物件圖,參見圖2。
Hibernate在檢索與Customer關聯的Order物件時,使用了預設的立即檢索策略。這種檢索策略存在兩大不足:
(a) select語句的數目太多,需要頻繁的訪問資料庫,會影響檢索效能。如果需要查詢n個Customer物件,那麼必須執行n+1次select查詢語句。這就是經典的n+1次select查詢問題。這種檢索策略沒有利用SQL的連線查詢功能,例如以上5條select語句完全可以通過以下1條select語句來完成:
select * from CUSTOMERS left outer join ORDERS
on CUSTOMERS.ID=ORDERS.CUSTOMER_ID
以上select語句使用了SQL的左外連線查詢功能,能夠在一條select語句中查詢出CUSTOMERS表的所有記錄,以及匹配的ORDERS表的記錄。
(b)在應用邏輯只需要訪問Customer物件,而不需要訪問Order物件的場合,載入Order物件完全是多餘的操作,這些多餘的Order物件白白浪費了許多記憶體空間。
為了解決以上問題,Hibernate提供了其他兩種檢索策略:延遲檢索策略和迫切左外連線檢索策略。延遲檢索策略能避免多餘載入應用程式不需要訪問的關聯物件,迫切左外連線檢索策略則充分利用了SQL的外連線查詢功能,能夠減少select語句的數目。
剛查閱了hibernate3的文件:
查詢抓取(預設的)在N+1查詢的情況下是極其脆弱的,因此我們可能會要求在對映文件中定義使用連線抓取:
<set name= "permissions "
fetch= "join ">
<key column= "userId "/>
<one-to-many class= "Permission "/>
</set
<many-to-one name= "mother " class= "Cat " fetch= "join "/>
在對映文件中定義的抓取策略將會有產生以下影響:
通過get()或load()方法取得資料。
只有在關聯之間進行導航時,才會隱式的取得資料(延遲抓取)。
條件查詢
在對映文件中顯式的宣告 連線抓取做為抓取策略並不會影響到隨後的HQL查詢。
通常情況下,我們並不使用對映文件進行抓取策略的定製。更多的是,保持其預設值,然後在特定的事務中, 使用HQL的左連線抓取(left join fetch) 對其進行過載。這將通知 Hibernate在第一次查詢中使用外部關聯(outer join),直接得到其關聯資料。 在條件查詢 API中,應該呼叫 setFetchMode(FetchMode.JOIN)語句。
6) 對於大資料量新增、修改、刪除操作或者是對大資料量的查詢,與資料庫的互動次數是決定處理時間的最重要因素,減少互動的次數是提升效率的最好途徑,所以在開發過程中,請將show_sql設定為true,深入瞭解Hibernate的處理過程,嘗試不同的方式,可以使得效率提升。
7) Hibernate是以JDBC為基礎,但是Hibernate是對JDBC的優化,其中使用Hibernate的緩衝機制會使效能提升,如使用二級快取以及查詢快取,若命中率較高明,效能會是到大幅提升。
8) Hibernate可以通過設定hibernate.jdbc.fetch_size,hibernate.jdbc.batch_size等屬性,對Hibernate進行優化。
hibernate.jdbc.fetch_size 50
hibernate.jdbc.batch_size 25
這兩個選項非常非常非常重要!!!將嚴重影響Hibernate的CRUD效能!
C = create, R = read, U = update, D = delete
Fetch Size 是設定JDBC的Statement讀取資料的時候每次從資料庫中取出的記錄條數。
例如一次查詢1萬條記錄,對於Oracle的JDBC驅動來說,是不會1次性把1萬條取出來的,而只會取出Fetch Size條數,當紀錄集遍歷完了這些記錄以後,再去資料庫取Fetch Size條資料。
因此大大節省了無謂的記憶體消耗。當然Fetch Size設的越大,讀資料庫的次數越少,速度越快;Fetch Size越小,讀資料庫的次數越多,速度越慢。
這有點像平時我們寫程式寫硬碟檔案一樣,設立一個Buffer,每次寫入Buffer,等Buffer滿了以後,一次寫入硬碟,道理相同。
Oracle資料庫的JDBC驅動預設的Fetch Size=10,是一個非常保守的設定,根據我的測試,當Fetch Size=50的時候,效能會提升1倍之多,當Fetch Size=100,效能還能繼續提升20%,Fetch Size繼續增大,效能提升的就不顯著了。
因此我建議使用Oracle的一定要將Fetch Size設到50。
不過並不是所有的資料庫都支援Fetch Size特性,例如MySQL就不支援。
MySQL就像我上面說的那種最壞的情況,他總是一下就把1萬條記錄完全取出來,記憶體消耗會非常非常驚人!這個情況就沒有什麼好辦法了 :(
Batch Size是設定對資料庫進行批量刪除,批量更新和批量插入的時候的批次大小,有點相當於設定Buffer緩衝區大小的意思。
Batch Size越大,批量操作的向資料庫傳送sql的次數越少,速度就越快。我做的一個測試結果是當Batch Size=0的時候,使用Hibernate對Oracle資料庫刪除1萬條記錄需要25秒,Batch Size = 50的時候,刪除僅僅需要5秒!!!
//
我們通常不會直接操作一個物件的識別符號(identifier),因此識別符號的setter方法應該被宣告為私有的(private)。這樣當一個物件被儲存的時候,只有Hibernate可以為它分配識別符號。你會發現Hibernate可以直接訪問被宣告為public,private和protected等不同級別訪問控制的方法(accessor method)和欄位(field)。 所以選擇哪種方式來訪問屬性是完全取決於你,你可以使你的選擇與你的程式設計相吻合。
所有的持久類(persistent classes)都要求有無參的構造器(no-argument constructor);因為Hibernate必須要使用Java反射機制(Reflection)來例項化物件。構造器(constructor)的訪問控制可以是私有的(private),然而當生成執行時代理(runtime proxy)的時候將要求使用至少是package級別的訪問控制,這樣在沒有位元組碼編入(bytecode instrumentation)的情況下,從持久化類裡獲取資料會更有效率一些。
而
hibernate.max_fetch_depth 設定外連線抓取樹的最大深度
取值. 建議設定為0到3之間
就是每次你在查詢時,會級聯查詢的深度,譬如你對關聯vo設定了eager的話,如果fetch_depth值太小的話,會發多很多條sql
Hibernate的Reference之後,可以採用批量處理的方法,當插入的資料超過10000時,就flush session並且clear。
下面是一個測試method。
1 /** */ /**
2 * 測試成批插入資料的事務處理,返回是否成功
3 *
4 * @param objPO Object
5 * @return boolean
6 */
7 public boolean insertBatch( final Object objPO) {
8 boolean isSuccess = false ;
9 Transaction transaction = null ;
10 Session session = openSession();
11 try {
12 transaction = session.beginTransaction();
13 for ( int i = 0 ; i < 100000 ; i ++ ) {
14 session.save(objPO);
15 if (i % 50 == 0 ) {
16 // flush a batch of inserts and release memory
17 session.flush();
18 session.clear();
19 }
20 }
21 transaction.commit();
22 logger.info( " transaction.wasCommitted: "
23 + transaction.wasCommitted());
24 isSuccess = true ;
25 } catch (HibernateException ex) {
26 if (transaction != null ) {
27 try {
28 transaction.rollback();
29 logger.error( " transaction.wasRolledBack: "
30 + transaction.wasRolledBack());
31 } catch (HibernateException ex1) {
32 logger.error(ex1.getMessage());
33 ex1.printStackTrace();
34 }
35 }
36 logger.error( " Insert Batch PO Error: " + ex.getMessage());
37 ex.printStackTrace();
38 } finally {
39 if (transaction != null ) {
40 transaction = null ;
41 }
42 session.close();
43 }
44 return isSuccess;
45 }
46
這只是簡單的測試,實際專案中遇到的問題,要比這個複雜得多。
這時候,我們可以讓Spring來控制Transaction,自己來控制Hibernate的Session,隨時更新資料。
首先,利用HibernateDaoSupport類來自定義個方法開啟Session;
1public Session openSession(){
2
3 return getHibernateTemplate().getSessionFactory().openSession();
4
5 }
然後,用開啟的Session處理你的資料;
1protected void doBusiness(Session session) {
2
3 while (true) {
4 //do your business with the opening session
5 someMethod(session);
6 session.flush();
7 session.clear();
8 logger.info( "good job! ");
9 }
10}
每做一次資料操作,就更新一次Session,這樣可以保證每次資料操作都成功,否則就讓Spring去控制它roll back吧。
最後,記得關閉Session。
1 Session session = openSession();
2 doBusiness(session);
3 session.close(); // 關閉session [/size][/color]
在專案中使用Hibernate進行大資料量的效能測試,有一些總結,
1) 在處理大資料量時,會有大量的資料緩衝儲存在Session的一級快取中,這快取大太時會嚴重顯示效能,所以在使用Hibernate處理大資料量的,可以使用session.clear()或者session. Evict(Object) 在處理過程中,清除全部的快取或者清除某個物件。
2) 對大資料量查詢時,慎用list()或者iterator()返回查詢結果,
1. 使用List()返回結果時,Hibernate會所有查詢結果初始化為持久化物件,結果集較大時,會佔用很多的處理時間。
2. 而使用iterator()返回結果時,在每次呼叫iterator.next()返回物件並使用物件時,Hibernate才呼叫查詢將對應的物件初始化,對於大資料量時,每呼叫一次查詢都會花費較多的時間。當結果集較大,但是含有較大量相同的資料,或者結果集不是全部都會使用時,使用iterator()才有優勢。
3. 對於大資料量,使用qry.scroll()可以得到較好的處理速度以及效能。而且直接對結果集向前向後滾動。
3) 對於關聯操作,Hibernate雖然可以表達複雜的資料關係,但請慎用,使資料關係較為簡單時會得到較好的效率,特別是較深層次的關聯時,效能會很差。
4) 對含有關聯的PO(持久化物件)時,若default-cascade= "all "或者 “save-update”,新增PO時,請注意對PO中的集合的賦值操作,因為有可能使得多執行一次update操作。
5) 在一對多、多對一的關係中,使用延遲載入機制,會使不少的物件在使用時方會初始化,這樣可使得節省記憶體空間以及減少的負荷,而且若PO中的集合沒有被使用時,就可減少互資料庫的互動從而減少處理時間。 資料庫
什麼叫n+1次select查詢問題?
在Session的快取中存放的是相互關聯的物件圖。預設情況下,當Hibernate從資料庫中載入Customer物件時,會同時載入所有關聯的Order物件。以Customer和Order類為例,假定ORDERS表的CUSTOMER_ID外來鍵允許為null,圖1列出了CUSTOMERS表和ORDERS表中的記錄。
以下Session的find()方法用於到資料庫中檢索所有的Customer物件:
List customerLists=session.find( "from Customer as c ");
執行以上find()方法時,Hibernate將先查詢CUSTOMERS表中所有的記錄,然後根據每條記錄的ID,到ORDERS表中查詢有參照關係的記錄,Hibernate將依次執行以下select語句:
select * from CUSTOMERS;
select * from ORDERS where CUSTOMER_ID=1;
select * from ORDERS where CUSTOMER_ID=2;
select * from ORDERS where CUSTOMER_ID=3;
select * from ORDERS where CUSTOMER_ID=4;
通過以上5條select語句,Hibernate最後載入了4個Customer物件和5個Order物件,在記憶體中形成了一幅關聯的物件圖,參見圖2。
Hibernate在檢索與Customer關聯的Order物件時,使用了預設的立即檢索策略。這種檢索策略存在兩大不足:
(a) select語句的數目太多,需要頻繁的訪問資料庫,會影響檢索效能。如果需要查詢n個Customer物件,那麼必須執行n+1次select查詢語句。這就是經典的n+1次select查詢問題。這種檢索策略沒有利用SQL的連線查詢功能,例如以上5條select語句完全可以通過以下1條select語句來完成:
select * from CUSTOMERS left outer join ORDERS
on CUSTOMERS.ID=ORDERS.CUSTOMER_ID
以上select語句使用了SQL的左外連線查詢功能,能夠在一條select語句中查詢出CUSTOMERS表的所有記錄,以及匹配的ORDERS表的記錄。
(b)在應用邏輯只需要訪問Customer物件,而不需要訪問Order物件的場合,載入Order物件完全是多餘的操作,這些多餘的Order物件白白浪費了許多記憶體空間。
為了解決以上問題,Hibernate提供了其他兩種檢索策略:延遲檢索策略和迫切左外連線檢索策略。延遲檢索策略能避免多餘載入應用程式不需要訪問的關聯物件,迫切左外連線檢索策略則充分利用了SQL的外連線查詢功能,能夠減少select語句的數目。
剛查閱了hibernate3的文件:
查詢抓取(預設的)在N+1查詢的情況下是極其脆弱的,因此我們可能會要求在對映文件中定義使用連線抓取:
<set name= "permissions "
fetch= "join ">
<key column= "userId "/>
<one-to-many class= "Permission "/>
</set
<many-to-one name= "mother " class= "Cat " fetch= "join "/>
在對映文件中定義的抓取策略將會有產生以下影響:
通過get()或load()方法取得資料。
只有在關聯之間進行導航時,才會隱式的取得資料(延遲抓取)。
條件查詢
在對映文件中顯式的宣告 連線抓取做為抓取策略並不會影響到隨後的HQL查詢。
通常情況下,我們並不使用對映文件進行抓取策略的定製。更多的是,保持其預設值,然後在特定的事務中, 使用HQL的左連線抓取(left join fetch) 對其進行過載。這將通知 Hibernate在第一次查詢中使用外部關聯(outer join),直接得到其關聯資料。 在條件查詢 API中,應該呼叫 setFetchMode(FetchMode.JOIN)語句。
6) 對於大資料量新增、修改、刪除操作或者是對大資料量的查詢,與資料庫的互動次數是決定處理時間的最重要因素,減少互動的次數是提升效率的最好途徑,所以在開發過程中,請將show_sql設定為true,深入瞭解Hibernate的處理過程,嘗試不同的方式,可以使得效率提升。
7) Hibernate是以JDBC為基礎,但是Hibernate是對JDBC的優化,其中使用Hibernate的緩衝機制會使效能提升,如使用二級快取以及查詢快取,若命中率較高明,效能會是到大幅提升。
8) Hibernate可以通過設定hibernate.jdbc.fetch_size,hibernate.jdbc.batch_size等屬性,對Hibernate進行優化。
hibernate.jdbc.fetch_size 50
hibernate.jdbc.batch_size 25
這兩個選項非常非常非常重要!!!將嚴重影響Hibernate的CRUD效能!
C = create, R = read, U = update, D = delete
Fetch Size 是設定JDBC的Statement讀取資料的時候每次從資料庫中取出的記錄條數。
例如一次查詢1萬條記錄,對於Oracle的JDBC驅動來說,是不會1次性把1萬條取出來的,而只會取出Fetch Size條數,當紀錄集遍歷完了這些記錄以後,再去資料庫取Fetch Size條資料。
因此大大節省了無謂的記憶體消耗。當然Fetch Size設的越大,讀資料庫的次數越少,速度越快;Fetch Size越小,讀資料庫的次數越多,速度越慢。
這有點像平時我們寫程式寫硬碟檔案一樣,設立一個Buffer,每次寫入Buffer,等Buffer滿了以後,一次寫入硬碟,道理相同。
Oracle資料庫的JDBC驅動預設的Fetch Size=10,是一個非常保守的設定,根據我的測試,當Fetch Size=50的時候,效能會提升1倍之多,當Fetch Size=100,效能還能繼續提升20%,Fetch Size繼續增大,效能提升的就不顯著了。
因此我建議使用Oracle的一定要將Fetch Size設到50。
不過並不是所有的資料庫都支援Fetch Size特性,例如MySQL就不支援。
MySQL就像我上面說的那種最壞的情況,他總是一下就把1萬條記錄完全取出來,記憶體消耗會非常非常驚人!這個情況就沒有什麼好辦法了 :(
Batch Size是設定對資料庫進行批量刪除,批量更新和批量插入的時候的批次大小,有點相當於設定Buffer緩衝區大小的意思。
Batch Size越大,批量操作的向資料庫傳送sql的次數越少,速度就越快。我做的一個測試結果是當Batch Size=0的時候,使用Hibernate對Oracle資料庫刪除1萬條記錄需要25秒,Batch Size = 50的時候,刪除僅僅需要5秒!!!
//
我們通常不會直接操作一個物件的識別符號(identifier),因此識別符號的setter方法應該被宣告為私有的(private)。這樣當一個物件被儲存的時候,只有Hibernate可以為它分配識別符號。你會發現Hibernate可以直接訪問被宣告為public,private和protected等不同級別訪問控制的方法(accessor method)和欄位(field)。 所以選擇哪種方式來訪問屬性是完全取決於你,你可以使你的選擇與你的程式設計相吻合。
所有的持久類(persistent classes)都要求有無參的構造器(no-argument constructor);因為Hibernate必須要使用Java反射機制(Reflection)來例項化物件。構造器(constructor)的訪問控制可以是私有的(private),然而當生成執行時代理(runtime proxy)的時候將要求使用至少是package級別的訪問控制,這樣在沒有位元組碼編入(bytecode instrumentation)的情況下,從持久化類裡獲取資料會更有效率一些。
而
hibernate.max_fetch_depth 設定外連線抓取樹的最大深度
取值. 建議設定為0到3之間
就是每次你在查詢時,會級聯查詢的深度,譬如你對關聯vo設定了eager的話,如果fetch_depth值太小的話,會發多很多條sql
Hibernate的Reference之後,可以採用批量處理的方法,當插入的資料超過10000時,就flush session並且clear。
下面是一個測試method。
1 /** */ /**
2 * 測試成批插入資料的事務處理,返回是否成功
3 *
4 * @param objPO Object
5 * @return boolean
6 */
7 public boolean insertBatch( final Object objPO) {
8 boolean isSuccess = false ;
9 Transaction transaction = null ;
10 Session session = openSession();
11 try {
12 transaction = session.beginTransaction();
13 for ( int i = 0 ; i < 100000 ; i ++ ) {
14 session.save(objPO);
15 if (i % 50 == 0 ) {
16 // flush a batch of inserts and release memory
17 session.flush();
18 session.clear();
19 }
20 }
21 transaction.commit();
22 logger.info( " transaction.wasCommitted: "
23 + transaction.wasCommitted());
24 isSuccess = true ;
25 } catch (HibernateException ex) {
26 if (transaction != null ) {
27 try {
28 transaction.rollback();
29 logger.error( " transaction.wasRolledBack: "
30 + transaction.wasRolledBack());
31 } catch (HibernateException ex1) {
32 logger.error(ex1.getMessage());
33 ex1.printStackTrace();
34 }
35 }
36 logger.error( " Insert Batch PO Error: " + ex.getMessage());
37 ex.printStackTrace();
38 } finally {
39 if (transaction != null ) {
40 transaction = null ;
41 }
42 session.close();
43 }
44 return isSuccess;
45 }
46
這只是簡單的測試,實際專案中遇到的問題,要比這個複雜得多。
這時候,我們可以讓Spring來控制Transaction,自己來控制Hibernate的Session,隨時更新資料。
首先,利用HibernateDaoSupport類來自定義個方法開啟Session;
1public Session openSession(){
2
3 return getHibernateTemplate().getSessionFactory().openSession();
4
5 }
然後,用開啟的Session處理你的資料;
1protected void doBusiness(Session session) {
2
3 while (true) {
4 //do your business with the opening session
5 someMethod(session);
6 session.flush();
7 session.clear();
8 logger.info( "good job! ");
9 }
10}
每做一次資料操作,就更新一次Session,這樣可以保證每次資料操作都成功,否則就讓Spring去控制它roll back吧。
最後,記得關閉Session。
1 Session session = openSession();
2 doBusiness(session);
3 session.close(); // 關閉session [/size][/color]
相關文章
- spring struts hibernate 優缺點?Spring
- 繼承的優點和缺點繼承
- mysql和Oracle的特點,優缺點MySqlOracle
- iframe有哪些優點和缺點?
- 事件委託優缺點和實現事件
- Java單例模式:缺點和優點Java單例模式
- iOS 實際開發中對效能優化的幾點建議iOS優化
- 資料庫索引的作用和優點缺點資料庫索引
- HTTP和RPC的優缺點HTTPRPC
- redis和mongodb各自的優缺點,怎麼選擇?面試時可能會用到RedisMongoDB面試
- 想學 iOS 開發高階一點的東西,從何開始?iOS
- 和程式設計師約會的優點和缺點程式設計師
- Docker的優缺點Docker
- 節點快取的優缺點快取
- MyBatis的優缺點以及特點MyBatis
- 【Java面試】RDB 和 AOF 的實現原理、優缺點Java面試
- 盤點:網際網路大廠端午禮盒的優缺點
- 併發程式設計的優缺點程式設計
- RabbitMQ優缺點MQ
- 執行緒和程式的優缺點執行緒
- SAP各模組優缺點和發展簡析
- vue實際工作用到的知識點Vue
- ERP系統有哪些優點和缺點?
- HTTPS 優點與缺點HTTP
- MySQL觸發器的使用和優缺點介紹ZGMHMySql觸發器
- 近來學習的一點東西
- MySQL索引的優缺點MySql索引
- 繼承的優缺點繼承
- rem佈局原理和優缺點REM
- svn和git版本管理優缺點Git
- 遊戲碎片化敘事的優缺點和實操技巧遊戲
- MySQL垂直拆分和水平拆分的優缺點和共同點總結MySql
- mixins和元件的區別和優缺點元件
- WebApp 開發框架推薦以及優缺點分析WebAPP框架
- SQL、NoSQL和NewSQL的優缺點比較SQL
- TCP和UDP的優缺點及區別TCPUDP
- Webpack4 那點兒東西Web
- 四大主流PHP框架的優點和缺點總結PHP框架