Cayenne入門指南

Cayenne 是一個 Apache 下的持久層框架項目, 作用與 Hibernate / iBATIS 之類的持久層框架類似.
- 本文譯自 Cayenne 3.1版的官方文檔 ( Cayenne Getting Started Guide, https://cayenne.apache.org/docs/3.1/getting-started-guide/ )
- 譯文的目的是為初學者快速了解 Cayenne 減少一些語言障礙. 若要深入研究和使用 Cayenne 還是建議直接閱讀官方原版文檔.
- 譯者水平有限, 翻譯不準確的地方還望斧正.

另附對Cayenne的相關(guān)知識闡述更全面的 Cayenne Guide 鏈接: https://cayenne.apache.org/docs/3.1/cayenne-guide/

1. 配置環(huán)境

本章的目標是安裝(或檢查您是否已安裝)構(gòu)建Cayenne應(yīng)用程序所需的最低軟件環(huán)境.

1.1 安裝Java

顯然,JDK 必須安裝. Cayenne 3.1需要 JDK 1.5 或 更高版本.

1.2 安裝Eclipse IDE 和 Maven插件

譯注: Maven是一個項目管理工具, 本教程主要使用它來進行項目所依賴的 jar 的管理(自動下載)

下載Eclipse. 本教程基于Galileo JEE版本的Eclipse(Eclipse 3.5), 但它同樣適用于最新的其它Eclipse通用版本. Eclipse下載完后, 解壓并運行之.

對本教程而言, 你唯一需要安裝的插件是m2eclipse.

選擇Eclipse菜單 "Help > Install New Software", 然后點擊 "Add…" 添加一個新的下載站點(download site), 在 "Name" 輸入框中輸入 "Maven", "Location"框中輸入http://m2eclipse.sonatype.org/sites/m2e.

你可以選擇任何你想要的可選組件, 但是對本教程而言, 你只需要選擇最少的基本組件即可, 如下圖:
[站外圖片上傳中...(image-9f7265-1524231454453)]

按照Eclipse對話框中的提示, 完成安裝.

2. 映射(mapping)基礎(chǔ)

2.1. 開始一個項目

本章目標是創(chuàng)建一個包含基本Cayenne映射(Cayenne mapping)的Java項目. 其中展示 CayenneModeler 圖形化工具的使用,
演示如何創(chuàng)建初始的映射對象: DataDomain, DataNode, DataMap.

譯注: CayenneModeler 是 Cayenne的一個圖形化建模工具, 使用此工具可比較直觀且自動化地創(chuàng)建 Cayenne 持久層對象模型所需各類文件

在Eclipse中創(chuàng)建一個新項目

在Eclipse中選擇 "File > New > Other…?", "Maven > Maven Project".
點擊 "Next".

在接下來的界面中選中 "Create a simple project" 復(fù)選框, 再次點擊 "Next".
對話框中顯示如下圖所示內(nèi)容, 填寫 "Group Id" 和 "Artifact Id" 并點擊 "Finish".


image

現(xiàn)在, 在Eclipse的workspace里應(yīng)該有一個空的項目. 檢查一下這個項目的Java編譯設(shè)置是否正確.
右鍵單擊 "tutorial" 項目, 選擇 "Properties > Java Compiler", 確保 "Compiler compliance level"
至少為 "1.5" (一些版本的Maven插件似乎會默認將其設(shè)置為1.4)

下載并運行CayenneModeler

盡管在本教程中稍后我們將使用Maven來導(dǎo)入Cayenne的運行時所需的jar文件到項目中, 你仍然需要下載Cayenne 以便可以使用 CayenneModeler 工具.

如果你直接使用Maven, 你也可以從Maven直接啟動CayenneModeler. 這里我們使用更傳統(tǒng)的文件來做.

下載最新的發(fā)布版. 解壓到任意位置, 根據(jù)特定操作系統(tǒng)平臺的要求啟動 CayenneModeler.

在大多數(shù)平臺下, 只需要簡單地雙擊 Modeler 的圖標即可.
Modeler的歡迎界面如下圖:
[站外圖片上傳中...(image-c9030e-1524231454453)]

在 CayenneModeler 中創(chuàng)建一個新的映射項目(Mapping Project)

在歡迎界面中點擊 New Project 按鈕即會出現(xiàn)一個新的 Mapping 項目, 并包含一個DataDomain.

DataDomain 的含意將會在本教程的其他地方解釋.
現(xiàn)在你只需要知道 DataDomain 是你的Mapping項目的根.

創(chuàng)建一個DataNode

你需要創(chuàng)建的下一個項目對象是 DataNode.

DataNode 是你應(yīng)用程序?qū)⒁B接的單個數(shù)據(jù)庫的描述.
Cayenne 的 mapping 項目可應(yīng)用于多于一個數(shù)據(jù)庫的情況, 但是現(xiàn)在我們僅使用一個數(shù)據(jù)庫.

選中左側(cè)的 "project", 點擊工具欄上的 Create DataNode 按鈕 ( 或者在菜單中選擇Project > Create DateNode ), 一個新的DataNode就出現(xiàn)了.

現(xiàn)在你需要指定JDBC連接參數(shù).
如果使用內(nèi)存型數(shù)據(jù)庫 Derby, 那你可以輸入如下的配置:

  • JDBC Driver: org.apache.derby.jdbc.EmbeddedDriver
  • DB URL: jdbc:derby:memory:testdb;create=true

這里我們創(chuàng)建了一個內(nèi)存型數(shù)據(jù)庫(in-memory database). 因此, 當你停止你的程序時, 所有的數(shù)據(jù)將會丟失. 在更多實際的項目中, 你應(yīng)該會連接一個實際將數(shù)據(jù)存儲于磁盤的數(shù)據(jù)庫, 但是對于這個簡單的教程而言, 我們將使用內(nèi)存數(shù)據(jù)庫.

譯注: 與傳統(tǒng)的數(shù)據(jù)庫(如mysql)不同, 內(nèi)存型數(shù)據(jù)庫可直接將數(shù)據(jù)加載到內(nèi)存中來運行, 可理解為一個直接在內(nèi)存中運行的關(guān)系型數(shù)據(jù)庫. 本教程使用 Derby, 并在 DB URL 處配置 create=true, 這樣可根據(jù) CayenneModeler 建立的模型來自動生成數(shù)據(jù)庫.

