Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Join us at AWS re:Invent 2024! Learn how to use MongoDB for AI use cases.
MongoDB Developer
Swift
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Languageschevron-right
Swiftchevron-right

Working with the MongoDB Single-Collection Pattern in Swift

Andrew Morgan6 min read • Published Jan 18, 2023 • Updated Jan 18, 2023
MongoDBSwift
Facebook Icontwitter iconlinkedin icon
Rate this quickstart
star-empty
star-empty
star-empty
star-empty
star-empty
It's a MongoDB axiom that you get the best performance and scalability by storing together the data that's most commonly accessed together.
The simplest and most obvious approach to achieve this is to embed all related data into a single document. This works great in many cases, but there are a couple of scenarios where it can become inefficient:
  • (Very) many to many relationships. This can lead to duplicated data. This duplication is often acceptable — storage is comparatively cheap, after all. It gets more painful when the duplicated data is frequently modified. You then have the cost of updating every document which embeds that data.
  • Reading small parts of large documents. Even if your query is only interested in a small fraction of fields in a document, the whole document is brought into cache — taking up memory that could be used more effectively.
  • Large, mutable documents. Whenever your application makes a change to a document, the entire document must be written back to disk at some point (could be combined with other changes to the same document). WiredTiger writes data to disk in 4 KB blocks after compression — that typically maps to a 16-20 KB uncompressed document. If you're making lots of small edits to a 20+ KB document, then you may be wasting disk IO.
If embedding all of the data in a single document isn't the right pattern for your application, then consider the single-collection design. The single-collection pattern can deliver comparable read performance to embedded documents, while also optimizing for updates.
There are variants on the single-collection pattern, but for this post, I focus on the key aspects:
  • Related data that's queried together is stored in the same collection.
  • The documents can have different structures.
  • Indexes are added so that all of the data for your frequent queries can be fetched with a single index lookup.
At this point, your developer brain may be raising questions about how your application code can cope with this. It's common to read the data from a particular collection, and then have the MongoDB driver convert that document into an object of a specific class. How does that work if the driver is fetching documents with different shapes from the same collection? This is the primary thing I want to demonstrate in this post.
I'll be using Swift, but the same principles apply to other languages. To see how to do this with Java/Spring Data, take a look at Single-Collection Designs in MongoDB with Spring Data.

Running the example code

