JPA快速入門(一)

作者:鐘昕靈,叩丁狼教育高級講師。原創文章,轉載請注明出處。

JPA簡介

JPA是Java Persistence API的簡稱,中文名Java持久層API,是JDK 5.0注解或XML描述對象-關系表的映射關系,并將運行期的實體對象持久化到數據庫中。

Sun引入新的JPA ORM規范出于兩個原因:
其一,簡化現有Java EE和Java SE應用開發工作;
其二,Sun希望整合ORM技術,實現天下歸一。

JPA的宗旨是為POJO提供持久化標準規范,由此可見,經過這幾年的實踐探索,能夠脫離容器獨立運行,方便開發和測試的理念已經深入人心了。Hibernate3.2+、TopLink 10.1.3以及OpenJPA都提供了JPA的實現。

JPA的總體思想和現有Hibernate、TopLink、JDO等ORM框架大體一致。總的來說,JPA包括以下3方面的技術:

ORM映射元數據

JPA支持XML和JDK5.0注解兩種元數據的形式,元數據描述對象和表之間的映射關系,框架據此將實體對象持久化到數據庫表中;

API

用來操作實體對象,執行CRUD操作,框架在后臺替代我們完成所有的事情,開發者從繁瑣的JDBC和SQL代碼中解脫出來。

查詢語言

這是持久化操作中很重要的一個方面,通過面向對象而非面向數據庫的查詢語言查詢數據,避免程序的SQL語句緊密耦合。


image.png
JPA開發環境搭建
  • jar包的依賴
    如果是maven項目,將下面的配置添加到pom.xml文件中
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.3</version>
            <configuration>
                <target>1.8</target>
                <source>1.8</source>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
    </plugins>
</build>
<dependencies>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>4.3.5.Final</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>4.3.5.Final</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.21</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.16.6</version>
    </dependency>
</dependencies>

如果是普通的java項目,將下面的jar包添加到項目的lib目錄中


image.png
  • persistence.xml文件
    如果是maven項目,在src/main/resources下創建META-INF文件夾,將persistence.xml文件放在該目錄下
    如果是普通的java項目,在src下創建META-INF文件夾,將persistence.xml文件夾放在該目錄下

在persistence.xml文件中做如下配置

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="2.0">
    <!--
    JPA根據下面的配置信息創建EntityManagerFactory,一個項目中可以配置多個持久單元
    name:為當前持久單元命名,可以通過該名稱指定加載對應的配置信息
-->
    <persistence-unit name="myPersistence">
        <!--指定掃描貼Entity實體類所在的jar包-->
        <properties>
    <!--數據庫的方言,告訴JPA當前應用使用的數據庫-->
            <property name="hibernate.dialect"  value="org.hibernate.dialect.MySQL5Dialect"/>
            <!--jpa的相關的配置信息-->
            <property name="javax.persistence.jdbc.url" value="jdbc:mysql:///jpa"/>
            <property name="javax.persistence.jdbc.driver"  value="com.mysql.jdbc.Driver"/>
            <property name="javax.persistence.jdbc.user" value="root"/>
            <property name="javax.persistence.jdbc.password" value="admin"/>
    <!--是否在控制臺打印執行的sql語句-->
            <property name="hibernate.show_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

到此,開發JPA應用的環境就搭建完成,接下來,在此基礎上來完成基本的CRUD操作吧

基于JPA的CRUD
  • 實體類及映射配置
//getter/setter和toString方法
@Getter@Setter@ToString

//JPA會掃描到貼了Entity注解的類,將其作為需要持久化的類
@Entity
//根據需求,對類和表做相關映射(如:表名)
@Table(name="user")
public class User {
    //標識該字段為主鍵列對應的字段
    @Id
    //指定主鍵的生成策略
    @GeneratedValue(strategy = GenerationType.AUTO)
    //為當前字段和對應的列做映射(如:列名,列的長度等)
    @Column(name = "id")
    private Long id;
    @Column(name = "name",length = 20)
    private String name;
    @Column(name = "sn",nullable = false)
    private String sn;
    //對日期類型做映射
    @Temporal(TemporalType.DATE)
    private Date hiredate;
}
  • EntityManagerFactory和EntityManager對象的創建
  1. EntityManagerFactory:JPA通過加載META-INF/persistence.xml文件中配置的persistence-unit創建EntityManagerFactory對象,該對象相當于一個連接池對象,用來創建EntityManager,是線程安全的,多線程可以共用同一個EntityManagerFactory,創建該對象需要消耗較多的資源,所以通常一個項目只需要創建一個EntityManagerFactory對象
  2. EntityManager:相當于一個連接對象,該對象線程不安全,所以,每次對象數據庫的訪問應該創建一個新的EntityManager對象