同時, 你需要更改 "Schema Update Strategy".

在下拉列框中選擇 org.apache.cayenne.access.dbsync.CreateIfNoSchemaStrategy,
這樣當程序啟動時, Cayenne 將會根據(jù)對象關(guān)系模型映射(ORM mapping)信息在Derby中創(chuàng)建一個新的數(shù)據(jù)庫模式(Schema).
[站外圖片上傳中...(image-798b88-1524231454453)]

創(chuàng)建一個DataMap

現(xiàn)在, 你將要創(chuàng)建一個 DataMap.

DataMap 是一個包含了所有映射信息的對象. 點擊工具欄上的 "Create DataMap" 按鈕 (或選擇相應(yīng)的菜單項).

注意, 新創(chuàng)建的 DataMap 將自動關(guān)聯(lián)到上一步驟中創(chuàng)建的 DataNode. 如果有多于一個DataNode, 你應(yīng)該手動關(guān)聯(lián) DataMap 到正確的 DataNode. 也就是說, 在 DataDomain 中的一個 DataMap 必須通過指定關(guān)聯(lián)來指向一個數(shù)據(jù)庫描述.

在DataMap的配置中, 除了 "Java Package", 你都可以保留DataMap的默認配置.

在 "Java Package" 框中輸入 "org.example.cayenne.persistent". 這個包名將在隨后應(yīng)用于所有的持久層類.

[站外圖片上傳中...(image-fd953-1524231454453)]

保存項目

在你進行實際的映射配置之前, 讓我們先保存一下這個項目.

點擊工具欄上的 "Save" 按鈕, 并指定保存路徑到本章前面創(chuàng)建的名為 "tutorial" 的 Eclipse 項目的文件夾中的子文件夾 "src/main/resources", 并將項目保存在這里.

現(xiàn)在, 回到 Eclipse, 右鍵點擊 "tutorial" 項目, 并選擇 "Refresh(刷新)", 你將看到3個 Cayenne 的 XML 文件. ( 譯注: 原文為3個, 但我只看到2個)

[站外圖片上傳中...(image-130a6f-1524231454453)]

注意, XML文件的存放位置不是隨意的. Cayenne 運行時將在應(yīng)用程序的 CLASSPATH 尋找 cayenne-*.xml 文件.

src/main/resources 文件夾應(yīng)成為Eclipse中我們項目的"class folder".
( 如果我們以命令行方式使用Maven, 那上述位置也是 Maven 復(fù)制 jar 文件的標準目標位置 )

譯注: 按前面一段的步驟保存項目文件到 "src/main/resources" 就行了, 對于 Eclipse 中創(chuàng)建的 Maven 項目 "src/main/resources" 默認就是在CLASSPATH中的. 如果你沒有使用 Maven, 可直接保存到 src 根目錄即可.

2.2. 對象關(guān)系映射入門(ORM)

本節(jié)的目標是學會怎么使用 CayenneModeler 來創(chuàng)建一個簡單的對象關(guān)系模型 ( Object-Relational model, ORM ).

我們將為以下數(shù)據(jù)庫模式創(chuàng)建一個完整的 ORM 模型:

[站外圖片上傳中...(image-a94d08-1524231454453)]

通常情況下, 你已經(jīng)有創(chuàng)建好了的數(shù)據(jù)庫, 那你可以通過菜單 "Tools > Reengineer Database Schema" 將其快速導(dǎo)入到 Cayenne 中. 相比手工映射, 這將節(jié)省你很多時間. 但是, 懂得如何手工創(chuàng)建映射同樣重要,
因此, 我們下面將演示手工操作的方法.

映射數(shù)據(jù)庫表和列

讓我們回到 CayenneModeler, 打開新我們新創(chuàng)建的項目, 并開始添加 ARTIST 表.

在 Cayenne 映射中, 數(shù)據(jù)庫表被稱作 DbEntities ( 可以是實際的表或視圖 ).

在左邊項目樹中選中 "datamap", 點擊工具欄上的 "Create DbEntity" 按鈕 ( 或使用菜單 "Project > Create DbEntity" ), 一個新的 DbEntity 即被創(chuàng)建出來了.

在 "DbEntity Name" 字段輸入 "ARTIST". 然后點擊實體工具欄(entity toolbar, 譯注:就是下圖右側(cè)詳情區(qū)域上方的二級工具欄 ) 上的 "Create Attribute" 按鈕切換到 "Attribute" 標簽頁, 并新增一個名叫 "untitledAttr" 的屬性( Attribute, 這里的 attribute 對應(yīng)一個表中的列 ).

讓我們將其重命名為ID, 并設(shè)置為 INTEGER 和 'PK' ( 主鍵 ):

[站外圖片上傳中...(image-317777-1524231454453)]

類似地, 增加 NAME VARCHAR(200) 和 DATE_OF_BIRTH DATE 屬性.

然后, 重復(fù)上述過程, 創(chuàng)建如前面數(shù)據(jù)庫模式圖中所示的 PAINTING 和 GALLERY 實體.

不要忘記定期保存你的項目, 以免丟失你所做的工作.
因為 Eclipse 默認情況下并不會自動感知建模工具中所做的修改, 所以, 每次 CayenneModeler 保存后,
你都應(yīng)該在 Eclipse 中刷新項目.

映射數(shù)據(jù)庫關(guān)系

現(xiàn)在我們需要指定 ARTIST, PAINTING 和 GALLERY 表之間的 ( 外鍵 ) 關(guān)系.

