使用 Morphia 和 MongoDB 實現域模型永續性
目錄
持久儲存、載入、刪除和查詢對映到 MongoDB 的 Java 域模型
MongoDB 是面向文件的資料庫,用於儲存並檢索類似 JavaScript 物件符號(JavaScript Object Notation,JSON)的文件。由於索引、複製和分片功能的增強,MongoDB 已成為強大的、可擴充套件的 NoSQL 競爭者(參見 參考資料)。
官方 Java 驅動程式可用於與 MongoDB 互動。該驅動程式提供 Map
的實現 BasicDBObject
,用於在資料儲存中表示文件。雖然 Map
表示法很方便,尤其是在讀對 JSON 序列化和反序列化時,但是能夠將文件表示為 Java 類層次也具有其優點。例如,反覆從 Java 域模型對映文件,允許您在 Java 層上強行執行型別安全,同時通過 MongoDB 享受無模式(schema)開發的好處。
Morphia 是基於 Apache 許可證的 Google Code 專案,讓您在 MongoDB 上持久儲存、檢索、刪除和查詢作為文件儲存的 POJO。通過提供一系列圍繞 Mongo Java 驅動程式的註釋和包裝程式,Morphia 完成了這些操作。Morphia 概念上類似於物件關係對映器(ORM),如 Java Persistence API (JPA) 或 Java 資料物件(Java Data Objects,JDO)實現。在本文中,我將演示如何對對映到 MongoDB 的 Java 域模型使用 Morphia。參見 下載 部分獲取完整的樣例程式碼。
-
定義域模型
我將使用簡化的域模型來演示 Morphia 的功能。BandManager(假想的 web 應用程式)提供有關音樂活動的資料:其成員、經銷商、目錄、型別等等。我將定義 Band
Song
、Distributor
和 ContactInfo
類來表示此域模型,如圖 1 所示:
圖 1. BandManager 的類
圖 1 中的統一建模語言(Unified Modeling Language,UML)顯示了域模型類層次。左側的矩形表示 Band
類。右側的矩形分別表示 ContactInfo
、Distributor
和 Song
類。從 Band
指向 ContactInfo
的箭頭在 ContactInfo
旁邊標有一個 1,這說明兩個類之間是一對一的關係。連線 Band
到 Distributor
的線在 Band
旁邊標有 0..* 且在 Distributor
Band
具有單一的 Distributor
且 Distributor
表示許多 Band
。最後,從 Band
到 Song
的箭頭在 Song
旁邊標有目錄 0..1,這說明 Band
具有與 Song
的一對多關係且這種關係被稱作 catalog
。
我將對這些類進行註釋,然後使用 Morphia 的 Datastore
介面在 MongoDB 上將它們儲存為文件。
註釋域模型
清單 1 顯示瞭如何註釋 Band
類:
清單 1. Band.java
@Entity("bands")
public class Band {
@Id
ObjectId id;
String name;
String genre;
@Reference
Distributor distributor;
@Reference("catalog")
List<Song> songs = new ArrayList<Song>();
@Embedded
List<String> members = new ArrayList<String>();
@Embedded("info")
ContactInfo info;
@Entity
註釋是必需的。其聲明瞭在專用集合(collection)上該類作為文件將持久儲存。提供給 @Entity
註釋的值(bands
)定義瞭如何命名集合。在預設情況下,Morphia 使用類名稱來命名集合。例如,如果我遺漏了 bands
值,則在資料庫中該集合將被稱為 Band
。
@Embedded
將資料物件巢狀在Band中。 @Embedded("info") ,info
屬性是另外一個嵌入的物件,info相當於鍵 我通過 info
值明確地設定 @Embedded
註釋。這會覆蓋文件中子集的預設命名,否則就會被稱為 contactInfo
。例如:
|
資料型別
MongoDB 比 Java 語言支援更小的資料型別集合,即 integer
、long
、double
和 string
。Morphia 可自動為您轉換基本 Java 型別(如 float
)。
@Id
註釋指示 Morphia 哪個欄位用作文件 ID。如果您試圖持久儲存物件(其 @Id
註釋的欄位為 null),則 Morphia 會為您自動生成 ID 值。
Morphia 試圖持久儲存每一個它遇到的沒有註釋的欄位,除非它們標有 @Transient
註釋。例如,在文件中 name
和 genre
屬性將被儲存為 string
,並具有 name
和 genre
鍵。
distributor
、songs
、members
和 info
屬性引用其他物件。除非註釋有 @Reference
(不久將看到),否則成員物件將被視為嵌入的(embedded)。它會顯示為集合中父文件的子集。例如,在持久儲存時,members List
看上去如下所示:
1 |
|
@Reference 和 DBRefs
在後臺,Morphia 使用 Mongo DBRef
來在不同集合中引用物件。
使用 @Reference
註釋說明物件是對另外一個集合中的文件的引用。在從 Mongo 集合中載入物件時,Morphia 遵循著這些引用來建立物件圖。例如,在持久儲存的文件中,distributor
屬性看起來如下所示:
1 |
|
正如 @Embedded
註釋一樣,@Reference
可以採用一個值來覆蓋預設命名。在本例中,我將 songs
的 List
稱為文件中的 catalog
。
現在看一下 Song
、Distributor
和 ContactInfo
的類定義。清單 2 顯示了 Song
的定義:
清單 2. Song.java
1 2 3 4 5 6 7 |
|
清單 3 顯示了 Distributor
的定義:
清單 3. Distributor.java
1 2 3 4 5 6 7 8 9 10 |
|
清單 4 顯示了 ContactInfo
的定義:
清單 4. ContactInfo.java
1 2 3 4 5 6 7 8 9 |
|
ContactInfo
類缺少 @Entity
註釋。這是故意而為的,因為我不需要 ContactInfo
的專用集合。例項總是被嵌入 band
文件。
現在我已經定義並註釋了域模型,我將向您展示如何使用 Morphia 的 Datastore
以便儲存、載入和刪除實體。
-
使用
Datastore
依賴注入(Dependency injection,DI)
Datastore
和 Mongo 都是 DI 友好型。例如,在 Spring 或 Guice 中,您不應該在連線它們的問題上遇到任何麻煩。如果有可能,您應該配置每一個作為單一例項並在合作(collaborating)bean 之間共享它們。
Datastore
介面 — Mongo Java 驅動程式的包裝程式 — 用於在 MongoDB 中管理實體。因為 Datastore
需要 Mongo 例項以進行例項化,您可以重新使用現有的 Mongo 例項或為您的環境適當地配置一個例項。下面是一個例項化 Datastore
的示例,其連線到本地 MongoDB 例項:
Mongo mongo = new Mongo("localhost");
Datastore datastore = new Morphia().createDatastore(mongo, "bandmanager");
下一步我將建立 Band
例項:
Band band = new Band();
band.setName("Love Burger");
band.getMembers().add("Jim");
band.getMembers().add("Joe");
band.getMembers().add("Frank");
band.getMembers().add("Tom");
band.setGenre("Rock");
現在我擁有了 Band
例項,我可以使用 datastore
來持久儲存它:
datastore.save(band);
band
現在應該儲存在 bandmanager
資料庫中被稱為 bands
的集合中。通過使用 Mongo 命令列介面客戶端,我可以檢視一下以便確保(在本示例和其他示例中,折行以便適合本文頁面的寬度):
1 2 3 4 |
|
這真是太棒了!它就在這裡。除了 className
欄位以外,一切看起來正如您所期望的。Morphia 自動建立此欄位以便記錄 MongoDB 中的物件型別。其主要用於確定在編譯時不必知道的物件型別(例如,在您從具有混合型別的集合中載入物件時)。如果這個困擾了您且您知道您不需要該功能,那麼通過將 noClassnameStored
值新增到 @Entity
註釋,您可以禁用持久儲存 className
:
1 |
|
現在我將載入 Band
並斷言它等同於我所持久儲存的 band
:
1 |
|
Datastore
的 get()
方法允許您使用實體的 ID 載入該實體。您無需指定集合或定義查詢字串來載入物件。您只需告訴 Datastore
,您想載入哪個類及其 ID。Morphia 進行其餘的操作。
現在可以檢視 Band
的合作物件了。我開始先定義一些 Song
,然後將它們新增到我剛剛建立的 Band
例項:
Song song1 = new Song("Stairway");
Song song2 = new Song("Free Bird");
datastore.save(song1);datastore.save(song2);
如果我在 Mongo 中檢視 songs
集合,我應該看到如下所示:
1 2 3 4 5 |
|
請注意 Song
還沒有從 band
引用。我將它們新增到 band
並檢視發生了什麼:
band.getSongs().add(song1);
band.getSongs().add(song2);
datastore.save(band);
現在我查詢 bands
集合時,我應該看到:
1 2 3 4 5 6 7 8 9 10 11 |
|
事務
非常重要的是記住 MongoDB 不像大多數關係資料庫管理系統那樣支援事務。如果您的應用程式需要協調多個執行緒寫入或讀取集合,您就必須依靠 Java 語言的序列化和併發功能。
請注意 songs
集合如何被儲存為一個被稱為 catalog
的陣列,作為兩個 DBRef
。
現在的限制是引用的物件必須先被儲存,然後其他物件才能引用它們。這解釋了為什麼我先儲存 song1
和 song2
,然後將它們新增到 band
。
現在我將刪除 song2
:
1 |
|
查詢 songs
集合應該說明沒有了 song2
。但是如果您檢視 band
,您將看到該歌曲仍在那裡。更糟糕的是,試圖載入 band
實體會導致異常:
1 2 3 |
|
現在,要避免此錯誤,您需要在刪除歌曲以前手動刪除對它的引用。
-
查詢
到目前為止,通過其 ID 載入實體只能得到實體的資訊。最終我希望能夠查詢 Mongo 並得到我想要的實體。
我將通過名稱查詢 band
,而不是通過其 ID 載入它。為此,我通過建立 Query
物件並指定篩選器來獲得我希望的結果:
Query query = datastore.createQuery(Band.class)
.filter("name = ","Love Burger");
//或者使用field
Query query = datastore.createQuery(Band.class)
.field("id").equal(new ObjectId(id))
.field("name").equal(1);
query.get(); //獲取一個,返回Band實體
query.asList(); //返回一個集合列表
我指定了想要查詢的類,即 Band
,和針對 createQuery()
方法的篩選器。一旦我定義了查詢,我就可以使用 asList()
方法來訪問結果:
1 |
|
Morphia 的篩選運算子緊密地對映到用於 MongoDB 查詢的查詢運算子。例如,我在上面查詢中使用的 =
運算子就類似於 MongoDB 中的 $eq
運算子。有關篩選運算子的全部細節都在 Morphia 線上文件中(參見 參考資料)。
作為篩選查詢的替代,Morphia 為構建查詢提供了更好的介面。例如,以下介面查詢等同於以前的篩選查詢:
Query query = datastore.createQuery(Band.class)
.field("name").equal("Love Burger");
您可以使用“點註釋”來查詢嵌入的物件。下面是使用點註釋和介面的查詢,用於選擇位於 Brooklyn 的所有樂隊:
Query query = datastore.createQuery(Band.class)
.field("info.city").equal("Brooklyn");
您可以進一步定義查詢結果集。我將修改以前的查詢以便根據名稱來對樂隊排序並將結果限制為 100:
Query query = datastore.createQuery(Band.class)
.field("info.city")
.equal("Brooklyn")
.order("name")
.limit(100);
-
索引
您將注意到隨著您的集合增長查詢效能將會降低。Mongo 集合(非常像關係資料庫表)需要正確進行索引以便確保合理的查詢效能。
通過 @Indexed
註釋對屬性進行註釋會對該欄位應用索引。這裡,我對 Band
的 genre
屬性建立了一個名為 genreName
的升序索引:
@Indexed(value = IndexDirection.ASC, name = "genreName")
String genre;
要應用索引,Morphia 需要知道對映哪些類。您需要以稍微不同的方式例項化 Morphia 以便確保應用索引。您可以按如下所示執行:
1 2 3 4 |
|
最終的 ensureIndexes()
呼叫可以指示資料儲存建立所需且不存在的索引。
索引還可用於避免將重複項插入到集合中。例如,通過在 band
名稱的 @Indexed
註釋上設定 unique
屬性,我可以確保在該集合中只有一個具有給定名稱的 band
:
1 2 |
|
隨後同名的 band
將被丟棄。
-
結束語
Morphia 是與 MongoDB 進行互動的強大工具。它允許對 MongoDB 文件進行型別安全的、慣用的訪問。本文涵蓋了使用 Morphia 的主要方面,但排除了一些功能。要獲得有關其資料訪問物件(Data Access Object,DAO)支援、驗證和手動對映功能的資訊,我鼓勵您檢視 Morphia Google Code 專案。
相關主題