public class JPAUtil {

    private static EntityManagerFactory emf;

    private JPAUtil() {}

    static {
        //加載persistence.xml文件中的persistence-util中的配置信息創建EntityManagerFactory對象
        emf = Persistence.createEntityManagerFactory("myPersistence");
    }

    //使用EntityManager創建EntityManager對象
    public static EntityManager getEntityManager() {
        return emf.createEntityManager();
    }
}
  • 保存操作
@Test
public void testSave() throws Exception {
    //封裝需要持久化的數據
    User u = new User();
    u.setName("Neld");
    u.setSn("sn");
    u.setHiredate(new Date());

    EntityManager em = JPAUtil.getEntityManager();
    //開啟事務
    em.getTransaction().begin();
    //執行保存
    em.persist(u);
    //提交事務
    em.getTransaction().commit();
    //釋放資源
    em.close();
}
  • 刪除操作
@Test
public void testDelete() throws Exception {
    EntityManager em = JPAUtil.getEntityManager();
    em.getTransaction().begin();
    User u = em.getReference(User.class, 1L);
//執行刪除,將持久化狀態的對象從數據庫中刪除
    em.remove(u);
    em.getTransaction().commit();
    em.close();
}
  • 修改操作
@Test
public void testUpdate() throws Exception {
    EntityManager em = JPAUtil.getEntityManager();
    em.getTransaction().begin();
    User u = em.find(User.class, 1L);
    u.setName("xxxx");
    em.merge(u);
    em.getTransaction().commit();
    em.close();
}
  • 查詢操作
@Test
public void testGet() throws Exception {
    EntityManager em = JPAUtil.getEntityManager();
    //查詢指定類型和OID的用戶信息
    User u = em.find(User.class, 1L);
    em.close();
    System.out.println(u);
}
  • CRUD小結
  1. persistence.xml文件的配置
    配置連接數據庫的基本信息
    JPA的基本行為配置

  2. 實體類的基本映射
    @Entity:標注該類為持久化類
    JPA掃描到類上的注解,會將當前類作為持久化類
    @Table:配置當前類和表的相關映射
    下面的注解可以貼在字段或者是get方法上,
    如果選定了一個位置,那么所有的屬性相關的注解都應該貼在這個位置,意思是說,不能一部分在字段上,一部分在get方法上
    @Id:主鍵屬性的映射---和表中的主鍵映射
    @GeneratedValue:主鍵生成策略(指定生成主鍵的方式:自增長/手動設置)
    @Column:配置當前屬性和列的映射
    @Temporal:對日期類型的屬性映射(Date/DateTime/TimeStemp)

  3. 完成CRUD的步驟
    加載persistence.xml文件,使用指定的<persistence-unit>配置創建EntityManagerFactory對象,相當于根據配置信息創建一個連接池對象
    創建EntityManager對象,相當于獲取到一個連接對象
    開啟事務
    執行crud相關的方法(persist/merge/remove/find),查詢所有調用Query中的getResultList方法
    Persist:保存數據
    Merge:保存或者更新,當對象有OID的時候,更新,反之,保存
    Remove:刪除數據
    Find:根據主鍵查詢數據
    Query:其他的查詢需要使用該對象,傳入對應的JPQL(相當于SQL),調用getResultList方法執行查詢,返回對應的List集合
    提交事務
    釋放資源

hbm2ddl工具的使用

在持久層應用的開發過程中,我們發現,實體類和表結構是一一對應的,所以,我們會想,是否可以讓JPA根據實體類和對應的映射信息的配置,為我們自動的生成對應的表結構呢?