首先創(chuàng)建一對多的 ARTIST/PAINTING 關(guān)系:

  • 選擇左邊的 ARTIST DbEntity, 并點擊 "Relationship" 標簽頁

  • 點擊實體工具欄上的 "Create Relationship" 按鈕 [站外圖片上傳中...(image-14881-1524231454453)], 一個名為 "untitledRel" 的關(guān)系即被創(chuàng)建出來.

  • 選擇 "Target" 為 "PAINTING"

  • 點擊右側(cè)工具欄上的 "Database Mapping" 按鈕, 即會彈出關(guān)系配置對話框.
    在這里你可以給關(guān)系取個名字, 同樣也可以給反向關(guān)系取名. 這個名字可以任取 ( 這實際上是數(shù)據(jù)庫參考約束的一個符號名稱 ), 但是, 更推薦使用 Java 標識符, 因為稍后這個名字將被以同樣的拼寫方式保存下來. 我們將這個關(guān)系稱作"paintings", 反向
    關(guān)系稱作 "artist".
    譯注: 這里的關(guān)系即 ARTIST 表和 PAINTING 表之間一對多的外鍵約束, 關(guān)系名稱即對于一個 ARTIST 來說 PAINTING 表的數(shù)據(jù)是它的什么, 而反向關(guān)系即: 對于一個 PAINTING 來說, ARTIST 是它的什么. 呵呵, 有點繞~ 簡單說, 對于一個藝術(shù)家( ARTIST ) 而言PAINTING 表中的數(shù)據(jù)是它的畫作, 而對于 PAINTING 表中的一條數(shù)據(jù)而言, ARTIST 表的對應(yīng)數(shù)據(jù)是這幅畫的作者

  • 點擊右邊的 "Add" 按鈕

  • "Source" 選擇 "ID" 列, "Target" 選擇 "ARTIST_ID" 列

  • 關(guān)系信息應(yīng)如下圖所示:

    [站外圖片上傳中...(image-9264b8-1524231454453)]

  • 點擊 "Done" 以確認所做的修改并關(guān)閉對話框.

  • 兩個關(guān)系已經(jīng)被創(chuàng)建: 從 ARTIST 到 PAINTING 的關(guān)系, 以及反向的關(guān)系. 不過你可能注意到有件事
    忘記了: "paintings" 關(guān)系應(yīng)該是 to-many, 但是 "To Many" 復(fù)選框并沒有選中.
    讓我們來改一下: 選中 "paintings" 關(guān)系的 "To Many" 復(fù)選框, 同時, 點擊 PAINTING DBEntity, 取消 "artist" 關(guān)系的 "To Many" 復(fù)選框以設(shè)置反向關(guān)系, 因為反向的 PAINTING 指向 ARTIST 的關(guān)系應(yīng)該是多對一(to-one).

  • 重復(fù)前面的步驟, 以建立從 PAINTING 到 GALLERY 的多對一關(guān)系, 讓我們將這對關(guān)系命名為 "gallery" 和 "paintings".

映射 Java 類

現(xiàn)在, 數(shù)據(jù)庫模式已經(jīng)映射完成, CayenneModeler 可以根據(jù)DbEntity中的所有內(nèi)容來創(chuàng)建Java類的映射(又稱作 "ObjEntity").

目前還不能直接通過一次點擊就完所有的 DataMap 映射, 因此我們將逐個表來做.

  • 選擇 "ARTIST" DbEntity 并點擊實體工具欄或主工具欄上的 "Create ObjEntity" 按鈕 [站外圖片上傳中...(image-d774b3-1524231454453)], 一個名為 "Artist" 的 ObjEntity 即被創(chuàng)建出來, 同時 Java class 輸入框的值被設(shè)置為 "org.example.cayenne.Artist". 建模工具會將數(shù)據(jù)庫中的名稱轉(zhuǎn)換為 Java 風格的名稱 ( 例如: 如果你點擊 "Attributes" 標簽頁, 將看到 "DATA_OF_BIRTH" 列被轉(zhuǎn)換為 Java 類屬性 "dateOfBirth" ).
  • 選擇 "GALLERY" DbEntity 并再次點擊 "Create ObjEntity" 按鈕, 你將看到一個 "Gallery" ObjEntity 被創(chuàng)建出來.
  • 最后, 為 "PAINTING" 做同樣的操作.

現(xiàn)在, 你需要同步關(guān)系. 因為在還沒有相關(guān)聯(lián)的 "Painting" 實體 ( objEntity ) 之前, Artist 和 Gallery 實體就已經(jīng)被創(chuàng)建出來了. 因此, 他們之間的關(guān)系并未被自動設(shè)置.

  • 點擊 "Artist" ObjEntity. 點擊工具欄上的 "Sync ObjEntity with DbEntity" 按鈕, 你會看到出現(xiàn)了 "paintings" 關(guān)系
  • 對 "Gallery" 實體做同樣的操作.

除非你想要自定義 Java 類和屬性名 ( 你可以很容易地做到 ), 映射已經(jīng)完成了.

2.3. 創(chuàng)建Java類

這里我們將根據(jù)前面章節(jié)中創(chuàng)建的模型生成 Java 類.

CayenneModeler 同樣可用來生成數(shù)據(jù)庫模式, 因為在我們先前創(chuàng)建 DataNode 的時候指定了 "CreateIfNoSchemaStrategy", 因此我們將跳過創(chuàng)建數(shù)據(jù)庫模式的步驟. ( 譯注: 因為設(shè)置了CreateIfNoSchemaStrategy策略, 建模工具會自動創(chuàng)建相應(yīng)的數(shù)據(jù)庫模式 )

如果你有需要, 可通過 "Tools > Create Database Schema" 做到這一點 ( 生成數(shù)據(jù)庫模式 ).

創(chuàng)建 Java 類

  • 選擇 "Tools > Generate Classes" 菜單
  • "Type" 選擇 "Standard Persistent Objects" ( 如果沒有選中的話 )
  • "Output Directory" 選擇你項目下的 "src/main/java" 文件夾 ( 這是與之前我們?yōu)?cayenne-*.xml 選擇的位置同等的位置 )
  • 點擊 "Classess" 標簽, 選中 "Check All Classes" 復(fù)選框
  • 點擊 "Generate"

現(xiàn)在回到 Eclipse, 右鍵點擊 "tutorial" 項目選擇 "Refresh" - 你應(yīng)該看到每個被映射的實體生成了兩個類.你可能也注意到, 有一堆紅色的波浪線在 Eclipse 中新出現(xiàn)的 Java 類旁邊.

譯注: 應(yīng)該是紅色的小叉叉吧~

