1. 程式人生 > >使用 Morphia 和 MongoDB 實現域模型永續性

使用 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

SongDistributor 和 ContactInfo 類來表示此域模型,如圖 1 所示:

圖 1. BandManager 的類

針對 BandManager 應用程式的域模型類層次的 UML 圖表

圖 1 中的統一建模語言(Unified Modeling Language,UML)顯示了域模型類層次。左側的矩形表示 Band 類。右側的矩形分別表示 ContactInfoDistributor 和 Song 類。從 Band 指向 ContactInfo 的箭頭在 ContactInfo 旁邊標有一個 1,這說明兩個類之間是一對一的關係。連線 Band 到 Distributor的線在 Band 旁邊標有 0..* 且在 Distributor

 旁邊標有一個 1,這說明 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。例如:

"info" : { "city" : "Brooklyn", "phoneNumber" : "718-555-5555" }

資料型別

MongoDB 比 Java 語言支援更小的資料型別集合,即 integerlongdouble 和 string。Morphia 可自動為您轉換基本 Java 型別(如 float)。

@Id 註釋指示 Morphia 哪個欄位用作文件 ID。如果您試圖持久儲存物件(其 @Id 註釋的欄位為 null),則 Morphia 會為您自動生成 ID 值。

Morphia 試圖持久儲存每一個它遇到的沒有註釋的欄位,除非它們標有 @Transient 註釋。例如,在文件中 name 和 genre 屬性將被儲存為 string,並具有 name 和 genre 鍵。

distributorsongsmembers 和 info 屬性引用其他物件。除非註釋有 @Reference(不久將看到),否則成員物件將被視為嵌入的(embedded)。它會顯示為集合中父文件的子集。例如,在持久儲存時,members List 看上去如下所示:

1

"members" : [ "Jim", "Joe", "Frank", "Tom"]

@Reference 和 DBRefs

在後臺,Morphia 使用 Mongo DBRef 來在不同集合中引用物件。

使用 @Reference 註釋說明物件是對另外一個集合中的文件的引用。在從 Mongo 集合中載入物件時,Morphia 遵循著這些引用來建立物件圖。例如,在持久儲存的文件中,distributor 屬性看起來如下所示:

1

"distributor" : { "$ref" : "distributors", "$id" : ObjectId("4cf7ba6fd8d6daa68a510e8b") }

正如 @Embedded 註釋一樣,@Reference 可以採用一個值來覆蓋預設命名。在本例中,我將 songs 的 List 稱為文件中的 catalog

現在看一下 SongDistributor 和 ContactInfo 的類定義。清單 2 顯示了 Song 的定義:

清單 2. Song.java 

1

2

3

4

5

6

7

@Entity("songs")

public class Song {

@Id

ObjectId id;

String name;

清單 3 顯示了 Distributor 的定義:

清單 3. Distributor.java 

1

2

3

4

5

6

7

8

9

10

@Entity("distributors")

public class Distributor {

@Id

ObjectId id;

String name;

@Reference

List<Band> bands = new ArrayList<Band>();

清單 4 顯示了 ContactInfo 的定義:

清單 4. ContactInfo.java

1

2

3

4

5

6

7

8

9

public class ContactInfo {

public ContactInfo() {

}

String city;

String phoneNumber;

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

> db.bands.find();

{ "_id" : ObjectId("4cf7cbf9e4b3ae2526d72587"), "className" :

"com.bandmanager.model.Band", "name" : "Love Burger", "genre" : "Rock",

"members" : [ "Jim", "Joe", "Frank", "Tom" ] }

這真是太棒了!它就在這裡。除了 className 欄位以外,一切看起來正如您所期望的。Morphia 自動建立此欄位以便記錄 MongoDB 中的物件型別。其主要用於確定在編譯時不必知道的物件型別(例如,在您從具有混合型別的集合中載入物件時)。如果這個困擾了您且您知道您不需要該功能,那麼通過將 noClassnameStored 值新增到 @Entity 註釋,您可以禁用持久儲存 className

1

@Entity(value="bands",noClassnameStored=true)

現在我將載入 Band 並斷言它等同於我所持久儲存的 band

1

assert(band.equals(datastore.get(Band.class, band.getId())));

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

> db.songs.find();

{ "_id" : ObjectId("4cf7d249c25eae25028ae5be"), "className" :

"com.bandmanager.model.Song", "name" : "Stairway" }

{ "_id" : ObjectId("4cf7d249c25eae25038ae5be"), "className" :

"com. bandmanager.model.Song", "name" : "Free Bird" }

請注意 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

{ "_id" : ObjectId("4cf7d249c25eae25018ae5be"), "name" : "Love Burger", "genre" : "Rock",

"catalog" : [

{

"$ref" : "songs",

"$id" : ObjectId("4cf7d249c25eae25028ae5be")

},

{

"$ref" : "songs",

"$id" : ObjectId("4cf7d249c25eae25038ae5be")

}

], "members" : [ "Jim", "Joe", "Frank", "Tom"] }

事務

非常重要的是記住 MongoDB 不像大多數關係資料庫管理系統那樣支援事務。如果您的應用程式需要協調多個執行緒寫入或讀取集合,您就必須依靠 Java 語言的序列化和併發功能。

請注意 songs 集合如何被儲存為一個被稱為 catalog 的陣列,作為兩個 DBRef

現在的限制是引用的物件必須先被儲存,然後其他物件才能引用它們。這解釋了為什麼我先儲存 song1 和 song2,然後將它們新增到 band

現在我將刪除 song2

1

datastore.delete(song2);

查詢 songs 集合應該說明沒有了 song2。但是如果您檢視 band,您將看到該歌曲仍在那裡。更糟糕的是,試圖載入 band 實體會導致異常:

1

2

3

Caused by: com.google.code.morphia.mapping.MappingException: The

reference({ "$ref" : "songs", "$id" : "4cf7d249c25eae25038ae5be" }) could not be

fetched for com.bandmanager.model.Band.songs

現在,要避免此錯誤,您需要在刪除歌曲以前手動刪除對它的引用。

  • 查詢

到目前為止,通過其 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

Band band = (Band) query.asList().get(0);

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

Morphia morphia = new Morphia();

morphia.mapPackage("com.bandmanager.model");

datastore = morphia.createDatastore(mongo, "bandmanager");

datastore.ensureIndexes();

最終的 ensureIndexes() 呼叫可以指示資料儲存建立所需且不存在的索引。

索引還可用於避免將重複項插入到集合中。例如,通過在 band 名稱的 @Indexed 註釋上設定 unique 屬性,我可以確保在該集合中只有一個具有給定名稱的 band

1

2

@Indexed(value = IndexDirection.ASC, name = "bandName", unique = true)

String name;

隨後同名的 band 將被丟棄。

  • 結束語

Morphia 是與 MongoDB 進行互動的強大工具。它允許對 MongoDB 文件進行型別安全的、慣用的訪問。本文涵蓋了使用 Morphia 的主要方面,但排除了一些功能。要獲得有關其資料訪問物件(Data Access Object,DAO)支援、驗證和手動對映功能的資訊,我鼓勵您檢視 Morphia Google Code 專案。

相關主題