答案是肯定的,又因為我們現在講的是hibernate對JPA的實現,所以我們應用hibernate中提供的hbm2ddl工具來實現,配置很簡單,在persistence.xml文件中作如下配置即可

<property name="hibernate.hbm2ddl.auto" value="create"/>

接下來,我們來解釋一下每種策略的含義及使用場景

  • hibernate.hbm2ddl.auto=create
    在啟動的時候先刪除被管理的實體對應的表,然后再創建jpa管理的實體類對應的表
  • hibernate.hbm2ddl.auto=create-drop
    和create一致,只是在關閉系統之前會刪除jpa管理的所有的表
  • hibernate.hbm2ddl.auto=update
    在啟動的時候,檢查實體類和表結構是否有變化,如果有,執行更新表結構相關的sql
    如果添加一個屬性,JPA可以幫我們在表中添加對應的列
    如果刪除一個屬性,JPA不會幫我們去表中刪除對應的列
    如果修改一個屬性(類型),JPA不會幫我們去表中刪除對應的列
  • hibernate.hbm2ddl.auto=validate
    在啟動的時候,檢查實體類和表結構是否有變化,如果有,啟動失敗,拋出異常
    Caused by: org.hibernate.HibernateException: Missing column: sn in jpa.user
選擇:
  • 在開發階段,我們通常使用create或者create-drop,可以快速的創建對應的表結構
  • 在測試階段,不要使用create或者create-drop,因為這樣會將我們辛苦錄入的測試數據刪除,所以,我們使用update,在實體類修改的時候,更新表結構即可
  • 在生產環境中,我們通常使用validate,這樣可以在啟動階段發現表結構相關的問題,至于表結構的修改,交給我們的DBA去完成吧.

單對象映射中常用的注解

  • 對象映射相關
  1. @Entity:
    對實體類的映射,默認使用當前類的簡單名稱作為類名,如在使用JPQL做查詢的時候,使用該名字實現數據的查詢
    JPQL語句:SELECT u FROM User u;
    User:為默認使用的類名,可以通過Entity中的name屬性修改
    @Entity(name=”UserInfo”):將類的名稱修改為UserInfo,那么上面的JPQL中的User修改為UserInfo即可

  2. @Table:
    指定實體類映射的表的相關信息,如:表名,默認和類名一致
    @Table(name=”t_user”):將映射的表名修改為t_user

  3. persistence.xml文件中的相關元素的配置說明
    <class>:指定需要掃描的實體類
    <exclude-unlisted-classes>:設置為true的時候,表示不掃描這里沒有列出來的類
    <jar-file>:指定對項目中引入的jar包中的類進行掃描

  • 屬性相關:
  1. @GeneratedValue,主鍵生成策略
    在一張表中,主鍵列的信息通常需要受到程序員的特殊關照,這里我們需要探討一下主鍵的生成方式(自動生成/手動設值)
    首先,我們需要在主鍵屬性上使用@GeneratedValue注解中的strategy屬性來設值主鍵的生成方式

    1. strategy=GenerationType.AUTO
      把主鍵生成策略交給JPA廠商(Persistence Provider),由它根據具體的數據庫選擇合適的策略,可以是Table/Sequence/Identity中的一種。假如數據庫是Oracle,則選擇Sequence。
      如果不做特別指定,默認是使用這種方式生成主鍵

    2. strategy=GenerationType.IDENTITY
      多數數據庫支持IDENTITY,數據庫會在新行插入時自動給ID賦值,這也叫做ID自增長列,比如MySQL中可以在創建表時聲明“AUTO_INCREMENT”,該策略在Oracle數據庫中不支持

    3. strategy=GenerationType.TABLE
      有時候為了不依賴于數據庫的具體實現,在不同數據庫之間更好的移植,可以在數據庫中新建序列表來生成主鍵,序列表一般包含兩個字段:第一個字段引用不 同的關系表,第二個字段是該關系表的最大序號。這樣,只需要一張序列就可以用于多張表的主鍵生成。
      如果不指定表生成器,JPA廠商會使用默認的表,比如Hibernate在Oracle數據庫上會默認使用表hibernate_sequence。
      這種方式雖然通用性最好,所有的關系型數據庫都支持,但是由于不能充分利用具體數據庫的特性,建議不要優先使用。

    4. strategy=GenerationType.SEQUENCE
      Oracle不支持ID自增長列而是使用序列的機制生成主鍵ID,對此,可以選用序列作為主鍵生成策略:
      如果不指定序列生成器的名稱,則使用廠商提供的默認序列生成器,比如Hibernate默認提供的序列名稱為hibernate_sequence。
      支持的數據庫: Oracle、PostgreSQL、DB2
      屬性映射
      @Column:
      使用該注解可以對屬性和列進行相關映射