這是因為我們的項目還沒有將 Cayenne 作為 Maven 的依賴包含進來.
讓我們來修復(fù)它, 在 pom.xml 文件的最下面插入 "cayenne-server" artifact.
最終的POM應(yīng)該像這樣:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.example.cayenne</groupId>
    <artifactId>tutorial</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.apache.cayenne</groupId>
            <artifactId>cayenne-server</artifactId>
            <!-- 在這里指定你實際想使用的 Cayenne 版本. 譯注: 建議使用3.1.1這樣的RELEASE版本, 而不是SNAPSHOT版本 -->
            <version>3.1.3-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

你的電腦必須連上Internet.
一旦你保存了 pom.xml, Eclipse 會自動下載 Cayenne 所需的 jar 文件, 并將其添加到項目構(gòu)建路徑 ( build path ).

最終, 所有的錯誤就會消失了.

[站外圖片上傳中...(image-de3854-1524231454453)]

現(xiàn)在, 讓我們來看一下實體類. 每個實體都有一個父類 ( 如: _Artist ) 和一個子類 ( 如: Artist ).

你不應(yīng)該修改名稱以"_" ( 下劃線 ) 開頭的父類, 因為他們將會在后續(xù)生成器運行的時候被覆蓋. 應(yīng)該把所有的自定義邏輯放在 "org.example.cayenne.persistent" 包的子類中(Artist), 這些子類不會被類生成器覆蓋.

類生成提示

通常你會先從 CayenneModeler 生成類, 但是在項目的后期階段, 通常通過Ant cgen task 或 Maven cgen mojo 自動生成代碼. 這三種方法均可, 但 Ant 和 Maven 方法可以確保您不會忘記在映射更改時重新生成類, 因為它們已集成到構(gòu)建周期中.

3. 學習Cayenne API

3.1. ObjectContext 入門

本節(jié)我們將寫一個簡單的 main 類來運行我們的應(yīng)用程序, 并對 Cayenne ObjectContext 作簡單的介紹.

創(chuàng)建 Main 類

  • Eclipse中, 在 "org.example.cayenne" 包中創(chuàng)建一個新的類, 命名為 "Main"
  • 創(chuàng)建一個標準的 "main" 方法, 以使其成為一個可運行的類:
package org.example.cayenne;

public class Main {

    public static void main(String[] args) {
    }
}
  • 要訪問數(shù)據(jù)庫首先要做的是創(chuàng)建一個 ServerRuntime 對象 ( 這實質(zhì)上是對Cayenne的一個封裝 ), 并使用它獲得一個 ObjectContext 的實例.
package org.example.cayenne;

import org.apache.cayenne.ObjectContext;
import org.apache.cayenne.configuration.server.ServerRuntime;

public class Main {

    public static void main(String[] args) {
        ServerRuntime cayenneRuntime = new ServerRuntime(
                        "cayenne-project.xml");
        ObjectContext context = cayenneRuntime.getContext();
    }
}

在 Cayenne 中, ObjectContext 是一個獨立的 "Session", 它提供了處理數(shù)據(jù)所需的所有API.

ObjectContext 擁有執(zhí)行查詢和管理持久層對象的方法. 我們將在后續(xù)的章節(jié)中討論它們.

當應(yīng)用程序中的第一個 ObjectContext 被創(chuàng)建時, Cayenne 將加載 XML 映射文件, 并創(chuàng)建共享的訪問接口, 這將可被后續(xù)創(chuàng)建的其它 ObjectContext 重用.

運行應(yīng)用程序

讓我們來看一下運行程序時發(fā)生了什么.

但是在此之前, 我們需要添加其它的依賴 ( Apache Derby - 我們的嵌入式數(shù)據(jù)庫引擎 ) 到 pom.xml.

下面這段XML代碼需要添加到 <dependencies>…?</dependencies> 部分, 在這里我們之前已經(jīng)添加過 Cayenne 所需的 jar:

<dependency>
   <groupId>org.apache.derby</groupId>
   <artifactId>derby</artifactId>
   <version>10.8.1.2</version>
</dependency>

現(xiàn)在我們可以運行了.

在 Eclipse 中右鍵單擊 "Main" 類, 選擇"Run As > Java Application".

在控制臺你將看到類似如下的輸出. 這表示 Cayenne 已經(jīng)被啟動起來了:

INFO: Loading XML configuration resource from file:cayenne-project.xml
INFO: loading user name and password.
INFO: Created connection pool: jdbc:derby:memory:testdb;create=true
    Driver class: org.apache.derby.jdbc.EmbeddedDriver
    Min. connections in the pool: 1
    Max. connections in the pool: 1

如何配置Cayenne的日志

按照日志一章中的介紹, 調(diào)整日志記錄輸出的詳盡程度.

譯注: 我沒找到所謂"日志一章"

3.2. 開始使用持久層對象

本節(jié)我們將學習關(guān)于持久層對象的知識, 如何定義它們, 如何創(chuàng)建并將其保存到數(shù)據(jù)庫.

檢視和定義持久層對象

在 Cayenne 中持久層類實現(xiàn)了一個數(shù)據(jù)對象 ( DataObject ) 接口.

如果你查看本教程此前生成的任何一個類 ( 如:org.example.cayenne.Artist ), 你會看到它繼承了一個名稱以下劃線開頭的類( 如: org.example.cayenne._Artist), 而這個類又繼承了 org.apache.cayenne.CayenneDataObject.

將每一個持久層類分解為一個用戶自定義子類 ( Xyz ) 和一個自動生成的父類 ( _Xyz ) 是一個很有用的技術(shù), 它將避免在刷新映射模型時覆蓋自定義的代碼.

讓我們來舉個例子, 添加一個工具方法到 Artist 類中, 用于設(shè)置出生日期. 此方法接收一個字符型的日期參數(shù). 即使后續(xù)模型發(fā)生變化, 這個方法亦將被保護 ( 避免被建模工具修改 ) :

public class Artist extends _Artist {

    static final String DEFAULT_DATE_FORMAT = "yyyyMMdd";

    /**
     * Sets date of birth using a string in format yyyyMMdd.
     */
    public void setDateOfBirthString(String yearMonthDay) {
        if (yearMonthDay == null) {
            setDateOfBirth(null);
        } else {
            Date date;
            try {
                date = new SimpleDateFormat(DEFAULT_DATE_FORMAT)
                        .parse(yearMonthDay);
            } catch (ParseException e) {
                throw new IllegalArgumentException(
                        "A date argument must be in format '"
                                + DEFAULT_DATE_FORMAT + "': " + yearMonthDay);
            }
            setDateOfBirth(date);
        }
    }
}