I recently started using the MongoDB Swift Driver for the first time. I decided to build a super-simple Mac desktop app that lets you browse your collections (which MongoDB Compass does a much better job of) and displays Change Stream events in real time (which Compass doesn't currently do).
You can download the code from the Swift-Change-Streams repo. Just build and run from Xcode.
Provide your connection-string and then browse your collections. Select the "Enable change streams" option to display change events in real time.
A simple desktop browser, connecting to a MongoDB cluster, browsing the collections, and enabling Change Streams. We then see change-stream events shown in the tool. Changes showup as a yellow JSON document which shows the changes as well as the new doc. Inserts show as green, and deletes as red.
The app will display data from most collections as generic JSON documents, with no knowledge of the schema. There's a special case for a collection named "Collection" in a database named "Single" — we'll look at that next.

Sample data

The Simple.Collection collection needs to contain these (or similar) documents:
1{ _id: 'basket1', docType: 'basket', customer: 'cust101' }
2{ _id: 'basket1-item1', docType: 'item', name: 'Fish', quantity: 5 }
3{ _id: 'basket1-item2', docType: 'item', name: 'Chips', quantity: 3 }
This data represents a shopping basket with an _id of "basket1". There are two items associated with basket1 — basket1-item1 and basket1-item2. A single query will fetch all three documents for the basket (find all documents where _id starts with "basket1"). There is always an index on the _id attribute, and so that index will be used.
Note that all of the data for a basket in this dataset is extremely small — well below the 16-20K threshold — and so in a real life example, I'd actually advise embedding everything in a single document instead. The single-collection pattern would make more sense if there were a large number of line items, and each was large (e.g., if they embedded multiple thumbnail images).
Each document also has a docType attribute to identify whether the document refers to the basket itself, or one of the associated items. If your application included a common query to fetch just the basket or just the items associated with the basket, then you could add a composite index: { _id: 1, docType: 1}.
Other uses of the docType field include:
  • A prompt to help humans understand what they're looking at in the collection.
  • Filtering the data returned from a query to just certain types of documents from the collection.
  • Filtering which types of documents are included when using MongoDB Compass to examine a collection's schema.
  • Allowing an application to identify what type of document its received. The application code can then get the MongoDB driver to unmarshal the document into an object of the correct class. This is what we'll look at next.

Handling different document types from the same collection

We'll use the same desktop app to see how your code can discriminate between different types of documents from the same collection.
The app has hardcoded knowledge of what a basket and item documents looks like. This allows it to render the document data in specific formats, rather than as a JSON document:
The desktop tool is showing documents from the "Single.Collection" collection. Rather than showing the document as generic JSON, the app shows tiles for the basket and for each of the 2 items. Each tile renders data from the associated document in a format that makes sense for that document type.
The code to determine the document docType and convert the document to an object of the appropriate class can be found in CollectionView.swift.
CollectionView fetches all of the matching documents from MongoDB and stores them in an array of BSONDocuments:
1@State private var docs = [BSONDocument]()
The application can then loop over each document in docs, checks the docType attribute, and then decides what to do based on that value:
1List(docs, id: \.hashValue) { doc in
2 if path.dbName == "Single" && path.collectionName == "Collection" {
3 if let docType = doc["docType"] {
4 switch docType {
5 case "basket":
6 if let basket = basket(doc: doc) {
7 BasketView(basket: basket)
8 }
9 case "item":
10 if let item = item(doc: doc) {
11 ItemView(item: item)
12 }
13 default:
14 Text("Unknown doc type")
15 }
16 }
17 } else {
18 JSONView(doc: doc)
19 }
20}
If docType == "basket", then the code converts the generic doc into a Basket object and passes it to BasketView for rendering.
This is the Basket class, including initializer to create a Basket from a BSONDocument:
1struct Basket: Codable {
2 let _id: String
3 let docType: String
4 let customer: String
5
6 init(doc: BSONDocument) {
7 do {
8 self = try BSONDecoder().decode(Basket.self, from: doc)
9 } catch {
10 _id = "n/a"
11 docType = "basket"
12 customer = "n/a"
13 print("Failed to convert BSON to a Basket: \(error.localizedDescription)")
14 }
15 }
16}
Similarly for Items:
1struct Item: Codable {
2 let _id: String
3 let docType: String
4 let name: String
5 let quantity: Int
6
7 init(doc: BSONDocument) {
8 do {
9 self = try BSONDecoder().decode(Item.self, from: doc)
10 } catch {
11 _id = "n/a"
12 docType = "item"
13 name = "n/a"
14 quantity = 0
15 print("Failed to convert BSON to a Item: \(error.localizedDescription)")
16 }
17 }
18}
The sub-views can then use the attributes from the properly-typed object to render the data appropriately:
1struct BasketView: View {
2 let basket: Basket
3
4 var body: some View {
5 VStack {
6 Text("Basket")
7 .font(.title)
8 Text("Order number: \(basket._id)")
9 Text("Customer: \(basket.customer)")
10 }
11 .padding()
12 .background(.secondary)
13 .clipShape(RoundedRectangle(cornerRadius: 15.0))
14 }
15}
1struct ItemView: View {
2 let item: Item
3
4 var body: some View {
5 VStack {
6 Text("Item")
7 .font(.title)
8 Text("Item name: \(item.name)")
9 Text("Quantity: \(item.quantity)")
10 }
11 .padding()
12 .background(.secondary)
13 .clipShape(RoundedRectangle(cornerRadius: 15.0))
14 }
15}

Conclusion

The single-collection pattern is a way to deliver read and write performance when embedding or other design patterns aren't a good fit.
This pattern breaks the 1-1 mapping between application classes and MongoDB collections that many developers might assume. This post shows how to work around that:
  • Extract a single docType field from the BSON document returned by the MongoDB driver.
  • Check the value of docType and get the MongoDB driver to map the BSON document into an object of the appropriate class.
Questions? Comments? Head over to our Developer Community to continue the conversation!

Facebook Icontwitter iconlinkedin icon
Rate this quickstart
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Tutorial

Migrating Your iOS App's Realm Schema in Production


Sep 01, 2022 | 5 min read
News & Announcements

Halting Development on MongoDB Swift Driver


Sep 11, 2024 | 1 min read
Quickstart

Working with Change Streams from Your Swift Application


Jan 25, 2023 | 4 min read
Tutorial

Adding Realm as a Dependency to an iOS Framework


Aug 12, 2024 | 4 min read
Table of Contents
  • Running the example code