該注解可以貼在字段上,也可貼在getter方法上,但是必須是統一的,不能一部分在字段上,一部分在getter方法上

  • @Access
    在實際開發中,也可以告訴JPA只去掃描哪個位置上的@Column注解,如果沒有就不在去其他地方掃描
    @Access(AccessType.PROPERTY):屬性,對應著get方法
    @Access(AccessType.FIELD):字段:對應字段

  • @Column
    name:列名,通常,屬性名和列名一直的時候,不需要指定,默認使用屬性名作為列名
    unique:唯一性約束
    nullable:非空約束
    insertable:false,表示在生成insert語句的時候不插入這一列的值
    updatable:false,表示在生成update語句的時候不更新這一列的值
    length:指定該列的長度
    columnDefination:自定義列的類型,默認是JPA根據屬性的類型自動生成
    precision:在使用decimal類型的時候指定總長度
    scale:在使用decimal類型的時候指定小數位數

  • @Temporal:
    日期類型的映射
    指定日期類型的屬性對應的列的類型(date/datatime/timestamp)

  • @Transient:
    非持久化類型的映射
    JPA在做對象關系映射的時候,默認是對實體類中的所有屬性進行映射的,如果有不需要映射的屬性,可以使用該注解完成

  • @Lob:
    大數據類型的映射
    對象如果是String類型的,默認情況下載表中映射的是VARCHAR類型
    該注解可以對應text/blob/clob類型進行映射,如:

@Lob
private String content;

一級緩存

在EntityManager中存在一個緩存區域,稱之為一級緩存

在該緩存區中,會將查詢到的對象緩存到該區域中

如果在同一個EntityManager中,查詢相同OID的數據,那么只需要發送一條sql

在事務提交/關閉EntityManager之后,一級緩存會清空,所以在不同的EntityManager中使用不
同的一級緩存

一級緩存也可以使用下面的方法手動清除緩存數據
detach:清除一級緩存中指定的對象
clear:清除一級緩存中的所有的緩存數據

image.png

但是一級緩存的緩存能力是非常有限的,因為我們不會經常在一個EntityManager中查詢相同的數據
延遲加載

JPA中,根據主鍵查詢數據可以使用下面兩個方法完成:
<T> T find(Class<T> type, Object oid);
<T> T getReference(Class<T> type, Object oid);
相同點:都是根據主鍵查詢指定類型的數據

不同點: getReference方法是在真實使用該對象的時候才會發送查詢的sql語句,如

public void testGetReference() throws Exception {
    EntityManager em = JPAUtil.getEntityManager();
    //這里不會立即發送sql查詢
    User u = em.getReference(User.class, 1L);
    System.out.println("-------------");
    //在訪問User對象中的屬性值的時候表示真正使用該對象
    System.out.println(u.getName());
    em.close();
}

執行結果:

-------------

Hibernate: select user0_.id as id1_0_0_, user0_.hiredate as hiredate2_0_0_, user0_.name             as  name3_0_0_, user0_.sn as sn4_0_0_ from User user0_ where user0_.id=?

Neld

根據執行的打印結果可以看到,是我們在真正使用該對象的時候才會執行查詢的sql,而在這之前是不會發送SQL執行數據的查詢

延遲加載

getReference方法查詢數據的方式我們稱之為延遲加載