創(chuàng)建新對象

現(xiàn)在我們將創(chuàng)建一組對象, 并將其保存到數(shù)據(jù)庫.

使用 ObjectContext 的"newObject"方法可創(chuàng)建并注冊一個對象.

對象必須被注冊到 DataContext 才能被持久化, 也才能被允許設(shè)置與其它對象的關(guān)系.

添加如下代碼到 Main 類的 "main" 方法中:

Artist picasso = context.newObject(Artist.class);
picasso.setName("Pablo Picasso");
picasso.setDateOfBirthString("18811025");

注意, 此時對象 picasso 僅被存儲于內(nèi)存中, 還未被保存到數(shù)據(jù)庫.

讓我們繼續(xù)添加一個名為 Metropolitan Museum 的 "Gallery" 對象, 和一些畢加索的畫作( Paintings ).

Gallery metropolitan = context.newObject(Gallery.class);
metropolitan.setName("Metropolitan Museum of Art");

Painting girl = context.newObject(Painting.class);
girl.setName("Girl Reading at a Table");

Painting stein = context.newObject(Painting.class);
stein.setName("Gertrude Stein");

現(xiàn)在我們可以把這些對象關(guān)聯(lián)起來, 建立關(guān)系.

注意, 在下面的每一個例子里, 雙向的關(guān)系均被自動建立起來. ( 例如: picasso.addToPaintings(girl) 完全等效于 girl.setToArtist(picasso) ).

picasso.addToPaintings(girl);
picasso.addToPaintings(stein);

girl.setGallery(metropolitan);
stein.setGallery(metropolitan);

現(xiàn)在, 讓我們使用一個方法來同時保存所有的5個對象:

context.commitChanges();

現(xiàn)在, 你可以使用前面章節(jié)中所述的方法的來再次運行程序.

新的輸出將顯示一些實際的數(shù)據(jù)庫操作:

...
org.apache.cayenne.configuration.XMLDataChannelDescriptorLoader load
INFO: Loading XML configuration resource from file:cayenne-project.xml
...
INFO: Opening connection: jdbc:derby:memory:testdb;create=true
    Login: null
    Password: *******
INFO: +++ Connecting: SUCCESS.
INFO: Detected and installed adapter: org.apache.cayenne.dba.derby.DerbyAdapter
INFO: --- transaction started.
INFO: No schema detected, will create mapped tables
INFO: CREATE TABLE GALLERY (ID INTEGER NOT NULL, NAME VARCHAR (200), PRIMARY KEY (ID))
INFO: CREATE TABLE ARTIST (DATE_OF_BIRTH DATE, ID INTEGER NOT NULL, NAME VARCHAR (200), PRIMARY KEY (ID))
INFO: CREATE TABLE PAINTING (ARTIST_ID INTEGER, GALLERY_ID INTEGER, ID INTEGER NOT NULL,
      NAME VARCHAR (200), PRIMARY KEY (ID))
INFO: ALTER TABLE PAINTING ADD FOREIGN KEY (ARTIST_ID) REFERENCES ARTIST (ID)
INFO: ALTER TABLE PAINTING ADD FOREIGN KEY (GALLERY_ID) REFERENCES GALLERY (ID)
INFO: CREATE TABLE AUTO_PK_SUPPORT (
      TABLE_NAME CHAR(100) NOT NULL,  NEXT_ID BIGINT NOT NULL,  PRIMARY KEY(TABLE_NAME))
INFO: DELETE FROM AUTO_PK_SUPPORT WHERE TABLE_NAME IN ('ARTIST', 'GALLERY', 'PAINTING')
INFO: INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('ARTIST', 200)
INFO: INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('GALLERY', 200)
INFO: INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('PAINTING', 200)
INFO: SELECT NEXT_ID FROM AUTO_PK_SUPPORT WHERE TABLE_NAME = ? FOR UPDATE [bind: 1:'ARTIST']
INFO: SELECT NEXT_ID FROM AUTO_PK_SUPPORT WHERE TABLE_NAME = ? FOR UPDATE [bind: 1:'GALLERY']
INFO: SELECT NEXT_ID FROM AUTO_PK_SUPPORT WHERE TABLE_NAME = ? FOR UPDATE [bind: 1:'PAINTING']
INFO: INSERT INTO GALLERY (ID, NAME) VALUES (?, ?)
INFO: [batch bind: 1->ID:200, 2->NAME:'Metropolitan Museum of Art']
INFO: === updated 1 row.
INFO: INSERT INTO ARTIST (DATE_OF_BIRTH, ID, NAME) VALUES (?, ?, ?)
INFO: [batch bind: 1->DATE_OF_BIRTH:'1881-10-25 00:00:00.0', 2->ID:200, 3->NAME:'Pablo Picasso']
INFO: === updated 1 row.
INFO: INSERT INTO PAINTING (ARTIST_ID, GALLERY_ID, ID, NAME) VALUES (?, ?, ?, ?)
INFO: [batch bind: 1->ARTIST_ID:200, 2->GALLERY_ID:200, 3->ID:200, 4->NAME:'Gertrude Stein']
INFO: [batch bind: 1->ARTIST_ID:200, 2->GALLERY_ID:200, 3->ID:201, 4->NAME:'Girl Reading at a Table']
INFO: === updated 2 rows.
INFO: +++ transaction committed.

Cayenne 創(chuàng)建了必要的表 ( 記住, 我們使用了 "CreateIfNoSchemaStrategy").

然后它運行一些插入, 即時生成了主鍵.

就這么幾行代碼就搞成這樣還不賴.

3.3. 檢索對象

本節(jié)演示如何使用 ObjectSelect 來從數(shù)據(jù)庫中檢索 ( 查詢 ) 對象.

ObjectSelect 介紹

前面已經(jīng)展示了如何持久化新的對象.

Cayenne 的 query 被用來訪問已經(jīng)保存的對象.

