摘要:同時,我們將語句預編譯在中,這個類可以使用占位符,避免注入,當然,后面說到的的占位符的原理也是這樣,同時,的占位符原理也是如此。的底層封裝了,比如說為了防止注入,一般會有占位符,也會有響應的占位符。
介紹jdbc
我們學習Java數據庫操作時,一般會設計到jdbc的操作,這是一位程序員最基本的素養。jdbc以其優美的代碼和高性能,將瞬時態的javabean對象轉化為持久態的SQL數據。但是,每次SQL操作都需要建立和關閉連接,這勢必會消耗大量的資源開銷;如果我們自行創建連接池,假如每個項目都這樣做,勢必搞死的了。同時,我們將SQL語句預編譯在PreparedStatement中,這個類可以使用占位符,避免SQL注入,當然,后面說到的hibernate的占位符的原理也是這樣,同時,mybatis的#{}占位符原理也是如此。預編譯的語句是原生的SQL語句,比如更新語句:
private static int update(Student student) { Connection conn = getConn(); int i = 0; String sql = "update students set Age="" + student.getAge() + "" where Name="" + student.getName() + """; PreparedStatement pstmt; try { pstmt = (PreparedStatement) conn.prepareStatement(sql); i = pstmt.executeUpdate(); System.out.println("resutl: " + i); pstmt.close(); conn.close(); } catch (SQLException e) { e.printStackTrace(); } return i; }
上面的sql語句沒有使用占位符,如果我們少了varchar類型的單引號,就會保存失敗。在這種情況下,如果寫了很多句代碼,最后因為一個單引號,導致代碼失敗,對于程序員來說,無疑是很傷自信心的事。如果涉及到了事務,那么就會非常的麻煩,一旦一個原子操作的語句出現錯誤,那么事務就不會提交,自信心就會跌倒低谷。然而,這僅僅是更新語句,如果是多表聯合查詢語句,那么就要寫很多代碼了。具體的jdbc的操作,可以參考這篇文章:jdbc操作。
因而,我們在肯定它的優點時,也不應該規避他的缺點。隨著工業化步伐的推進,每個數據庫往往涉及到很多表,每張表有可能會關聯到其他表,如果我們還是按照jdbc的方式操作,這無疑是增加了開發效率。所以,有人厭倦這種復雜繁瑣、效率低下的操作,于是,寫出了著名的hibernate框架,封裝了底層的jdbc操作,以下是jdbc的優缺點:
由上圖可以看見,jdbc不適合公司的開發,公司畢竟以最少的開發成本來創造更多的利益。這就出現了痛點,商機伴隨著痛點的出現。因而,應世而生了hibernate這個框架。即便沒有hibernate的框架,也會有其他框架生成。hibernate的底層封裝了jdbc,比如說jdbc為了防止sql注入,一般會有占位符,hibernate也會有響應的占位符。hibernate是orm(object relational mapping)的一種,即對象關系映射。
對象關系映射通俗地來說,對象在pojo中可以指Javabean,關系可以指MySQL的關系型數據庫的表字段與javabean對象屬性的關系。映射可以用我們高中所學的函數映射,即Javabean瞬時態的對象映射到數據庫的持久態的數據對象。我們都知道javabean在內存中的數據是瞬時狀態或游離狀態,就像是宇宙星空中的一顆顆行星,除非行星被其他行星所吸引,才有可能不成為游離的行星。瞬時狀態(游離狀態)的javabean對象的生命周期隨著進程的關閉或者方法的結束而結束。如果當前javabean對象與gcRoots沒有直接或間接的關系,其有可能會被gc回收。我們就沒辦法長久地存儲數據,這是一個非常頭疼的問題。假如我們使用文件來存儲數據,但是文件操作起來非常麻煩,而且數據格式不是很整潔,不利于后期的維護等。因而,橫空出世了數據庫。我們可以使用數據庫存儲數據,數據庫中的數據才是持久數據(數據庫的持久性),除非人為的刪除。這里有個問題——怎么將瞬時狀態(游離狀態)的數據轉化為持久狀態的數據,肯定需要一個連接Javabean和數據庫的橋梁,于是乎就有了上面的jdbc。
多帶帶來說mysql,mysql是一個遠程服務器。我們在向mysql傳輸數據時,并不是對象的方式傳輸,而是以字節碼的方式傳輸數據。為了保證數據準確的傳輸,我們一般會序列化當前對象,用序列號標志這個唯一的對象。如果,我們不想存儲某個屬性,它是有數據庫中的數據拼接而成的,我們大可不用序列化這個屬性,可以使用Transient來修飾。比如下面的獲取圖片的路徑,其實就是服務器圖片的文件夾地址和圖片的名稱拼接而成的。當然,你想存儲這個屬性,也可以存儲這個屬性。我們有時候圖片的路由的字節很長,這樣會占用MySQL的內存。因而,學會取舍,未嘗不是一個明智之舉。
@Entity @Table(name = "core_picture") public class Picture extends BaseTenantObj { 。。。。 @Transient public String getLocaleUrl() { return relativeFolder.endsWith("/") ? relativeFolder + name : relativeFolder + "/" + name; } 。。。。 }
網上流傳盛廣的對象關系映射的框架(orm)有hibernate、mybatis等。重點說說hibernate和mybatis吧。hibernate是墨爾本的一位厭倦重復的javabean的程序員編寫而成的,mybatis是appache旗下的一個子產品,其都是封裝了jdbc底層操作的orm框架。但hibernate更注重javabean與數據表之間的關系,比如我們可以使用注解生成數據表,也可以通過注解的方式設置字段的類型、注釋等。他將javabean分成了游離態、瞬時態、持久態等,hibernate根據這三種狀態來觸及javabean對象的數據。而mybatis更多的是注重SQL語句的書寫,也就是說主要是pojo與SQL語句的數據交互,對此,Hibernate對查詢對象有著良好的管理機制,用戶無需關心SQL。一旦SQL語句的移動,有可能會影響字段的不對應,因而,mybatis移植性沒有hibernate好。mybatis接觸的是底層SQL數據的書寫,hibernate根據javabean的參數來生成SQL語句,再將SQL查詢的結果封裝成pojo,因而,mybatis的性能相來說優于hibernate,但這也不是絕對的。性能還要根據你的表的設計結構、SQL語句的封裝、網絡、帶寬等等。我只是拋磚引玉,它們具體的區別,可以參考這篇文檔。mybatis和hibernate的優缺點。
hibernate和mybatis之間的區別,也是很多公司提問面試者的問題。但是真正熟知他們區別的人,一般是技術選型的架構師。如果你只是負責開發,不需要了解它們的區別,因為他們都封裝了jdbc。所以,你不論使用誰,都還是比較容易的。然而,很多公司的HR提問這種問題很死板,HR并不懂技術,他們只是照本宣科的提問。如果你照本宣科的回答,它們覺著你很厲害。但是,如果是一個懂技術的人提問你,如果你只是臨時背了它們的區別,而沒有相應的工作經驗,他們會問的讓你手足無措。
hibernate講解因為我們公司使用的是hibernate,我在這里簡單地介紹下hibernate。但相對于jdbc來說,hibernate框架還是比較重的。為什么說他重,因為它集成了太多的東西,看如下的hibernate架構圖:
你會發現最上層使我們的java應用程序的開始,比如web的Tomcat服務器的啟動,比如main方法的啟動等。緊接著就是需要(needing)持久化的對象,這里為什么說是需要,而不是持久化的對象。只有保存到文件、數據庫中的數據才是持久化的。想通過hibernate,我們可以毫不費力的將瞬時狀態的數據轉化為持久狀態的數據,下面便是hibernate的內部操作數據。其一般是這樣的流程:
我個人畫的
這個地址的圖片
如果你是用過jdbc連接數據庫的話,我們一般是這樣寫的:
package com.zby.jdbc.config; import com.zby.util.exception.TableException; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; /** * Created By zby on 22:12 2019/1/5 */ public class InitJdbcFactory { private static Properties properties = new Properties(); private static Logger logger = LoggerFactory.getLogger(InitJdbcFactory.class); static { try { //因為使用的類加載器獲取配置文件,因而,配置文件需要放在classpath下面, // 方能讀到數據 properties.load(Thread.currentThread().getContextClassLoader(). getResourceAsStream("./jdbc.properties")); } catch (IOException e) { logger.info("初始化jdbc失敗"); e.printStackTrace(); } } public static Connection createConnection() { String drivers = properties.getProperty("jdbc.driver"); if (StringUtils.isBlank(drivers)) { drivers = "com.mysql.jdbc.Driver"; } String url = properties.getProperty("jdbc.url"); String username = properties.getProperty("jdbc.username"); String password = properties.getProperty("jdbc.password"); try { Class.forName(drivers); return DriverManager.getConnection(url, username, password); } catch (ClassNotFoundException e) { logger.error(InitColTable.class.getName() + ":連接數據庫的找不到驅動類"); throw new TableException(InitColTable.class.getName() + ": 連接數據庫的找不到驅動類", e); } catch (SQLException e) { logger.error(InitColTable.class.getName() + ":連接數據庫的sql異常"); throw new TableException(InitColTable.class.getName() + "連接數據庫的sql異常", e); } } }
hibernate一般這樣連接數據庫:
public class HibernateUtils { private static SessionFactory sf; //靜態初始化 static{ //【1】加載配置文件 Configuration conf = new Configuration().configure(); //【2】 根據Configuration 配置信息創建 SessionFactory sf = conf.buildSessionFactory(); //如果這里使用了hook虛擬機,需要關閉hook虛擬機 Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { @Override public void run() { System.out.println("虛擬機關閉!釋放資源"); sf.close(); } })); } /** * 采用openSession創建一個與數據庫的連接會話,但這種方式需要手動關閉與數據庫的session連接(會話), * 如果不關閉,則當前session占用數據庫的資源無法釋放,最后導致系統崩潰。 * **/ public static org.hibernate.Session openSession(){ //【3】 獲得session Session session = sf.openSession(); return session; } /** * 這種方式連接數據庫,當提交事務時,會自動關閉當前會話; * 同時,創建session連接時,autoCloseSessionEnabled和flushBeforeCompletionEnabled都為true, * 并且session會同sessionFactory組成一個map以sessionFactory為主鍵綁定到當前線程。 * 采用getCurrentSession()需要在Hibernate.cfg.xml配置文件中加入如下配置: 如果是本地事務,及JDBC一個數據庫:thread 如果是全局事務,及jta事務、多個數據庫資源或事務資源:jta 使用spring的getHiberanteTemplate 就不需要考慮事務管理和session關閉的問題: * **/ public static org.hibernate.Session getCurrentSession(){ //【3】 獲得session Session session = sf.getCurrentSession(); return session; } }
mybatis的配置文件:
public class DBTools { public static SqlSessionFactory sessionFactory; static{ try { //使用MyBatis提供的Resources類加載mybatis的配置文件 Reader reader = Resources.getResourceAsReader("mybatis.cfg.xml"); //構建sqlSession的工廠 sessionFactory = new SqlSessionFactoryBuilder().build(reader); } catch (Exception e) { e.printStackTrace(); } } //創建能執行映射文件中sql的sqlSession public static SqlSession getSession(){ return sessionFactory.openSession(); } }
hibernate、mybatis、jdbc創建于數據庫的連接方式雖然不同,但最紅都是為了將瞬時態的數據寫入到數據庫中的,但這里主要說的是hibernate。但是hibernate已經封裝了這些屬性,我們可以在configuration在配置驅動類、用戶名、用戶密碼等。再通過sessionFactory創建session會話,也就是加載Connection的物理連接,創建sql的事務,然后執行一系列的事務操作,如果事務全部成功即可成功,但凡有一個失敗都會失敗。jdbc是最基礎的操作,但是,萬丈高樓平地起,只有基礎打牢,才能走的更遠。因為hibernate封裝了這些基礎,我們操作數據庫不用考慮底層如何實現的,因而,從某種程度上來說,hibernate還是比較重的。
hibernate為什么重(zhong)?比如我們執行插入語句,可以使用save、saveOrUpdate,merge等方法。需要將實體bean通過反射轉化為mysql的識別的SQL語句,同時,查詢雖然用到了反射,但是最后轉化出來的還是object的根對象,這時需要將根對象轉化為當前對象,返回給客戶端。雖然很笨重,但是文件配置好了,可以大大地提高開發效率。畢竟現在的服務器的性能都比較好,公司追求的是高效率的開發,而往往不那么看重性能,除非用戶提出性能的問題。
merge和saveOrUpdatemerge方法與saveOrUpdate從功能上類似,但他們仍有區別。現在有這樣一種情況:我們先通過session的get方法得到一個對象u,然后關掉session,再打開一個session并執行saveOrUpdate(u)。此時我們可以看到拋出異常:Exception in thread "main" org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session,即在session緩存中不允許有兩個id相同的對象。不過若使用merge方法則不會異常,其實從merge的中文意思(合并)我們就可以理解了。我們重點說說merge。merge方法產生的效果和saveOrUpdate方法相似。這是hibernate的原碼:
void saveOrUpdate(Object var1); void saveOrUpdate(String var1, Object var2); public Object merge(Object object); public Object merge(String var1, Object var2);
前者不用返回任何數據,后者返回的是持久化的對象。如果根據hibernate的三種狀態,比如瞬時態、持久態、游離態來說明這個問題,會比較難以理解,現在,根據參數有無id或id是否已經存在來理解merge,而且從執行他們兩個方法而產生的sql語句來看是一樣的。
參數實例對象沒有提供id或提供的id在數據庫找不到對應的行數據,這時merger將執行插入操作嗎,產的SQL語句如下:
Hibernate: select max(uid) from user Hibernate: insert into hibernate1.user (name, age, uid) values (?, ?, ?)
一般情況下,我們新new一個對象,或者從前端向后端傳輸javabean序列化的對象時,都不會存在當前對象的id,如果使用merge的話,就會向數據庫中插入一條數據。
參數實例對象的id在數據庫中已經存在,此時又有兩種情況
(1)如果對象有改動,則執行更新操作,產生sql語句有:
Hibernate: select user0_.uid as uid0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from hibernate1.user user0_ where user0_.uid=? Hibernate: update hibernate1.user set name=?, age=? where uid=?
(2)如果對象未改動,則執行查詢操作,產生的語句有:
Hibernate: select user0_.uid as uid0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from hibernate1.user user0_ where user0_.uid=?
以上三種是什么情況呢?如果我們保存用戶時,數據庫中肯定不存在即將添加的用戶,也就是說,我們的保存用戶就是向數據庫中添加用戶。但是,其也會跟著某些屬性, 比如說用戶需要頭像,這是多對一的關系,一個用戶可能多個對象,然而,頭像的關聯的id不是放在用戶表中的,而是放在用戶擴張表中的,這便用到了切分表的概念。題外話,我們有時會用到快照表,比如商品快照等,也許,我們購買商品時,商品是一個價格,但隨后商品的價格變了,我們需要退商品時,就不應該用到商品改變后的價格了,而是商品改變前的價格。擴展表存放用戶額外的信息,也就是用戶非必須的信息,比如說昵稱,性別,真實姓名,頭像等。 因而,頭像是圖片類型,使用hibernate的注解方式,創建用戶表、圖片表、用戶擴展表。如下所示(部分重要信息已省略)
//用戶頭像 @Entity @Table(name = "core_user") public class User extends BaseTenantConfObj { /** * 擴展表 * */ @OneToOne(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.ALL) private UserExt userExt; } //用戶擴展表的頭像屬性 @Entity @Table(name = "core_user_ext") public class UserExt implements Serializable { /** * 頭像 */ @ManyToOne @JoinColumn(name = "head_logo") private Picture headLogo; } //圖片表 @Entity @Table(name = "core_picture") public class Picture extends BaseTenantObj { private static Logger logger = LoggerFactory.getLogger(Picture.class); 。。。。。。 //圖片存放在第三方的相對url。 @Column(name = "remote_relative_url", length = 300) private String remoteRelativeUrl; // 圖片大小 @Column(length = 8) private Integer size; /** * 圖片所屬類型 * user_logo:用戶頭像 */ @Column(name = "host_type", length = 58) private String hostType; //照片描述 @Column(name = "description", length = 255) private String description; }
前端代碼是:
//這里使用到了vue.js的代碼,v-model數據的雙向綁定,前端的HTML代碼//這里用到了js代碼,這里用到了js的屬性方法 upImg: function(me) { Utils.asyncImg({ fn: function(data) { vm.pageData.userInfo.logo = { path: data.remoteRelativeUrl, id: data.id }; } }); }, 頭像: 推薦尺寸800*800;支持.jpg, .jpeg, .bmp, .png類型文件,1M以內
上傳頭像是異步提交,如果用戶上傳了頭像,我們在提交用戶信息時,通過“headLogo.id”可以獲取當前頭像的持久化的圖片對象,hibernate首先會根據屬性headLogo找到圖片表,根據當前頭像的id找到圖片表中對應的行數據,為什么可以根據id來獲取行數據?
-圖片表的表結構信息
從這張圖片可以看出,圖片采用默認的存儲引擎,也就是InnoDB存儲引擎,而不是myiSam的存儲引擎。
innodb存儲引擎我們都知道這兩種存儲引擎的區別,如果不知道的話,可以參這篇文章MySQL中MyISAM和InnoDB的索引方式以及區別與選擇。innodb采用BTREE樹的數據結構方式存儲,它有0到1直接前繼和0到n個直接后繼,這是什么意思呢?一棵樹當前葉子節點的直接父節點只有一個,但其兒子節點可以一個都沒有,也可以有1個、2個、3個......,如果mysql采用的多對一的方式存儲的話,你就會發現某條外鍵下有許多行數據,比如如下的這張表
這張表記錄的是項目的完成情況,一般有預約階段,合同已簽,合同完成等等。你會發現project_id=163的行數據不止一條,我們通過查詢語句:SELECT zpp.* from zq_project_process zpp WHERE zpp.is_deleted = 0 AND zpp.project_id=163,查找速度非常快。為什么這么快呢,因為我剛開始說的innodb采用的BTREE樹結構存儲,其數據是放在當前索引下,什么意思?innodb的存儲引擎是以索引作為當前節點值,比如說銀行卡表的有個主鍵索引,備注,如果我們沒有創建任何索引,如果采用的innodb的數據引擎,其內部會創建一個默認的行索引,這就像我們在創建javabean對象時,沒有創建構造器,其內部會自動創建一個構造器的道理是一樣的。其數據是怎么存儲的呢,如下圖所示:
mysql銀行卡數據
其內部存儲數據
其所對應的行數據是放在當前索引下的,因而,我們取數據不是取表中的數據,而是取當前主鍵索引下的數據。項目進程表如同銀行卡的主鍵索引,只不過其有三個索引,分別是主鍵索引和兩個外鍵索引,如圖所示的索引:
索引名是hibernate自動生成的一個名字,索引是項目id、類型兩個索引。因為我們不是從表中取數據,而是從當前索引的節點下取數據,所以速度當然快了。索引有主鍵索引、外鍵索引、聯合索引等,但一般情況下,主鍵索引和外鍵索引使用頻率比較高。同時,innodb存儲引擎的支持事務操作,這是非常重要,我們操作數據庫,一般都是設計事務的操作,這也mysql默認的存儲引擎是innodb。
我們通過主鍵獲取圖片的行數據,就像通過主鍵獲取銀行卡的行數據。這也是上面所說的,根據是否有id來確定是插入還是更新數據。通過圖片主鍵id獲取該行數據后,hibernate會在堆中創建一個picture對象。用戶擴展表的headLogo屬性指向這個圖片對象的首地址,從而創建一個持久化的圖片對象。前臺異步提交頭像時,如果是編輯頭像,hibernate會覺擦到當前對象的屬性發生了改變,于是,在提交事務時將修改后的游離態的類保存到數據庫中。如果我們保存或修改用戶時,我們保存的就是持久化的對象,其內部會自動存儲持久化頭像的id。這是hibernate底層所做的,我們不需要關心。
再舉一個hibernate事務提交的例子:
我們在支付當中搞得提現事務時,調用第三方支付的SDK時,第三方一般會用我們到訂單號,比如我們調用連連支付這個第三方支付的SDK的payRequestBean的實體類:
/** * Created By zby on 11:00 2018/12/11 * 發送到連連支付的body內容 */ @Data @AllArgsConstructor @NoArgsConstructor public class PaymentRequestBean extends BaseRequestBean { /** * 版本號 */ @NonNull private String api_version; /** * 銀行賬戶 */ @NonNull private String card_no; /** * 對私 */ @NonNull private String flag_card; /** * 回調接口 */ @NonNull private String notify_url; /** * 商戶訂單號 */ @NonNull private String no_order; /** * 商戶訂單時間,時間格式為 YYYYMMddHHmmss */ @NonNull private String dt_order; /** * 交易金額 */ @NonNull public String money_order; /** * 收款方姓名 即賬戶名 */ @NonNull private String acct_name; /** * 收款銀行姓名 */ private String bank_name; /** * 訂單描述 ,代幣類型 + 支付 */ @NonNull private String info_order; /** * 收款備注 */ private String memo; /** * 支行名稱 */ private String brabank_name; }
商戶訂單號是必傳的,且這個訂單號是我們這邊提供的,這就有一個問題了,怎么避免訂單號不重復呢?我們可以在提現記錄表事先存儲一個訂單號,訂單號的規則如下:"WD" +系統時間+ 當前提現記錄的id,這個id怎么拿到呢?既然底層使用的是merge方法,我們事先不創建訂單號,先保存這個記錄,其返回的是已經創建好的持久化的對象,該持久化的對象肯定有提現主鍵的id。我們拿到該持久化對象的主鍵id,便可以封裝訂單號,再次保存這個持久化的對象,其內部會執行類似以下的操作:
Hibernate: select user0_.uid as uid0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from hibernate1.user user0_ where user0_.uid=? Hibernate: update hibernate1.user set name=?, age=? where uid=?
代碼如下:
withdraw.setWithdrawStatus(WITHDRAW_STATUS_WAIT_PAY); withdraw.setApplyTime(currentTime); withdraw.setExchangeHasThisMember(hasThisMember ? YES : NO); withdraw = withdrawDao.save(withdraw); withdraw.setOrderNo("WD" + DateUtil.ISO_DATETIME_FORMAT_NONE.format(currentTime) + withdraw.getId()); withdrawDao.save(withdraw);
不管哪種情況,merge的返回值都是一個持久化的實例對象,但對于參數而言不會改變它的狀態。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73170.html
摘要:一配置屬性詳解可以在各式各樣不同環境下工作而設計的因此存在著大量的配置參數。以簡便操作,多數配置參數都有默認的配置值也是我們日常使用的必須品。 Hibernate (開放源代碼的對象關系映射框架) Hibernate是一個開放源代碼的對象關系映射框架,它對JDBC進行了非常輕量級的對象封裝, 它將POJO與數據庫表建立映射關系,是一個全自動的orm框架,hibernat...
摘要:不管是還是,表之間的連接查詢,被映射為實體類之間的關聯關系,這樣,如果兩個實體類之間沒有實現關聯關系,你就不能把兩個實體或者表起來查詢。 因為項目需要選擇數據持久化框架,看了一下主要幾個流行的和不流行的框架,對于復雜業務系統,最終的結論是,JOOQ是總體上最好的,可惜不是完全免費,最終選擇JDBC Template。 Hibernate和Mybatis是使用最多的兩個主流框架,而JOO...
摘要:在使用作為應用時推薦使用作為開發工具導入相應的的包到文件下的目錄下關于開發中導入的說明在此提供一個包下載鏈接,地址百度云盤下載好以后解壓到某個文件夾里解壓好以后,開發所需要的包在解壓后的包下,將該文件夾下的包復制到項目中另外還需 1.在使用Hibernate作為orm應用時推薦使用myeclipse作為開發工具2.導入相應的Hibernate的jar包到webroot文件下的lib目錄...
閱讀 1330·2021-11-25 09:43
閱讀 739·2021-11-18 10:02
閱讀 2862·2021-09-07 09:59
閱讀 2748·2021-08-30 09:44
閱讀 2921·2019-08-30 13:17
閱讀 2305·2019-08-29 12:17
閱讀 1673·2019-08-28 17:57
閱讀 1281·2019-08-26 14:04