什么是延遲加載? 就是不會立即執行查詢的sql,而是延遲到真正使用的時候再執行,上面的例子已經證明了這一點

再觀察:
find方法查詢到的結果,如果查詢到了對應的數據,返回查詢到的結果即可,反之,返回null,所以可以使用ifnull判斷是否有數據
getReference方法查詢到的結果,無論是否查詢到了數據,結果都不會是null,所以不能使用ifnull判斷是否有對應的數據
如果在表中沒有對應的數據,拋出異常
javax.persistence.EntityNotFoundException: Unable to find cn.wolfcode._01_hello.User with id 2

原理:
JPA使用動態代理機制實現延遲加載,覆寫該對象中的所有的getter方法,在getter方法中執行查詢當前對象的sql

延遲加載需要搞懂的問題:
1.延遲加載什么時候發送SQL執行數據?
2.為什么需要在關閉EntityManager對象之前初始化延遲加載對象?
3.為什么在訪問對象的get方法的時候,會去初始化當前對象(發送SQL執行查詢)呢?
4.使用find方法沒有查詢到數據的時候,返回值是什么?使用getReference方法沒有查詢到數據的時候,返回值是什么?

對象狀態

對象的狀態是JPA中非常重要的概念,描述了實體對象從瞬時到持久、從刪除到游離的狀態變換。對實體的操作其實就是對象實體狀態的改變, 這對于我們分析SQL的執行情況有很大的幫助。

  • 瞬時狀態(Transient)
    使用new關鍵字創建出來的新對象,沒有OID,不在一級緩存中
  • 持久狀態(Persistent)
    調用持久化方法之后,將對象保存到數據庫中,對象狀態轉化成持久狀態
  • 游離狀態(Detached)
    對象存在于數據庫中,但是不在一級緩存中
  • 刪除狀態(Removed)
    事務一旦提交,對象就會被從數據庫中刪除,是介于持久狀態和被刪除之間的一個臨界狀態

我們可以通過下面的表格了解到各個狀態的特點:

狀態 是否在一級緩存 是否有OID
瞬時狀態(Transient)
持久狀態(Persistent)
游離狀態(Detached)
刪除狀態(Removed)

EntityManager提供一系列的方法管理實體對象的狀態,包括:

  • persist, 將新創建的或已刪除的實體轉變為Persistent狀態,數據存入數據庫。
  • remove,刪除持久狀態的實體
  • merge,將游離實體轉變為Persistent狀態,數據存入數據庫。

如果使用了事務管理,則事務的commit/rollback也會改變實體的狀態。
如圖:


image.png

有了對對象狀態的了解之后,我們來分析面的案例中sql的發送

@Test
public void test() throws  Exception{
    EntityManager em = JPAUtil.getEntityManager();
    em.getTransaction().begin();
    //通過find方法查詢到處于持久狀態的User對象
    User u = em.find(User.class, 1L);
    u.setName("Lucy");//①
    em.getTransaction().commit();
    em.close();
}

執行結果:
Hibernate: select user0_.id as id1_0_0_, user0_.hiredate as hiredate2_0_0_, user0_.name as name3_0_0_, user0_.sn as sn4_0_0_ from User user0_ where user0_.id=?

Hibernate: update User set hiredate=?, name=?, sn=? where id=?

  • 分析:
    ①:在這里,我們修改了查詢出來處于持久狀態的User對象的name屬性的值

我們并沒有調用merge方法去更新User對象,為什么會發送update語句呢?

  • 原因:
    首先,將數據從數據庫中查詢出來后,在內存中會有兩份數據,一份在EntityManager一級緩存區域,一份在EntityManager的快照區,兩份數據完全一樣

然后,修改User的name屬性時,其實是修改的緩存區的數據

最后,在提交事務的時候,會清理一級緩存,此時會對比兩份數據是否一致,如果不一致,發送對應的update語句將緩存中的臟數據(和數據庫中的數據不一致)同步到數據庫中

所以,在上面的例子中,我們看到執行了一條更新語句,這樣相信大家就能夠理解了,這也是在我們了解了對象的狀態之后對SQL的發送有了更深入的認識

WechatIMG9.jpeg
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372