用于檢索對象的主要查詢類型是 ObjectSelect. 它可以直接在 CayenneModeler 中進行映射, 也可以通過 API 創(chuàng)建. 本節(jié)我們將使用后一種方法.

雖然我們還沒有太多的數(shù)據(jù)在數(shù)據(jù)庫中, 但是我們?nèi)钥梢匝菔救缦碌闹饕椒?

  • 檢索所有的畫作 ( 代碼 及 產(chǎn)生的日志輸出 ):
SelectQuery select1 = new SelectQuery(Painting.class);
List paintings1 = context.performQuery(select1);
INFO: SELECT t0.GALLERY_ID, t0.ARTIST_ID, t0.NAME, t0.ID FROM PAINTING t0
INFO: === returned 2 rows. - took 18 ms.
  • 檢索以 "gi" 開頭的畫作, 忽略大小寫:
Expression qualifier2 = ExpressionFactory.likeIgnoreCaseExp(
                Painting.NAME_PROPERTY,
                "gi%");
SelectQuery select2 = new SelectQuery(Painting.class, qualifier2);
List paintings2 = context.performQuery(select2);
INFO: SELECT t0.GALLERY_ID, t0.NAME, t0.ARTIST_ID, t0.ID FROM PAINTING t0 WHERE UPPER(t0.NAME) LIKE UPPER(?)
      [bind: 1->NAME:'gi%'] - prepared in 6 ms.
INFO: === returned 1 row. - took 18 ms.
  • 檢索所有100年前出生的藝術(shù)家的畫作 ( 演示使用 Expression.fromString(..) 而不是 ExpressionFactory ):
Calendar c = new GregorianCalendar();
c.set(c.get(Calendar.YEAR) - 100, 0, 1, 0, 0, 0);

Expression qualifier3 = Expression.fromString("artist.dateOfBirth < $date");
qualifier3 = qualifier3.expWithParameters(Collections.singletonMap("date", c.getTime()));
SelectQuery select3 = new SelectQuery(Painting.class, qualifier3);
List paintings3 = context.performQuery(select3);
INFO: SELECT t0.GALLERY_ID, t0.NAME, t0.ARTIST_ID, t0.ID FROM PAINTING t0 JOIN ARTIST t1 ON (t0.ARTIST_ID = t1.ID)
      WHERE t1.DATE_OF_BIRTH < ? [bind: 1->DATE_OF_BIRTH:'1911-01-01 00:00:00.493'] - prepared in 7 ms.
INFO: === returned 2 rows. - took 25 ms.

3.4. 刪除對象

本節(jié)解釋如何建立關(guān)系的刪除約束模型, 如何刪除單個對象和一組對象.

同時, 也將演示執(zhí)行查詢時 Cayenne 類的使用.

設(shè)置刪除約束

在我們討論刪除對象的 API 前, 讓我們回到 CayenneModeler, 進行一些刪除約束的設(shè)置.

這樣做是可選的 ( 不是必須的 ) , 但它將使得我們可以以簡單的方式正確處理與被刪除對象相關(guān)聯(lián)的其它對象.

在建模工具中轉(zhuǎn)到 "Artist" ObjEntity 的 "Relationships" 標簽頁, 為 "paintings" 關(guān)系選擇 "Cascade" ( 級聯(lián) ) 刪除約束:

[站外圖片上傳中...(image-a4dd47-1524231454453)]

為其它關(guān)系重復(fù)上述步驟:

  • 為 Gallery 設(shè)置 "paintings" 關(guān)系為 "Nullify", 因為可以存在一副畫作未在任何畫廊展出的情況.
  • 為 Painting 設(shè)置其兩個關(guān)系的刪除約束均為 "Nullify".

現(xiàn)在, 保存映射.

刪除對象

雖然可以通過SQL刪除對象. 但在Cayenne( 或一般的ORM )中更常用的方法是首先獲取對象, 然后通過 context 刪除它.

讓我們使用 Cayenne 的工具類找到一個藝術(shù)家:

Expression qualifier = ExpressionFactory.matchExp(Artist.NAME_PROPERTY, "Pablo Picasso");
SelectQuery select = new SelectQuery(Artist.class, qualifier);
Artist picasso = (Artist) Cayenne.objectForQuery(context, select);

現(xiàn)在, 讓我們刪除這個藝術(shù)家:

if (picasso != null) {
    context.deleteObject(picasso);
    context.commitChanges();
}

因為我們?yōu)?Artist.paintings 關(guān)系設(shè)置了 "Cascade" 刪除約束, Cayenne會自動刪除這個藝術(shù)家所有的畫作.

因此, 當我們運行這個程序時, 你將會看到如下輸出:

INFO: SELECT t0.DATE_OF_BIRTH, t0.NAME, t0.ID FROM ARTIST t0
      WHERE t0.NAME = ? [bind: 1->NAME:'Pablo Picasso'] - prepared in 6 ms.
INFO: === returned 1 row. - took 18 ms.
INFO: +++ transaction committed.
INFO: --- transaction started.
INFO: DELETE FROM PAINTING WHERE ID = ?
INFO: [batch bind: 1->ID:200]
INFO: [batch bind: 1->ID:201]
INFO: === updated 2 rows.
INFO: DELETE FROM ARTIST WHERE ID = ?
INFO: [batch bind: 1->ID:200]
INFO: === updated 1 row.
INFO: +++ transaction committed.

4. 轉(zhuǎn)換為Web應(yīng)用程序

本章將展示 Cayenne 如何在Web應(yīng)用程序中工作

4.1. 將 tutorial 項目轉(zhuǎn)換為 Web應(yīng)用程序

Web應(yīng)用程序教程的 Web 部分是在 JSP 中完成的, 而 JSP 是 Java Web 技術(shù)中最常見的實現(xiàn)方法.

本教程在 UI 方面盡可能地簡單, 主要專注于 Cayenne 集成, 而不是界面.

一個典型的 Cayenne Web 應(yīng)用程序像下面這樣工作:

  • 在應(yīng)用程序上下文啟動時, 使用一個特定的 Servlet 過濾器加載 Cayenne 的配置
  • 用戶請求被過濾器攔截, 并將 DataContext 綁定到請求線程, 因此應(yīng)用程序可以從任何地方輕松訪問它.
  • 同一個 DataContext 實例在單個用戶會話 ( Session ) 中被重用; 不同的會話使用不同的 DataContexts ( 以及不同的對象集). 根據(jù)應(yīng)用的具體情況, 上下文可以有不同的范圍. 本教程中我們將使用會話范圍的上下文 ( Context ).

譯注: 每一個 DataContext 是一個獨立的數(shù)據(jù)上下文空間, 其中包含了從數(shù)據(jù)庫中調(diào)入的 ( 和新創(chuàng)建尚未持久化的 ) 數(shù)據(jù)對象集合. 在實際 Web 項目中如果像這樣將 DataContext 綁定到用戶會話級, 在多用戶并發(fā)時可能導(dǎo)致內(nèi)存中有很多冗余的數(shù)據(jù)對象. 也有可能出現(xiàn)多個用戶分別從數(shù)據(jù)庫檢索同一數(shù)據(jù)后, 其中一個用戶修改了數(shù)據(jù), 導(dǎo)致其它用戶持有的數(shù)據(jù)對象是 "臟數(shù)據(jù)". 所以實際 Web 項目中本例所述的做法有待斟酌...

讓我們將我們此前創(chuàng)建的 tutorial 項目轉(zhuǎn)換為一個 Web 應(yīng)用程序:

  • 在Eclipse中的 "tutorial" 項目下創(chuàng)建一個新的文件夾 "src/main/webapp/WEB-INF".
  • WEB-INF 下創(chuàng)建一個新文件 web.xml ( 一個標準的Web應(yīng)用程序描述文件 ):

web.xml

<?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE web-app
   PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
  "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
    <display-name>Cayenne Tutorial</display-name>

    <!-- This filter bootstraps ServerRuntime and then provides each request thread
         with a session-bound DataContext. Note that the name of the filter is important,
         as it points it to the right named configuration file.
    -->
    <filter>
        <filter-name>cayenne-project</filter-name>
        <filter-class>org.apache.cayenne.configuration.web.CayenneFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>cayenne-project</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>
  • 創(chuàng)建一個藝術(shù)家瀏覽頁面 src/main/webapp/index.jsp, 包含如下內(nèi)容:

webapp/index.jsp

<%@ page language="java" contentType="text/html" %>
<%@ page import="org.example.cayenne.persistent.*" %>
<%@ page import="org.apache.cayenne.*" %>
<%@ page import="org.apache.cayenne.query.*" %>
<%@ page import="org.apache.cayenne.exp.*" %>
<%@ page import="java.util.*" %>

<%
    SelectQuery query = new SelectQuery(Artist.class);
    query.addOrdering(Artist.NAME_PROPERTY, SortOrder.ASCENDING);

    ObjectContext context = BaseContext.getThreadObjectContext();
    List<Artist> artists = context.performQuery(query);
%>
<html>
    <head>
        <title>Main</title>
    </head>
    <body>
        <h2>Artists:</h2>

        <% if(artists.isEmpty()) {%>
        <p>No artists found</p>
        <% } else {
            for(Artist a : artists) {
        %>
        <p><a href="detail.jsp?id=<%=Cayenne.intPKForObject(a)%>"> <%=a.getName()%> </a></p>
        <%
            }
            } %>
        <hr>
        <p><a href="detail.jsp">Create new artist...</a></p>
    </body>
</html>
  • 創(chuàng)建一個藝術(shù)家編輯頁面 src/main/webapp/detail.jsp, 包含如下內(nèi)容:

webapp/detail.jsp

<%@ page language="java" contentType="text/html" %>
<%@ page import="org.example.cayenne.persistent.*" %>
<%@ page import="org.apache.cayenne.*" %>
<%@ page import="java.util.*" %>
<%@ page import="java.text.*" %>

<%
    ObjectContext context = BaseContext.getThreadObjectContext();
    String id = request.getParameter("id");

    // find artist for id
    Artist artist = null;
    if(id != null && id.trim().length() > 0) {
        artist = Cayenne.objectForPK(context, Artist.class, Integer.parseInt(id));
    }

    if("POST".equals(request.getMethod())) {
        // if no id is saved in the hidden field, we are dealing with
        // create new artist request
        if(artist == null) {
            artist = context.newObject(Artist.class);
        }

        // note that in a real application we would so dome validation ...
        // here we just hope the input is correct
        artist.setName(request.getParameter("name"));
        artist.setDateOfBirthString(request.getParameter("dateOfBirth"));

        context.commitChanges();

        response.sendRedirect("index.jsp");
    }

    if(artist == null) {
        // create transient artist for the form response rendering
        artist = new Artist();
    }

    String name = artist.getName() == null ? "" : artist.getName();
    String dob = artist.getDateOfBirth() == null
            ? "" : new SimpleDateFormat("yyyyMMdd").format(artist.getDateOfBirth());
%>
<html>
    <head>
        <title>Artist Details</title>
    </head>
    <body>
        <h2>Artists Details</h2>
        <form name="EditArtist" action="detail.jsp" method="POST">
            <input type="hidden" name="id" value="<%= id != null ? id : "" %>" />
            <table border="0">
                <tr>
                    <td>Name:</td>
                    <td><input type="text" name="name" value="<%= name %>"/></td>
                </tr>
                <tr>
                    <td>Date of Birth (yyyyMMdd):</td>
                    <td><input type="text" name="dateOfBirth" value="<%= dob %>"/></td>
                </tr>
                <tr>
                    <td></td>
                    <td align="right"><input type="submit" value="Save" /></td>
                </tr>
            </table>
        </form>
    </body>
</html>

運行Web應(yīng)用程序

為了運行這個Web應(yīng)用程序, 我們將使用 "maven-jetty-plugin".

譯注: Jetty 是一個 Web 應(yīng)用程序容器, 作用類似 Tomcat.

為了激活它, 讓我們添加如下的代碼到 "pom.xml" 中, 跟在 "dependencies" 部分的后面, 保存POM.

pom.xml

<build>
    <plugins>
        <plugin>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-maven-plugin</artifactId>
            <version>9.3.14.v20161028</version>
        </plugin>
    </plugins>
</build>
  • 打開 "Run > Run Configurations…?" 菜單, 選擇 "Maven Build", 點擊右鍵并選擇 "New"
  • 確定你填寫了 "Name", "Base directory" 和 "Goals", 如下圖:

[站外圖片上傳中...(image-83c748-1524231454453)]

  • 依次點擊 "Apply" 和 "Run".

首次運行時可能會花費幾分鐘下載Jetty插件所有的依賴,
但是最終你將看到如下的日志:

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building tutorial 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
...
[INFO] Configuring Jetty for project: tutorial
[INFO] Webapp source directory = /.../tutorial/src/main/webapp
[INFO] Reload Mechanic: automatic
[INFO] Classes = /.../tutorial/target/classes
[INFO] Context path = /tutorial
[INFO] Tmp directory =  determined at runtime
[INFO] Web defaults = org/mortbay/jetty/webapp/webdefault.xml
[INFO] Web overrides =  none
[INFO] web.xml file = /.../tutorial/src/main/webapp/WEB-INF/web.xml
[INFO] Webapp directory = /.../tutorial/src/main/webapp
[INFO] Starting jetty 6.1.22 ...
INFO::jetty-6.1.22
INFO::No Transaction manager found - if your webapp requires one, please configure one.
INFO::Started SelectChannelConnector@0.0.0.0:8080
[INFO] Started Jetty Server
  • 至此, Jetty 容器已經(jīng)啟動了.

  • 現(xiàn)在, 在瀏覽器中打開網(wǎng)址 http://localhost:8080/tutorial/.

    你應(yīng)該在瀏覽器中看到 "No artists found message", 同時在 Eclipse 的控制臺中可看到如下輸出:

INFO: Loading XML configuration resource from file:/.../tutorial/target/classes/cayenne-project.xml
INFO: loading user name and password.
INFO: Created connection pool: jdbc:derby:memory:testdb;create=true
    Driver class: org.apache.derby.jdbc.EmbeddedDriver
    Min. connections in the pool: 1
    Max. connections in the pool: 1
INFO: Opening connection: jdbc:derby:memory:testdb;create=true
    Login: null
    Password: *******
INFO: +++ Connecting: SUCCESS.
INFO: Detected and installed adapter: org.apache.cayenne.dba.derby.DerbyAdapter
INFO: --- transaction started.
INFO: No schema detected, will create mapped tables
INFO: CREATE TABLE GALLERY (ID INTEGER NOT NULL, NAME VARCHAR (200), PRIMARY KEY (ID))
INFO: CREATE TABLE ARTIST (DATE_OF_BIRTH DATE, ID INTEGER NOT NULL, NAME VARCHAR (200), PRIMARY KEY (ID))
INFO: CREATE TABLE PAINTING (ARTIST_ID INTEGER, GALLERY_ID INTEGER, ID INTEGER NOT NULL,
      NAME VARCHAR (200), PRIMARY KEY (ID))
INFO: ALTER TABLE PAINTING ADD FOREIGN KEY (ARTIST_ID) REFERENCES ARTIST (ID)
INFO: ALTER TABLE PAINTING ADD FOREIGN KEY (GALLERY_ID) REFERENCES GALLERY (ID)
INFO: CREATE TABLE AUTO_PK_SUPPORT (
      TABLE_NAME CHAR(100) NOT NULL,  NEXT_ID BIGINT NOT NULL,  PRIMARY KEY(TABLE_NAME))
INFO: DELETE FROM AUTO_PK_SUPPORT WHERE TABLE_NAME IN ('ARTIST', 'GALLERY', 'PAINTING')
INFO: INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('ARTIST', 200)
INFO: INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('GALLERY', 200)
INFO: INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('PAINTING', 200)
INFO: SELECT t0.DATE_OF_BIRTH, t0.NAME, t0.ID FROM ARTIST t0 ORDER BY t0.NAME - prepared in 43 ms.
INFO: === returned 0 rows. - took 56 ms.
INFO: +++ transaction committed.
  • 你可以點擊 "Create new artist" 鏈接去新建藝術(shù)家. 對于已存在的藝術(shù)家的可以通過點擊他的名字來進行編輯.

[站外圖片上傳中...(image-f6fcc3-1524231454453)]

你已完成了本教程!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市涯捻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖矛辕,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件收苏,死亡現(xiàn)場離奇詭異悄雅,居然都是意外死亡台诗,警方通過查閱死者的電腦和手機完箩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拉队,“玉大人嗜憔,你說我怎么就攤上這事∈险蹋” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵夺鲜,是天一觀的道長皆尔。 經(jīng)常有香客問我,道長币励,這世上最難降的妖魔是什么慷蠕? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮食呻,結(jié)果婚禮上流炕,老公的妹妹穿的比我還像新娘。我一直安慰自己仅胞,他們只是感情好每辟,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著干旧,像睡著了一般渠欺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上椎眯,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天挠将,我揣著相機與錄音,去河邊找鬼编整。 笑死舔稀,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的掌测。 我是一名探鬼主播内贮,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了贺归?” 一聲冷哼從身側(cè)響起淆两,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拂酣,沒想到半個月后秋冰,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡婶熬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年剑勾,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赵颅。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡虽另,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出饺谬,到底是詐尸還是另有隱情捂刺,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布募寨,位于F島的核電站族展,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏拔鹰。R本人自食惡果不足惜仪缸,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望列肢。 院中可真熱鬧恰画,春花似錦、人聲如沸瓷马。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽决采。三九已至自沧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間树瞭,已是汗流浹背拇厢。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留晒喷,地道東北人孝偎。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像凉敲,于是被迫代替她去往敵國和親衣盾。 傳聞我的和親對象是個殘疾皇子寺旺,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)势决,斷路器阻塑,智...
    卡卡羅2017閱讀 134,656評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,118評論 25 707
  • 像是小時候 這樣素凈的冬雪 這樣無聲的黃土垣 還有還有 窯洞里的熱炕上 姥姥靈動的剪紙 姥爺不離手的煙袋鍋子 母親...
    來世海神閱讀 196評論 0 0
  • 端午已過 * 端午已過 詩友增多 我仿佛打開了 另一個世界 萬里路上 又有一群新的 一道詩藝社 * 這里春暖花開 ...
    留云俠客閱讀 217評論 0 0
  • ____祐閱讀 140評論 0 0