

Copyright © 2020 Packt Publishing
All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews.
Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, nor Packt Publishing or its dealers and distributors, will be held liable for any damages caused or alleged to have been caused directly or indirectly by this book.
Packt Publishing has endeavored to provide trademark information about all of the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information.
Commissioning Editor: Pravin Dhandre
Acquisition Editor: Savia Lobo
Content Development Editor: Pratik Andrade
Senior Editor: Ayaan Hoda
Technical Editor: Sarvesh Jaywant
Copy Editor: Safis Editing
Project Coordinator: Neil Dmello
Proofreader: Safis Editing
Indexer: Rekha Nair
Production Designer: Alishon Mendonca
First published: September 2020
Production reference: 1030920
Published by Packt Publishing Ltd.
Livery Place
35 Livery Street
Birmingham
B3 2PB, UK.
ISBN 978-1-78961-938-6

Subscribe to our online digital library for full access to over 7,000 books and videos, as well as industry leading tools to help you plan your personal development and advance your career. For more information, please visit our website.
Spend less time learning and more time coding with practical eBooks and Videos from over 4,000 industry professionals
Improve your learning with Skill Plans built especially for you
Get a free eBook or video every month
Fully searchable for easy access to vital information
Copy and paste, print, and bookmark content
Did you know that Packt offers eBook versions of every book published, with PDF and ePub files available? You can upgrade to the eBook version at www.packt.com and as a print book customer, you are entitled to a discount on the eBook copy. Get in touch with us at customercare@packtpub.com for more details.
At www.packt.com, you can also read a collection of free technical articles, sign up for a range of free newsletters, and receive exclusive discounts and offers on Packt books and eBooks.
Learning something new is a part of a developer’s everyday job. Whether you are a beginner trying to find a book that will quickly teach you all the essential concepts of the latest version of MongoDB, or a senior developer trying to set up your company’s new MongoDB server this morning, you will need a dependable source of information that will teach you all that you need to know to get things up and running as quickly as possible. For these reasons and many more, you need a book that will get you to where you want to be, without the burden of doing lengthy research just to get the important stuff down pat.
This is where my good friend, Doug Bierer, comes in. Doug is not only a great developer, but he is a natural-born teacher. Doug has this extraordinary ability to get you up to speed and ready to go in no time, even with the most advanced programming concepts. He will not waste your time by giving you meaningless recipes that you are expected to apply blindly from now on, but he will rather give you what is needed, whether it be by way of computer theory or history, in order to help you understand what you are doing. His code examples are always very enlightening because they are chosen in order to get you working as quickly as possible with the tools that you wish to better understand and master.
In a few moments from now, you will grasp what NoSQL is in its most essential expression, without having to go through many bulky books. You will start by installing and configuring your MongoDB server; wrap your mind around MongoDB’s more advanced concepts, such as security, replication, and sharding; and use it like a pro. Moreover, you will learn how to design your new NoSQL databases while avoiding the common pitfalls that go along with having an SQL mindset and background.
Once you are done reading this book, I know that you will be well on your way to mastering MongoDB and to becoming a better developer than you were before.
Andrew Caya,
CEO Foreach Code Factory
https://andrewscaya.net/
Doug Bierer has been writing code since the early 1970s. His first project was a terrain simulator that ran on a Digital Equipment Corporation PDP-8 with 4 K of RAM. Since then, he's written applications for a variety of customers worldwide, in various programming languages, including BASIC, Assembler, PL/I, C, C++, FORTH, Prolog, Pascal, Java, PERL, PHP, and Python. His work with database technology spans a similar range in terms of years and includes Ingres, d:Base, Clipper, FoxBase, r:Base, ObjectVision, Btrieve, Oracle, MySQL, and, of course, MongoDB. Doug runs his own company, unlikelysource, and is also CTO at Foreach Code Factory, being involved in both training (php-cl) and a new cloud services platform (Linux for PHP Cloud Services).
Blaine Mincey is a principal solutions architect with MongoDB and is based in Atlanta, GA. He is responsible for guiding MongoDB customers to design and build reliable, scalable systems using MongoDB technology. Prior to MongoDB, Blaine was a software engineer with several notable organizations/start-ups in the Atlanta area before joining the Red Hat/JBoss solutions architecture team.
When Blaine is not talking about technology or his family (wife, two boys, and a dog), he can be found at one of three locations: Fado's Midtown, Atlanta, watching his beloved Manchester United; Mercedes-Benz stadium, while supporting Atlanta United; or on the local football pitch coaching either of his sons' soccer squads.
If you're interested in becoming an author for Packt, please visit authors.packtpub.com and apply today. We have worked with thousands of developers and tech professionals, just like you, to help them share their insight with the global tech community. You can make a general application, apply for a specific hot topic that we are recruiting an author for, or submit your own idea.
Aging legacy database technologies used in current relational database management systems (RDBMS) are no longer able to handle the needs of modern web applications. MongoDB, built upon a NoSQL foundation, has taken the world by storm and is rapidly gaining market share over inadequate RDBMS-based websites. It is critical for DevOps, technical managers, and those whose careers are in transition to gain knowledge on how to build dynamic database-driven web applications based upon MongoDB. This book takes a deep dive into management and the application development of a web application that uses MongoDB 4.x as its database.
The book takes a hands-on approach, building a set of useful Python classes, starting with simple operations, and adding more complexity as the book progresses. When you have finished going through the book, you'll have mastered not only basic day-to-day MongoDB database operations, but will have gained a firm understanding of development using complex MongoDB features such as replication, sharding, and the aggregation framework. Complex queries are presented in both JavaScript form as well as the equivalent in Python. The JavaScript queries presented will allow DBAs to manage and manipulate data directly, as well as being able to generate ad hoc reports for management. Throughout the book, practical examples of each concept are covered in detail, along with potential errors and gotchas.
All examples in the book are based upon realistic real-world scenarios ranging from a small company that sells sweets online, all the way to a start-up bank that offers micro-financing to customers worldwide. With each scenario, you'll learn how to develop a usable MongoDB database structure based on realistic customer requirements. Each major part of the book, based upon a different scenario, guides you through progressively more complex tasks, ranging from generating simple reports, to directing queries through a global network of sharded MongoDB database clusters.
This book is designed for anyone who roughly fits any of these descriptions:
Chapter 1, Introducing MongoDB 4.x, provides a general introduction to MongoDB 4.x with a focus on new features and a brief high-level overview of the technology and the differences between MongoDB 4.x and MongoDB 3.
Chapter 2, Setting Up MongoDB 4.x, covers MongoDB 4.x and Python programming language drivers. In addition, this chapter shows you how to set up a demo test environment based upon Docker, in which you can practice working with MongoDB.
Chapter 3, Essential MongoDB Administration Techniques, shows how to perform administration critical to a properly functioning database using the mongo shell.
Chapter 4, Fundamentals of Database Design, describes moving from a set of customer requirements to a working MongoDB database design.
Chapter 5, Mission-Critical MongoDB Database Tasks, presents a series of common tasks and then shows you how to define domain classes that perform critical database access services including performing queries, adding, editing, and deleting documents.
Chapter 6, Using AJAX and REST to Build a Database-Driven Website, moves on to what is needed to have the application respond to AJAX queries and REST requests.
Chapter 7, Advanced MongoDB Database Design, covers how to define a dataset with complex requirements. You are also shown how MongoDB can be integrated with the popular Django web framework.
Chapter 8, Using Documents with Embedded Lists and Objects, covers working with the documents designed in the previous chapter, learning how to handle create, read, update and delete operations that involve embedded lists (arrays) and objects.
Chapter 9, Handling Complex Queries in MongoDB, introduces you to the aggregation framework, a feature unique to MongoDB. In this chapter, you also learn about map-reduce, geospatial queries, generating financial reports, and performing risk analysis.
Chapter 10, Working with Complex Documents across Collections, starts with a brief discussion on how to handle monetary data, after which the focus shifts to working with complex documents across multiple collections. Lastly, GridFS technology is introduced as a way of handling large files and storing documents directly in the database.
Chapter 11, Administering MongoDB Security, focuses on the administration needed to secure the database by creating database users, implement role-based access control and implement transport layer (SSL/TLS) security based upon x.509 certificates.
Chapter 12, Developing in a Secured Environment, looks into how to write applications that access a secured database using role-based access control and TLS security using x.509 certificates.
Chapter 13, Deploying a Replica Set, starts with an overview of MongoDB replication, after which you'll learn how to configure and deploy a replica set.
Chapter 14, Replica Set Runtime Management and Development, focuses on replica set management, monitoring, backup, and restore. In addition, it addresses how your application program code might change when accessing a replica set.
Chapter 15, Deploying a Sharded Cluster, starts with an overview of sharding, after which you'll learn how to configure and deploy a sharded cluster.
Chapter 16, Sharded Cluster Management and Development, covers how to manage a sharded cluster as well as learning about the impact of sharding on application program code.
In order to test the sample queries and coding examples presented in this book, you need the following:
Minimum recommended hardware:
Software requirements:
In order to successfully understand and work through the examples presented in this book, the following background knowledge will be helpful:
Python 3.x, the PyMongo driver, an Apache web server, and various Python libraries are already installed in the Docker container used for the book. Chapter 2, Setting Up MongoDB 4.x, gives you instructions on how to install the demo environment used in the book.
If you are using the digital version of this book, we advise you to type the code yourself or access the code via the GitHub repository (link available in the next section). Doing so will help you avoid any potential errors related to the copying and pasting of code.
You can download the example code files for this book from your account at www.packt.com. If you purchased this book elsewhere, you can visit www.packtpub.com/support and register to have the files emailed directly to you.
You can download the code files by following these steps:
Once the file is downloaded, please make sure that you unzip or extract the folder using the latest version of:
The code bundle for the book is also hosted on GitHub at https://github.com/PacktPublishing/Learn-MongoDB-4.x. In case there's an update to the code, it will be updated on the existing GitHub repository. We also have other code bundles from our rich catalog of books and videos available at https://github.com/PacktPublishing/. Check them out!
We also provide a PDF file that has color images of the screenshots/diagrams used in this book. You can download it here: http://www.packtpub.com/sites/default/files/downloads/9781789619386_ColorImages.pdf.
There are a number of text conventions used throughout this book.
CodeInText: Indicates code words in text, database collection and field names, folder names, filenames, file extensions, pathnames, dummy URLs, user input, and Twitter handles. Here is an example: "Finally, the $sort stage reorders the final results by the match field _id (that is, the country code)."
A block of code is set as follows:
db.bookings.aggregate([
{ $match : { "bookingInfo.paymentStatus" : "confirmed" } },
{ $group: { "_id" : "$customer.customerAddr.country",
"total" : { $sum : "$totalPrice" } } },
{ $sort : { "_id" : 1 } }
]);
If a line of code or a command needs to be all on a single line, but the book's page width prevents this, the line will be broken up into two lines. A backslash (\) is placed at the break point. The remainder of the line appears indented on the next line as follows:
this.command('has', 'many', 'arguments', 'and', \
'would', 'occupy', 'a', 'single', 'line')
Any command-line input or output is written as follows:
cd /path/to/repo
docker-compose build
docker-compose up -d
docker exec -it learn-mongo-server-1 /bin/bash
Bold: Indicates a new term, an important word, or words that you see onscreen. For example, words in menus or dialog boxes appear in the text like this. Here is an example: "You can also select Fill in connection fields individually, in which case these are the two tab screens you can use."
Feedback from our readers is always welcome.
General feedback: If you have questions about any aspect of this book, mention the book title in the subject of your message and email us at customercare@packtpub.com.
Errata: Although we have taken every care to ensure the accuracy of our content, mistakes do happen. If you have found a mistake in this book, we would be grateful if you would report this to us. Please visit www.packtpub.com/support/errata, selecting your book, clicking on the Errata Submission Form link, and entering the details.
Piracy: If you come across any illegal copies of our works in any form on the Internet, we would be grateful if you would provide us with the location address or website name. Please contact us at copyright@packt.com with a link to the material.
If you are interested in becoming an author: If there is a topic that you have expertise in and you are interested in either writing or contributing to a book, please visit authors.packtpub.com.
Please leave a review. Once you have read and used this book, why not leave a review on the site that you purchased it from? Potential readers can then see and use your unbiased opinion to make purchase decisions, we at Packt can understand what you think about our products, and our authors can see your feedback on their book. Thank you!
For more information about Packt, please visit packt.com.
This section is designed to give you a high-level overview of MongoDB 4.x. In this section, you are introduced to the first of three realistic scenarios, a fictitious company called Sweets Complete, Inc. These scenarios are representative of real-life companies and are designed to show how MongoDB can be incorporated into various types of businesses.
In this section, you'll learn about the key differences between MongoDB 3 and 4. You will learn not only how to install MongoDB 4.x for a future customer, but also how to set up an environment based upon Docker technology that you can use throughout the remainder of the book in order to run the examples. You will also learn basic MongoDB administration using the mongo shell.
This section contains the following chapters:
In this book, we cover how to work with a MongoDB 4.x database, starting with the simplest concepts and moving on to more complex ones. The book is divided into parts or sections, each of which looks at a different scenario.
In this chapter, you are given a general introduction to MongoDB 4.x with a focus on new features and a brief high-level overview of the technology. We also discuss security enhancements, along with backward-incompatible changes that might cause an application written for MongoDB 3 to break after an upgrade to MongoDB 4.x.
In the next chapter, a simple scenario is introduced: a fictitious company called Sweets Complete Inc. that sells confections online to a small base of international customers. In the next two parts that follow, you are introduced to BookSomething.com, another fictitious company with a large database of hotel listings worldwide. Finally, in the last part, you are introduced to BigLittle Micro Finance Ltd., a fictitious company that connects lenders with borrowers and deals with a massive volume of geographically dispersed data.
In this chapter, the following topics are covered:
When it was first introduced in 2009, MongoDB took the database world by storm, and since that time it has rapidly gained in popularity. According to the 2019 StackOverflow developer survey (https://insights.stackoverflow.com/survey/2019#technology-_-databases), MongoDB is ranked fifth, with 26% of professional developers and 25.5% of all respondents saying they use MongoDB. DB-Engines (https://db-engines.com/en/ranking) also ranks MongoDB as the fifth most widely used database, using an algorithm that takes into account the frequency of search, DBA Stack Exchange and StackOverflow references, and the frequency with which MongoDB appears in job postings. What is of even more interest is that the trend graph generated by DB-Engines shows that the score (and therefore ranking) of MongoDB has grown by 200% since 2013. You can refer to https://db-engines.com/en/ranking_trend for more details. In 2013, MongoDB was not even in the top 10!
There are many key features of MongoDB that account for its rise in popularity. Subsequent chapters in this book cover the most important of these features in detail. In this section, we present you with a brief, big-picture overview of three key aspects of MongoDB.
One of the most important distinctions between MongoDB and the traditional relational database management systems (RDBMS) is that instead of tables, rows, and columns, the basis for storage in MongoDB is a document. In a certain sense, you can think of the traditional RDBMS system as two dimensional, whereas MongoDB is three dimensional. Documents are typically modeled using JSON formatting and then inserted into the database where they are converted to a binary format for storage (more on that in later chapters!).
Related to the document basis for storage is the fact that MongoDB documents have no fixed schema. The main benefit of this is vastly reduced overhead. Database restructuring is a piece of cake, and doesn't cause the massive problems, website crashes, and security breaches seen in applications reliant upon a traditional RDBMS database restructuring.
The really great news for developers is that most modern programming applications are based on classes representing information that needs to be stored. This has spawned the creation of a large number of object-relational mapping (ORM) libraries for the various programming languages. In MongoDB, on the other hand, the need for a complex ORM infrastructure is completely eliminated as programmatic objects can be directly stored in the database as-is:

So instead of columns, MongoDB documents have fields. Instead of tables, there are collections of documents. Let's now have a look at replication in MongoDB.
Another feature that causes MongoDB to stand out from other database technologies is its ability to ensure high availability through a process known as replication. A server running MongoDB can have copies of its databases duplicated across two more servers. These copies are known as replica sets. Replica sets are organized through an election process whereby the members of the replica vote on which server becomes the primary. Other servers are then assigned the role of secondary.
This arrangement not only ensures that the database is continuously available, but that it can also be used by application code by way of read preferences. A read preference tells the replica set which servers in the replica set are preferred. If the read preferences are set less restrictively, then the first server in the set to respond might be able to satisfy the request, thereby implementing a form of parallel process that has the potential to greatly enhance performance. This setup is illustrated in the following diagram:

This topic is covered in extensive detail in Chapter 13, Deploying a Replica Set. Lastly, we have a look at sharding.
One more feature, among many, is MongoDB's ability to handle a massive amount of data. This is accomplished by splitting up a sizeable collection across multiple servers, creating a sharded cluster. In the process of splitting the collection, a shard key is chosen from among the fields present with the collection's documents. The shard key is then used by the sharded cluster balancer to determine the appropriate distribution of documents. Application program code is then able, by its knowledge of the value of the shard key, to direct queries to specific members of the sharded cluster, achieving potentially enormous performance gains. This setup is illustrated in the following diagram:

This topic is covered in extensive detail in Chapter 15, Deploying a Sharded Cluster. In the next section of this chapter, we have a look at the major differences between MongoDB 3 and MongoDB 4.x.
What's new and different in the MongoDB 4.x release can be broken down into two main categories: new features and internal enhancements. Let's look at the most significant new features first.
The most significant new features introduced in MongoDB 4.x include the following:
In the database world, a transaction is a block of database operations that should be treated as if the entire block of commands was just a single command. An example would be where your application is performing end-of-the-month payroll processing. In order to maintain the integrity of the database, and your accounting files, you would need this set of operations to be safeguarded in the event of a failure. ACID is an acronym that stands for atomicity, consistency, isolation, and durability. It represents a set of principles that the database needs to follow in order to safeguard a block of database updates. For more information on ACID, you can refer to https://en.wikipedia.org/wiki/ACID.
In MongoDB 3, a write operation on a single document, even a document containing other embedded documents, was considered atomic. In MongoDB 4.x, multiple documents can be included in a single atomic transaction. Although invoking this support negatively impacts performance, the gain in database integrity might prove attractive. It's also worth noting that the lack of such support prior to MongoDB 4.x was a major criticism leveled against MongoDB, and slowed its adoption at the corporate level.
Invoking transaction support impacts read preferences and write concerns. For more information, you can refer to https://docs.mongodb.com/manual/core/transactions/#transaction-options-read-concern-write-concern-read-preference. Although these topics are covered in detail later in the book, we can briefly summarize them by stating that read preferences allow you to direct operations to specific members of a replica set. For example, you might want to indicate a preference for the primary member server in a replica set rather than allowing any member, including secondaries, to be used in a read operation. Write concerns allow you to adjust the level of acknowledgement when writing to the database, thereby ensuring that data integrity is maintained. In MongoDB 4.x, you are able to set read preferences and write concerns at the transaction level, that in turn influences individual document operations.
In MongoDB version 4.2 and above, the 16 MB limit on transaction size is removed. Also, as of MongoDB 4.2, full support for multidocument transactions is added for sharded clusters. In addition, full transaction support is extended to replica sets whose secondary members are using the in-memory storage engine (https://docs.mongodb.com/manual/core/inmemory/).
MongoDB developers have often included read concerns (as mentioned previously) in their operations in order to shift the burden of response from the primary server in a replica set to its secondaries instead. This frees up the primary to process write operations. Traditionally, MongoDB, prior to version 4, blocked such secondary reads while an update from the primary was in progress. The block ensured that any data read from the secondary would appear exactly the same as data read from the primary.
The downside to this approach, however, was that while the block was in place, the secondary read operation had to wait, which in turn negatively impacted read performance. Likewise, if a read operation was requested prior to a write, it would hold up update operations between the primary and secondary, negatively impacting write performance.
Because of internal changes introduced in MongoDB 4.x to support multidocument transactions, storage engine timestamps and snapshots are now used, which has the side effect of eliminating the need to block secondary reads. The net effect is an overall improvement in consistency and lower latency in terms of reads and writes. Another way to view this enhancement is that it allows an application to read from a secondary at the same time writes are being applied without delay.
A big problem with versions of MongoDB prior to 4.4 is that the following commands error out if an index build (https://docs.mongodb.com/master/core/index-creation/#index-builds-on-populated-collections) operation is in progress:
In MongoDB 4.4, when this happens, an attempt to force the in-progress index build operation is made. If successful, the index build is halted, and the drop*() operation continues without error. In the case of a drop*() performed on a replica set, the abort attempt is made on the primary. Once the primary commits to the abort, it then synchronizes to the secondaries.
There are a number of other new features that do not represent a massive paradigm shift, but are extremely useful nonetheless. These include improvements to the aggregation pipeline, field-level encryption, password() prompt, and wildcard indexes. Let's first have a look at aggregation pipeline improvements.
Another major new feature we discuss here involves the introduction of $convert, a new aggregation pipeline operator. This new operator allows the developer to change the data type of a document field while being processed in the pipeline. Target data types include double, string, Boolean, date, integer, long, and decimal. In addition, you can convert a field in the pipeline to the data type objectId, which is critically useful when you need direct access to the autogenerated unique identification field _id. For more information on the aggregation pipeline operator and $convert, go to https://docs.mongodb.com/master/core/aggregation-pipeline/#aggregation-pipeline.
The official programming language drivers for MongoDB 4.2 now support client-side field-level encryption. The implications for security improvements are enormous. This enhancement means that your applications can now provide end-to-end encryption for transmitted data down to the field level. So you could have a transmission of data from your application to MongoDB that includes, for example, an encrypted national identification number mixed in with otherwise plain-text data.
In many cases, it is highly undesirable to include a hard-coded password in a Mongo script. Starting with MongoDB 4.2, in place of a hard-coded password value, you can substitute a built-in JavaScript function passwordPrompt(). You can refer to https://docs.mongodb.com/master/reference/method/passwordPrompt/#passwordPrompt for more details on the function. This causes the Mongo shell to pause and wait for manual user input before proceeding. The password that is entered is then used as the password value.
Starting with MongoDB 4.2, support has been added for wildcard indexes (https://docs.mongodb.com/master/core/index-wildcard/#wildcard-indexes). This feature is useful for situations where the index is either not yet available or is unknown. For situations where the field is known and well established, it's best to create a normal index. There are cases, however, where you have a subset of documents that contain a particular field otherwise lacking in other documents in the collection. You might also be in a situation where a field is added later, and where the DBA has not yet had a chance to create an index on the new field. In these cases, adding a wildcard index allows MongoDB to perform a query more efficiently.
Starting with MongoDB 4.2, support for the Extended JSON v2 (https://docs.mongodb.com/master/reference/mongodb-extended-json/#mongodb-extended-json-v2) specification has been enabled for the following utilities:
Starting with MongoDB 4.2, there are now five verbosity log levels, each revealing increasing amounts of information. In MongoDB 4.0.6, you can now set a threshold on the maximum time it should take for data to replicate between members of a replica set. It's now possible to get the information from the MongoDB log file if that time is exceeded.
A further enhancement to diagnostics capabilities includes additional fields that are added to the output of the db.serverStatus() command that can be issued from a Mongo shell.
MongoDB 4.4 adds the ability to perform a hedged read (https://docs.mongodb.com/master/core/sharded-cluster-query-router/#hedged-reads) on a sharded cluster. By setting a hedged read preference option, applications are able to direct read requests to servers in replica sets other than the primary. The advantage of this approach is that the application simply takes the first result response, improving performance. The potential cost, of course, is that if a secondary responds, the data might be slightly out of date.
MongoDB version 4.4 introduces support for TCP Fast Open (TCO) connections. For this to work, it must be supported by the operating system hosting MongoDB. The following configuration file (and command line) parameters have been added to enable and control support under the setParameter configuration option: tcpFastOpenServer, tcpFastOpenClient, and tcpFastQueueSize. In addition, four new TCO-related information counters have been added to the output of the serverStatus() database command.
MongoDB version 4.4 introduces a new operator, $natural, which is used in a cursor.hint() operation. This operator causes the results of a sort operation to return a list in natural (also called human-readable) order. As an example, take these values:
['file19.txt','file10.txt','file20.txt','file8.txt']
An ordinary sort would return the list in this order:
['file10.txt','file19.txt','file20.txt','file8.txt']
Whereas a natural sort would return the following:
['file8.txt','file10.txt','file19.txt','file20.txt']
The first enhancement we examine is related to nonblocking secondary reads (as mentioned earlier). After that, we cover shard migration, authentication, and stream enhancements.
One of the major new features introduced in MongoDB version 3 was the integration of the WiredTiger storage engine. Prior to December 2014, WiredTiger Inc. was a company that specialized in database storage engine technology. Its impressive list of customers included Amazon Inc. In December 2014, WiredTiger was acquired by MongoDB after partnering with them on multiple projects.
In MongoDB version 3, multidocument transactions were not supported. Furthermore, in the replication process (https://docs.mongodb.com/manual/replication/#replication), changes accepted by the primary server in a replica set were pushed out to the secondary servers in the set, which were largely controlled through oplogs (https://docs.mongodb.com/manual/core/replica-set-oplog/#replica-set-oplog) and programming logic outside of the storage engine. Simply stated, the oplog represents changes made to the database. When a secondary synchronizes with a primary, it creates a copy of the oplog and then applies the changes to its own local copy of the database. The logic in place that controlled this process in MongoDB 3 was quite complicated and consumed resources that could otherwise have been used to satisfy user requests.
In MongoDB 4, the internal update mechanism of WiredTiger, the storage engine, was rewritten to include a timestamp in each update document. The main reason for this change was to provide support for multidocument transactions. It was soon discovered, however, that this seemingly simple change could potentially revolutionize the entire replication process.
In MongoDB 4, much of the logic required to ensure data integrity during replication synchronization has now been shifted to the storage engine itself, which in turn frees up resources to service user requests. The net effect is threefold: improved data integrity, read operations producing a more up-to-date set of documents, and improved performance.
Typically, DevOps engineers distribute the database into shards to support a massive amount of data. There comes a time, however, when the data needs to be moved. For example, let's say a host server needs to be upgraded or replaced. In MongoDB version 3.2 and earlier, this process could be quite daunting. In one documented case, a 500 GB shard took 13 days to migrate. In MongoDB 3.4, parallelism support was provided that sped up the migration process. Part of the reason for the improvement was that the chunk balancer logic was moved to the config server (https://docs.mongodb.com/manual/core/sharded-cluster-config-servers/#config-servers), which must be configured as part of a replica set.
Another improvement, available with MongoDB 4.0.3, allows the sharded cluster balancer (https://docs.mongodb.com/manual/core/sharding-balancer-administration/#sharded-cluster-balancer) to preallocate chunks if zones and ranges have been defined, which facilitates rapid capacity expansion. DevOps engineers are able to add and remove nodes from a sharded cluster in real time. The sharded cluster balancer handles the work of rebalancing data between the nodes, thereby alleviating the need for manual intervention. This gives DevOps engineers the ability to scale database capacity up or down on demand. This feature is especially needed in environments that experience seasonal shifts in demand. An example would be a retail outlet that needs to scale up its database capacity to support increased consumer spending during holidays.
As the database is updated, changes are recorded in the oplog maintained by the primary server in the replica set, which is then used to replicate changes to the secondaries. Trying to read a list of changes via the oplog is a tedious and resource-intensive process, so many developers choose to use change streams (https://docs.mongodb.com/manual/changeStreams/?jmp=blog&_ga=2.5574835.1698487790.1546401611-137143613.1528093145#change-streams) to subscribe to all changes on a collection. For those of you who are familiar with software design patterns, this is a form of the publish/subscribe pattern.
Aside from their obvious use in troubleshooting and diagnostics, changing streams can also be used to give an indicator of whether or not data changes are durable.
What is new and different in MongoDB 4.x is the introduction of a startAtOperationTime parameter that allows you to specify the timestamp at which you wish to tap into the change stream. This timestamp can also be in the past, but cannot extend beyond what is recorded in the current oplog.
If you enter 4.0 as a value of another parameter, featureCompatibilityVersion, then the streams return token data, used to restart the stream, in the form of a hex-encoded string, which gives you greater flexibility when comparing blocks of token data. An interesting side effect of this is that a replica set based on MongoDB 4.x could theoretically make use of a change stream token opened on a replica set based on MongoDB 3.6. Another new feature in MongoDB 4 is that change streams that are opened on multidocument transactions include the transaction number.
There were many security improvements introduced in MongoDB 4, but here, we highlight the two most significant changes: support for SHA-256 and transport layer security (TLS) handling.
SHA stands for secure hash algorithm. SHA-256 is a hash function (https://csrc.nist.gov/Projects/Hash-Functions) derivative of the SHA-2 family. The significance of offering SHA-256 support is based on the difference between the SHA-1, which MongoDB supports, and SHA-2 families of hash algorithms. SHA-1, introduced in 1995, used algorithms similar to an older family of hash functions: MD2, MD4, and MD5. SHA-1, however, produces a hash value of 160 bits compared with 128 for the MDx series. SHA-256, introduced in 2012, increases the hash value size to 256, which makes it exponentially more difficult to crack. Attack vectors that could compromise communications based upon SHA-1 and SHA-2 include the preimage attack, the collision attack, and the length-extension attack.
The first attack relies upon brute-force attack methods to reverse the hash. In the past, this required computational power beyond the reach of anyone other than a well-funded organization (for example, a government agency or a large corporation). Today, a normal desktop computer could have multiple cores, plenty of memory, and a graphics processing unit (GPU) that are easily capable of such attacks. To launch the attack, the attacker would need to be in a place where access to the database itself is possible, which means that other layers of security (such as the firewall) would have first been breached.
A collision attack uses two different messages that produce the same hash. Once the match has been found, it is mathematically possible to interfere with TLS communications. The attacker could, for example, start forging signatures, which would wreak havoc on systems dependent on digitally signed documents. The danger of this form of attack is that it can theoretically be successfully launched in half the number of iterations compared with a preimage attack.
At the time of writing, the SHA-256 hash function is immune to both preimage and collision attacks; however, both the SHA-1 and SHA-2 family of hash functions, including SHA-256, are vulnerable to length-extension attacks. This attack involves adding to the message, thereby extending its length and then recalculating the hash. The modified message is then seen as valid, allowing the attacker a way into the communication stream. Unfortunately, even though SHA-256 is resistant to this form of attack, it is still vulnerable.
Transport layer security (TLS) was introduced in 1999 to address serious vulnerabilities inherent in all versions of the Secure Sockets Layer (SSL). It is highly recommended that you secure your MongoDB installations with TLS 1.1 or above (covered later in this book). Once you have configured your mongod instances to use TLS, all communications are affected. These include communications between clients, drivers, and the server, as well as internal communications between members of a replica set and between nodes in a sharded cluster.
TLS security depends on which block cipher algorithm and mode are selected. For example, the 3DES (Data Encryption Standard 3) algorithm with the Cipher Block Chaining (CBC) mode are considered vulnerable to attack even in TLS version 1.2! The Advanced Encryption Standard (AES) algorithm and Galois Counter Mode (GCM) are considered a secure combination, but are only supported in TLS versions 1.2 and 1.3 (ratified in 2018). It should be noted, however, that the AES-256 and GCM combination is not supported when running the MongoDB Enterprise edition on a Windows server.
Using any form of SSL with MongoDB is now deprecated. TLS 1.0 support is also disabled in MongoDB 4.x and above. Ultimately, the version of TLS you end up using in your MongoDB installation completely depends on what cryptographic libraries are available for the server's operating system. This means that as you upgrade your OS and refresh your MongoDB 4+ installation, TLS support is also automatically upgraded. Currently, MongoDB 4+ uses OpenSSL on Linux hosts, Secure Channel on Windows, and Secure Transport on the Mac.
As of MongoDB 4.4, the Mongo shell now issues a warning if the x.509 certificate is due to expire within the next 30 days. Likewise, you now see log file messages if there is a pending certificate expiration between mongod instances in a sharded cluster or replica set.
If you are not familiar with the concept of backward incompatibilities, then you have probably not yet survived a major upgrade! To give you an idea of how important it is to be aware of this when reviewing the change log for MongoDB, this concept is also referred to as code breaks...as in things that can break your code.
MMAPv1 (https://docs.mongodb.com/manual/storage/#mmapv1-storage-engine), the original MongoDB storage engine, has been deprecated in MongoDB 4.0 and removed as of MongoDB 4.2. It has been replaced by WiredTiger, which has been available since MongoDB version 3.0. WiredTiger has been the default since MongoDB version 3.2, so there is a good chance that this backward-incompatible change does not affect your applications nor installation.
If your installation was using the MMAPv1 storage engine before the upgrade, then you immediately notice more efficient memory and disk-space allocation. Simply put, MMAPv1 grabbed as much free memory as it could, and would allocate additional disk space with an insatiable appetite. This made a MongoDB 3 installation using MMAPv1 a bad neighbor on a server that is also doing other things!
Another difference that DevOps engineers appreciate is that embedded documents no longer continue to grow in size after being created. This was an unwanted side effect produced by the MMAPv1 storage engine, which could have potentially affected write performance and lead to data fragmentation.
If, as a result of an upgrade from MongoDB 3 to 4, you are in the unfortunate position of having to update a database that is stored using the MMAPv1 storage engine, then you must manually back up the data on each server prior to the MongoDB 4.x upgrade. After the upgrade has completed, you then need to perform a manual restore.
Communications between members of a replica set are governed by an internal protocol simply referred to as pv0 or pv1. pv0 was the original protocol. Prior to MongoDB 3.2, the only version available was pv0. MongoDB 4.x dropped support for pv0 and only supports pv1. Accordingly, before you perform an upgrade to MongoDB 4, you must reconfigure all replica sets to pv1 (https://docs.mongodb.com/manual/reference/replica-set-protocol-versions/#modify-replica-set-protocol-version).
Fortunately, this process is quite easy, and can be accomplished by this simple procedure. These steps must then be repeated for each replica set: verify oplog entry replication and upgrade to pv1. Let's go into more detail regarding these two steps.
These steps must be performed on each secondary in the replica set:
mongo --host <address of secondary>
You can now upgrade the replica set protocol version to pv1:
mongo --host <address of primary>
cfg = rs.conf();
cfg.protocolVersion=1;
rs.reconfig(cfg);
A number of the new features available in MongoDB 4.x only work if you update the setFeatureCompatibilityVersion parameter (https://docs.mongodb.com/master/reference/command/setFeatureCompatibilityVersion/#setfeaturecompatibilityversion) in the admin database. The new features affected include the following:
To view the current featureCompatibility setting, go through the following steps:
mongo --username <name of user> --password
db.adminCommand({getParameter:1, featureCompatibilityVersion:1})
To perform the update, go through the following steps:
mongo --username <name of user> --password
db.adminCommand( { setFeatureCompatibilityVersion: "<VERSION>" } )
When establishing security for database users, you have a choice of several different approaches. One of the most popular approaches is challenge-response. Simply put: the database challenges the user to prove their identity. The response (in most cases), is a username and password combination. In MongoDB 3, this popular approach was implemented by default using MONGODB-CR (MongoDB Challenge Response). As of MongoDB 4, this mechanism is no longer available. This means that when you upgrade from MongoDB 3 to MongoDB 4, you must implement at least its replacement, Salted Challenge Response Authentication Method (SCRAM).
If your user credentials are in MONGODB-CR format, then you must use the following command to upgrade to SCRAM format:
db.adminCommand({authSchemaUpgrade: 1});
It is critical that you perform this upgrade while still running MongoDB 3. The reason for this is that the authSchemaUpgrade parameter has been removed in MongoDB 4! Another side effect of upgrading to SCRAM is that your application driver might also be affected. Have a look at the SCRAM Driver Support table in the documentation at https://docs.mongodb.com/manual/core/security-scram/#driver-support to be sure. The minimum programming language driver for Python that supports SCRAM authentication, for example, is version 2.8.
Any command or executable binary that is removed can potentially cause problems if you are relying on these as part of your application or automated maintenance procedures. Any application that relies upon an item that is been deprecated should be examined and scheduled to be rewritten in a timely manner.
The following table shows the removed items:
| Item | Type | Notes |
| mongoperf | Binary | Used to measure disk I/O performance without having to enter a mongo shell or otherwise access MongoDB. |
| $isolated | Operator | This operator was used in previous versions of MongoDB during update operations to prevent multidocument updates from being read until all changes took place. MongoDB 4.x uses transaction support instead. Any commands that include this operator need to be rewritten. |
The following table shows the removed items:
| Item | Notes | Type |
| copyDb clone |
Command | Copies an entire database. Although this command is still available, you cannot use it to copy a database managed by a mongod version 4 instance to one managed by a mongod version 3.4 or earlier instance. Use the external binaries mongodump and mongorestore instead after upgrading to MongoDB 4. |
| db.copyDatabase() | Mongo shell command | This is a wrapper for the copyDb command. The same notes for copyDb clone apply. |
| db.cloneDatabase() | Mongo shell command | This is a wrapper for the clone command. The same notes for copyDb clone apply. |
| geoNear | Command |
Reads geospatial information (that is, latitude and longitude) and returns documents in order, based on their proximity to the source point. Instead of this command, in MongoDB 4.x, you would use the $geoNear aggregation stage operator or the $near or $nearSphere query operators, depending on the nature of the query you wish to construct. |
These commands were deprecated in MongoDB 4.0 and removed as of MongoDB 4.2.
Another compatibility issue comes from the TLS/SSL configuration options. In MongoDB 3 and MongoDB 4.0, in the configuration files for both mongod (MongoDB database daemon) and mongos (which is used to control a sharded cluster), you could add a series of options under the net.ssl (https://docs.mongodb.com/v4.0/reference/configuration-options/#net-ssl-options) key. As of MongoDB 4.2, these options are deprecated in favor of the net.tls options. The net.tls options have enhanced functionality compared with the net.ssl options. These are covered in detail in Chapter 11, Administering MongoDB Security.
In this chapter, you learned about the most important features that were added to MongoDB version 4. These were broken down into three categories: new features, security enhancements, and things to avoid during an upgrade from MongoDB 3 to 4.
One important new feature was adding timestamps to the WiredTiger storage engine, which opened the doors for multidocument transaction support and nonblocking secondary read enhancements. Other internal enhancements include improvements in the shard-migration process, which significantly cuts down the time required for this operation to complete.
In the realm of security, you learned about how SHA-256 support gives you greater security when communicating with the MongoDB database, and also with communications between servers within a replica set or sharded cluster. You also learned that TLS 1.0 support has been removed, and that the new default is TLS 1.1. MongoDB 4.x even provides support for the latest version of TLS, version 1.3, but only if the underlying operating system libraries provide support.
Finally, as the original MongoDB storage engine, MMAPv1, has been removed in favor of WiredTiger, if your original MongoDB 3 installation had data that used MMAPv1, you need to back up while still running MongoDB 3 and then restore after the MongoDB 4.x upgrade has occurred. You were also presented with a list of the most significant items, including binary executables, parameters, and commands, which have been removed, and those which have been deprecated.
In the next chapter, you learn how to install MongoDB 4.x and its Python programming language driver.
This chapter helps you install everything you need to use MongoDB 4.x with the Python programming language. In this chapter, you first learn about installing MongoDB on a customer site, including requirements for random-access memory (RAM), the central processing unit (CPU), and storage. Next, you learn the differences between a MongoDB 3 and a MongoDB 4.x installation. After that, you learn to install and configure MongoDB 4.x along with the PyMongo driver. At the end of the chapter, you learn how to download the source code and sample data for the book, as well as set up a demo test environment you can use to follow the book examples.
In this chapter, we cover the following topics:
The minimum recommended hardware are:
The software requirements are:
Installation of the required software and how to restore the code repository for the book are explained in this chapter. The code used in this chapter can be found in the book's GitHub repository at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/chapters/02.
In this section, you are given an overview of what is needed to install MongoDB on a server running Linux, Windows, or macOS, located at a customer site. Later, in another section, you are shown how to create a virtual test environment in which you can practice the techniques discussed in this book. Before you can start on the installation, however, it is important to review which version of MongoDB is desired, as well as host computer hardware requirements.
There are a number of versions of MongoDB 4.x available, including the following:
The last product listed is free and tends to have a faster release cycle. Interestingly, MongoDB asserts that the Community Edition, unlike the model used by other open source companies, uses the same software pool for its beta versions as does the Enterprise Edition. The Enterprise Edition, when purchased, also includes support as well as advanced security features, such as Kerberos and Lightweight Directory Access Protocol (LDAP) authentication.
Let's now look at the RAM, CPU, and storage requirements.
The minimum CPU requirements for MongoDB are quite light. The MongoDB team recommends that each mongod instance be given access to two cores in a cloud environment, or a multi-core CPU when running on a physical server. The WiredTiger engine is multithreaded and is able to take advantage of multiple cores. Generally speaking, the number of CPU cores available improves throughput and performance. When the number of concurrent users increases, however, if operations occupy all available cores, a drastic downturn in throughput and performance is observed.
As of MongoDB 3.4, the WiredTiger internal cache will default either to 50% of RAM (1 GB), or 256 megabytes (MB), whichever is larger.
Accordingly, you need to make sure that you have enough RAM available on your server to handle other OS demands. So, following this formula, if your server has 64 GB of RAM, MongoDB will grab 31.5 GB! You can adjust this parameter, of course, by setting the cacheSizeGB parameter in the MongoDB configuration file. Values for <number>, as illustrated in the following code snippet, represent the size in GB, and can range from 0.25 to 10,000:
storage.wiredTiger.engineConfig.cacheSizeGB : <number>
Another consideration is what type of RAM is being used. In many cases, especially in corporate environments where server purchasing and server maintenance are done by different groups, memory might have been swapped around to the point where your server suffers from non-uniform memory access (NUMA) problems (https://queue.acm.org/detail.cfm?id=2513149).
The amount of disk space used by MongoDB depends on how much data you plan to house, so it's difficult to give you an exact figure. The type of drive, however, will have a direct impact on read and write performance. For example, if your data is able to fit into a relatively smaller amount of drive space, you might consider using a Solid State Drive (SSD) rather than a standard hard disk drive due to radical differences in speed and performance. On the other hand, for massive amounts of data, an SSD might not prove economical.
Here are some other storage recommendations for MongoDB:
A critical aspect of storage requirements has to do with how many Input/Output Operations Per Second (IOPS) the disk supports. If your use case requires 1,000 operations per second and your disk supports 500 IOPS, you will have to shard to achieve your ideal performance requirements. Additionally, you should consider the compression feature of the WiredTiger storage engine. As an example, if you have 1 terabyte (TB) of data, MongoDB will typically achieve a 30% compression factor, reducing the size of your physical disk requirements.
Each OS offers a choice of filesystem types. Some filesystem types are older and less performant. However, they are not good candidates to run MongoDB. There are two main reasons for this: the need to support legacy applications and legacy files. The other reason is that OS vendors want to give their customers more choice.
The recommended filesystem types are summarized in this table:
| OS | Filesystem | Notes |
| Linux kernel 2.6.25+ | XFS | This is the preferred filesystem. |
| Linux kernel 2.6.28+ | EXT4 | The next best choice for MongoDB on Linux. |
| Windows Server 2019, 2016, 2012 R2 | ReFS | Resilient File System (ReFS). |
Let's now look at other server considerations.
In addition to the hardware considerations listed previously, here are some additional points to consider:
The next section in this chapter expands upon this discussion, taking into consideration the differences between installing MongoDB 3 and installing MongoDB 4.x.
Now that you have an idea what the requirements are in terms of the OS, filesystem, and server hardware, and understand the most important differences between MongoDB 3 and MongoDB 4.x installation, it's time to have a look at the actual installation process. In this section, we cover a general overview of MongoDB installation that can be applied either at a customer site or inside a demo environment running in a Docker container on your own computer.
For those of you who have experience in MongoDB 3, in the next section, we briefly discuss major differences in installation between version 3 and 4.x.
As mentioned in the first chapter, one of the main differences between MongoDB 3.x and MongoDB 4.x is that the WiredTiger storage engine is now the default. Accordingly, you will need to ensure that you take advantage of WiredTiger's multithreaded capabilities by adjusting the RAM and CPU usage. There is an excellent guideline given here: https://docs.mongodb.com/manual/administration/production-notes/#allocate-sufficient-ram-and-cpu.
One of the main differences between installing MongoDB 3.x compared to MongoDB 4.x is the list of operating systems supported. When considering upgrading a server currently running MongoDB 3.x, consult the supported platforms guide (https://docs.mongodb.com/manual/installation/#supported-platforms) in the MongoDB documentation. Generally speaking, each new version of MongoDB removes support for older OS versions and adds support for new versions. Also, as a general consideration, where versions of MongoDB below v3.4 supported 32-bit x86 platforms, in MongoDB 3.4 and later—including MongoDB 4.x—there is no support for 32-bit x86 platforms.
Another difference you will observe is that as of MongoDB 4.4, time zone data is now incorporated into the installation program, as seen in this screenshot:

The next section shows you how to install MongoDB 4.x on a Linux-based platform.
It's a fact that there are literally hundreds of Linux distributions available. Most are free, but there are others for which you must pay. Many of the free Linux vendors also offer enterprise Linux versions that are stable, tested, and offer support. Obviously, for the purposes of this book, it would be impossible to detail the MongoDB installation process on every single distribution. Instead, we will concentrate on two main families of Linux: the RPM- (formerly RedHat Package Manager: https://rpm.org/) based and the Debian-based distributions.
Of the RPM-based distributions, we find RedHat, Fedora, and CentOS, among others, which are extremely popular. On the Debian side, we have Kali Linux and Ubuntu (and its offshoots!). There are many other distributions—and even other major families—that are not covered in this book. Among the more well-known distributions not covered are SUSE, pacman, Gentoo, and Slackware.
MongoDB Linux installations all tend to follow the same general procedure, detailed as follows:
Here is a screenshot of the start of the main installation on an Ubuntu 20.04 server:

Once the main installation has completed, you will note that the installation script creates a user and group named mongodb. Here is a screenshot of the end of the main installation process:

The script also creates a /etc/mongod.conf default configuration file, covered later in this chapter. To start the mongod instance using this configuration file, use this command:
mongod -f /etc/mongod.conf &
To access the database, issue the mongo command, which starts a shell, giving direct access to the database, as shown here:

Let's now look at the installation of MongoDB on Windows and macOS.
Installing MongoDB directly on a Windows server is actually easier than with Linux, as all you need to do is to go to the downloads website for MongoDB and choose the Windows installer script matching your Windows version. To start MongoDB, you would go to the Windows Task Manager, locate MongoDB, and choose start. You can also configure MongoDB to start automatically as a service. One final consideration is that the MongoDB Windows installation also automatically installs MongoDB Compass (discussed in more detail in Chapter 9, Handling Complex Queries in MongoDB).
Next, we look at the MongoDB configuration.
In the previous section, you learned how to install and then start MongoDB. When starting MongoDB in this manner, you were actually using the default configuration options. It is extremely important that you understand some of the different startup options located in a file we will refer to as mongod.conf, or the MongoDB configuration file, throughout the rest of the book. In this section, we cover MongoDB configuration concepts that can be applied either at a customer site or inside a demo environment running in a Docker container on your own computer.
The name and location will vary slightly between operating systems. The table shown here describes the defaults for the four major operating systems covered in the previous section on installation:
| OS | MongoDB configuration file |
| DEB-based | /etc/mongod.conf |
| RPM-based | /etc/mongod.conf |
| Windows | C:\Program Files\MongoDB\Server\4.x\bin\mongod.cfg |
| Mac OS X | /usr/local/etc/mongod.conf |
Before getting into the details, let's have a look at the syntax.
The MongoDB configuration file is an American Standard Code for Information Interchange (ASCII) text file using the YAML Ain't Markup Language (YAML) style of formatting, with the following features:
key : value
heading:
heading:
key1: value1
key2: value2
# This is a comment
We will now examine some of the more important configuration options.
It is important to provide configuration to direct how the mongod server daemon starts. Although you could theoretically start mongod using command-line options, most DevOps prefer to place a permanent configuration into a configuration file. Configuration options fall into these major categories:
In addition, the Enterprise (that is, paid) version of MongoDB features auditing and Simple Network Management Protocol (SNMP) options. Of these options in this chapter, we will only focus on the Core category. Other options will be covered in later chapters as those topics arise.
Under the storage key, the following table summarizes the more widely used options:
| Key | Sub key | Notes |
| dbPath | Directory where the actual database files reside. | |
| journal | enabled | Possible values: true | false. If set true, journaling is enabled. |
| journal | commitIntervalMs | This represents the interval between journal commits. Values can be from 1 to 500 milliseconds. The default is 100. |
| engine | The default is wiredTiger. You can also specify inMemory, which activates the in-memory storage engine. |
There are a number of other parameters as well; however, these are covered in other chapters in the book as appropriate.
It is important for troubleshooting and diagnostics purposes to activate MongoDB logging. Accordingly, there are a number of parameters that can be activated under the systemLog key, as summarized in this table:
| Key | Notes |
| path | Directory where log files will be created and stored. |
| logAppend | When set to true, after a MongoDB server restart, new entries are appended to the existing log entries. If set to false (default), upon restart, the existing log file is backed up and a new one is created. |
| verbosity | Determines how much information goes into the log. Verbosity levels range from 0 to 5, with 0 as the default. Level 0 includes informational messages. Setting verbosity to 1 through 5 increases the amount of information in the log message as well as debug information. |
| component | This option opens up the possibility of creating more refinements in logging. As an example, to set logging verbosity to 1 for queries, but 2 for storage, add these settings: systemLog: component: query: verbosity: 1 storage: verbosity: 2 |
Let's now look at different network configuration options.
The net configuration file key allows you to adjust network-related settings. Here are some of the core options:
| Key | Notes |
| port | The port on which the MongoDB server daemon listens. The default is 2017. |
| bindIp | You can specify one or more Internet Protocol (IP) addresses as values. Instead of 127.0.0.1, you can also indicate localhost. If you wish to bind to all IP addresses, specify 0.0.0.0. Alternatively, starting with MongoDB 4.2, you can specify "*" (double quotes required). IP addresses can be IPv4 or IPv6. |
| bindIpAll | As an alternative to bindIp: 0.0.0.0, you can set a value of true for this key instead. |
| ipV6 | Set this parameter to true if you wish to use IPv6 addressing. |
| maxIncomingConnections | Set an integer value to limit the number of network connections allowed. The default is 65536. |
Another important networking option is net.tls; however, this is not covered in this chapter, but rather in the next chapter, Chapter 3, Essential MongoDB Administration Techniques.
Here is an example using the three directives discussed in this section:
storage:
dbPath: /var/lib/mongodb
journal:
enabled: true
systemLog:
destination: file
logAppend: true
path: /var/log/mongodb/mongod.log
net:
port: 27017
bindIp: 0.0.0.0
In this example, journaling is enabled, and the database files are stored in the /var/lib/mongodb directory. The system log is recorded in the /var/log/mongodb/mongod.log file, and new entries are added to the existing file. The MongoDB server daemon listens on port 27017 to requests from all IP addresses.
It's important to note that there is a one-to-one correspondence between configuration file settings and command-line switches. Thus, for quick testing, you can simply start the MongoDB server daemon with a command-line switch. If successful, this can then be added to the configuration file.
For example, let's say you are testing IPv6 routing. To confirm that the MongoDB server will listen to requests on a given IPv6 address, from the command line you could start the server daemon as follows:
# mongod --ipv6 --bind_ip fe80::42:a6ff:fe0c:a377
The --ipv6 option enables IPv6 support, and the bind_ip option specifies the address. If successful, these commands could then be added to the mongod.conf file.
First introduced in MongoDB 4.2, there are two magic keys added that are referred to as externally sourced or expansion directives. The two directives are __rest, allowing the use of a representational state transfer (REST) endpoint to obtain further configuration, and __exec, which loads configuration from the output of a shell or OS command.
The __rest expansion directive allows mongod or mongos to start with additional directives, or an entire configuration block, from an external REST request. Using parameters supplied with the directive, the server will issue an HTTP GET request to the REST application programming interface (API) endpoint specified and will receive its configuration in turn. The advantage of this approach is that it enables the centralization of startup directives across a large distributed MongoDB system.
An obvious disadvantage when making requests to remote resources is that it opens a potentially huge security vulnerability. Thus, at a minimum, requests to any remote endpoint must use HTTPS. In addition, operating system permissions must restrict read access of the configuration file containing the __rest expansion directive to only the mongod or mongos user.
Here is a summary of parameters needed in conjunction with the __rest expansion directive:
| Parameter | Notes |
| __rest | A string that represents the REST endpoint Uniform Resource Identifier (URI). |
| type | Indicates the formatting of the return value. Currently accepted values are either string or yaml. |
| trim | Defaults to none. You can otherwise set this value to whitespace, which causes leading and/or trailing whitespace to be removed from the returned value. |
| digest | The SHA-256 digest of the result. digest_key must also be supplied (next). |
| digest_key | Hex string representing the key used to produce the SHA-256 digest. |
Here is a brief example:
net:
tls:
certificateKeyFilePassword:
__rest: "https://continuous-learning.com/api/cert/certPwd"
type: "string"
In the preceding code example, the certificate key file password is provided by a REST request to continuous-learning.com.
The __exec expansion directive operates in much the same manner as the __rest directive. The main difference is that instead of consulting a REST endpoint, an operating system command is referenced, which in turn produces the result. Another difference is that the __exec directive could be used to load an entire configuration file. This gives DevOps the ability to automate mongod or mongos startup options.
As with the __rest directive, a number of parameters are associated with the __exec directive, as summarized here:
| Parameter | Notes |
| __exec | The OS command to be executed. Note that this command could well be a Python script. |
| type | Indicates the formatting of the return value. Currently accepted values are either string or yaml. |
| trim | Defaults to none. You can otherwise set this value to whitespace, which causes leading and/or trailing whitespace to be removed from the returned value. |
| digest | The SHA-256 digest of the result. digest_key must also be supplied (next). |
| digest_key | Hex string representing the key used to produce the SHA-256 digest. |
Here is a brief example:
net:
tls:
certificateKeyFilePassword:
__exec: "python /usr/local/scripts/cert_pwd.py"
type: "string"
In the preceding code example, the password for the certificate key file is provided by a cert_pwd.py Python script.
We do not cover the actual installation of Python itself, as that is beyond the scope of this book. However, we will cover the installation of the PyMongo driver, which sits between Python and MongoDB. Before we dive into the details of installing the PyMongo driver package, it's important to discuss potential driver compatibility issues.
Before you proceed to install the PyMongo driver, you might need to deal with driver compatibility issues. These issues are twofold, and are detailed as follows:
There is an excellent documentation reference that cross-references the version of PyMongo against the version of MongoDB. The table can be found here: https://docs.mongodb.com/ecosystem/drivers/pymongo/#mongodb-compatibility. In the table, for example, you will notice that MongoDB version 2.6 is supported by PyMongo driver versions 2.7 all the way up to 3.9. However, do you really want to be using MongoDB version 2? Probably not!
More relevant to this book is the support for MongoDB 4.x. As of the time of writing, the PyMongo driver version 3.9 supports both MongoDB 4.0 and 4.2. On the other hand, if you find that you need a slightly older version of the PyMongo driver, perhaps due to issues with the version of Python you are running (discussed next), you will find that MongoDB 4.0 is supported by PyMongo driver versions 3.7, 3.8, and 3.9.
In the same section of the MongoDB documentation, there is a follow-up table that lists which version of Python supports the various versions of the PyMongo driver. That table can be found here: https://docs.mongodb.com/ecosystem/drivers/pymongo/#language-compatibility.
Many operating systems automatically provide Python; however, more often than not, it's version 2. Interestingly, you will note that the PyMongo driver version 3.9 does in fact support Python version 2.7. If you are using Python version 3, again, you will find that the PyMongo driver version 3.9 supports Python 3.4 through 3.7.
So, in summary, the version of the PyMongo driver that you choose to install is dictated by both the version of MongoDB you plan to use and the version of Python. Next, we'll have a look at a tool called pip.
The Python package manager is called pip. You will need pip to install the PyMongo driver, but, just as with Python, there is pip for Python release 2 and also pip3 for Python 3. The pip command does not work with Python 3. If you do not have pip3 available, use the appropriate installation procedure for the operating system that will house the Python code you plan to create, which will communicate with MongoDB.
There are a number of ways in which you can install the PyMongo driver package, detailed as follows:
This section describes how to use pip3 to install the PyMongo driver package as well as where PyMongo files are stored in the filesystem. For more information on the latest PyMongo driver, you can consult the Python Package Index (PyPI) (see https://pypi.org/). Enter pymongo in the search dialog box, and a list of pertinent results is displayed. Clicking on the https://pypi.org/project/pymongo/ link, you will see the current version of PyMongo.
To use pip3 to install the PyMongo driver, open a Terminal window, and run the following command. Please note that you will most likely need either root or administrator privileges to perform this command:
pip3 install pymongo
Here is the output from an Ubuntu Linux 20.04 server:

We have now successfully installed PyMongo using pip. The next step is to test the connection.
For this simple test, all we need is the MongoClient class from the pymongo module. The example shown in this sub-section can be found at /path/to/repo/chapters/02/test_connection.py. Let's have a look at the code, as follows:
data = { 'first_name' : 'Fred', 'last_name' : 'Flintstone'}
import pprint
from pymongo import MongoClient
client = MongoClient('localhost')
test_db = client.test
In order to maintain the purity of the test, as we are likely to run it many times, we add a statement that removes all documents from the new collection, test_collection.
test_db.test_collection.delete_many({})
insert_result = test_db.test_collection.insert_one(data).inserted_id
find_result = test_db.test_collection.find_one()
pprint.pprint(insert_result)
pprint.pprint(find_result)
Here is a screenshot of how the output might appear:

In the next section, you will learn to retrieve and load the source code and sample data provided in the GitHub repository that accompanies this book.
Although the focus of this chapter is on installation, the remainder of the book teaches you how to implement and manage the major features of MongoDB (for example, replica sets, sharded clusters, and more). In addition, the book focuses on mastering MongoDB queries and developing applications based on Python. Accordingly, we have provided you with a Docker image that includes the following:
In this section, we will show you how to restore the source code accompanying this book from a GitHub repository and then bring online the demo environment using Docker and Docker Compose. First, we will walk you through the process of restoring the source code and sample data from the GitHub repository associated with this book.
In order to load the sample data and Python code needed to run the examples shown in this book, you will need to either clone the GitHub repository for the book or download and unzip it. The code repository for the book is located here:
https://github.com/PacktPublishing/Learn-MongoDB-4.x
How you can go about restoring the source code and sample data depends on whether or not you have git installed on your computer. Accordingly, we'll look at two techniques, with and without using git.
Here are a series of steps to restore the sample data using git:
https://github.com/PacktPublishing/Learn-MongoDB-4.x

git clone https://github.com/PacktPublishing/Learn-MongoDB-4.x.git /path/to/repo

If you prefer not to use git, another technique is to simply download the entire repository in the form of a ZIP file. You can then extract its contents wherever is appropriate on your computer. To use this approach, proceed as follows:
Now, let's have a look at the directory structure of the sample data.
The source code and sample data for the examples shown in the book are located in these directories under /path/to/repo:
Next, we will have a look at creating a demo environment you can use to test the examples shown in the book using Docker and Docker Compose.
Docker is a virtualization technology that has significant advantages over others (for example, VirtualBox or VMware) in that it directly uses resources from the host computer and is able to share those resources between virtual machines (VMs) (referred to as containers in the Docker reference guide). In this section, you learn how to configure a Docker container in which you can practice installing MongoDB following the discussion in this chapter. Following that, you learn how to configure a Docker container that can be used to practice using the source code and MongoDB query examples found in Chapter 3, Essential MongoDB Administration Techniques, to Chapter 12, Developing in a Secured Environment, of this book.
Before you can recreate the demo environment used to follow examples from this book, you will need to install Docker. In this section, we will not cover the actual installation of Docker as it's already well documented. Please note that in all cases, you will need to make sure that hardware virtualization support has been enabled for your computer's basic input/output system (BIOS). Also, for Windows, you will install Docker Desktop on Windows rather than the straight binary executable, as is the case for Linux- or Mac-based operating systems. In addition, for Windows, you will need to enable the Windows hypervisor. The Windows installation steps will give you more information on this.
In addition, to recreate the demo environment, you will need to install Docker Compose. Docker Compose is an open source tool provided by Docker that allows you to install a Docker container defined by a configuration file. As with Docker itself, installation instructions vary slightly between Linux, Windows, and macOS computer hosts.
A special Docker Compose configuration has been created that allows you to test your skills installing MongoDB. This container is drawn from the latest long-term support (LTS) version of the Ubuntu Linux image on Docker Hub. It also has the necessary prerequisites for installing MongoDB. The files to create and run this container are located in the source code repository for the book, at /path/to/repo/chapters/02.
Let's first have a look at the docker-compose.yml file first. It is in YAML file format (as is the mongod.conf file), so you must pay careful attention to indents. The first line indicates the version of Docker Compose. After that, we define services. The only service defined creates a container named learn-mongo-installation-1 and an image named learn-mongodb/installation-1. Two volumes are mounted to allow data to be retained even if the container is restarted.
Also, a file in the common_data directory is mounted as /etc/hosts, as illustrated in the following code block:
version: "3"
services:
learn-mongodb-installation:
container_name: learn-mongo-installation-1
hostname: installation1
image: learn-mongodb/installation-1
volumes:
- db_data_installation:/data/db
- ./common_data:/data/common
- ./common_data/hosts:/etc/hosts
Continuing with the services definition, we also need to map the MongoDB port 27017 to something else so that there will be no port conflicts on your host computer. We indicate the directory containing the Docker build information (for example, the location of the Dockerfile), and tell the container to stay open by setting stdin_open and tty both to true. Finally, still in the services block, we assign an IP address of 171.16.2.11 to the container, as illustrated in the following code block:
ports:
- 27111:27017
build: ./installation_1
stdin_open: true
tty: true
networks:
app_net:
ipv4_address: 172.16.2.11
In the last two definition blocks, we define a Docker virtual network, allowing the container to route through your local host computer. We also instruct Docker to create a volume called db_data_installation that resides outside the container. In this manner, data is retained on your host computer between container restarts. The code for this can be seen in the following snippet:
networks:
app_net:
ipam:
driver: default
config:
- subnet: "172.16.2.0/24"
volumes:
db_data_installation: {}
Next, we look at the main build file, /path/to/repo/chapters/02/installation_1/Dockerfile. As you can see from the following code block, this file draws from the latest stable version of Ubuntu and installs tools needed for the MongoDB installation:
FROM ubuntu:latest
RUN \
apt-get update && \
apt-get -y upgrade && \
apt-get -y install vim && \
apt-get -y install inetutils-ping && \
apt-get -y install net-tools && \
apt-get -y install wget && \
apt-get -y install gnupg
To build the container, use this command:
docker-compose build
To run the container, use this command (the -d option causes the container to run in the background):
docker-compose up -d
To access the container, use this command:
docker exec -it learn-mongo-installation-1 /bin/bash
Once the command shell into the installation container is established, you can then practice installing MongoDB following the instructions given in this chapter. When you are done practicing MongoDB installation, be sure to stop the container, using this command:
docker-compose down
Let's now have a look at creating the demo environment used in Chapter 3, Essential MongoDB Administration Techniques, to Chapter 12, Developing in a Secured Environment, of this book.
For more information about the directives in the docker-compose.yml file, please review the Overview of Docker Compose documentation article (https://docs.docker.com/compose/). Here is a useful list of Docker commands: https://docs.docker.com/engine/reference/commandline/container/.
In the root directory of the repository source code, you will see a docker-compose.yml file. This file contains instructions to create the infrastructure for the demo server you can use to test files discussed in Chapter 3, Essential MongoDB Administration Techniques, to Chapter 12, Developing in a Secured Environment, of the book. The virtual environment created through this docker-compose file contains not only MongoDB fully installed, but also, Python, pip, and the PyMongo driver have all been installed as well. The heart of this Docker container is based upon the official MongoDB image hosted on Docker Hub.
After the version identification, the next several lines contain services definitions, as follows:
version: 3
services:
learn-mongodb-server-1:
container_name: learn-mongo-server-1
hostname: server1
image: learn-mongodb/member-server-1
volumes:
- db_data_server_1:/data/db
- .:/repo
- ./docker/hosts:/etc/hosts
- ./docker/mongod.conf:/etc/mongod.conf
ports:
- 8888:80
- 27111:27017
build: ./docker
restart: always
command: -f /etc/mongod.conf
networks:
app_net:
ipv4_address: 172.16.0.11
In this example, there is only one definition, learn-mongodb-server-1, which is used for most of the examples in this book. In this definition is the name of the Docker container, its hostname, the name of the image, volumes to be mounted, and additional configuration information. You will also notice that an IP address of 172.16.0.11 has been assigned to the container. Once it is up and running, you can access it using this IP address.
The remaining two directives define the overall Docker virtual network and an external disk allocation where the Docker container will store MongoDB data files, as illustrated in the following code snippet:
networks:
app_net:
ipam:
driver: default
config:
- subnet: "172.16.0.0/24"
volumes:
db_data_server_1: {}
Docker Compose uses a /path/to/repo/docker/Dockerfile file for instructions on how to build the new image. Here are the contents of that file:
FROM unlikelysource/learning_mongodb
COPY ./init.sh /tmp/init.sh
RUN chmod +x /tmp/init.sh
RUN /tmp/init.sh
COPY ./run_services.sh /tmp/run_services.sh
RUN chmod +x /tmp/run_services.sh
CMD /tmp/run_services.sh
The first directive tells Docker to pull the base image from the image created expressly for this book. After that, an initialization script is copied and executed. Here are the contents of the initialization script:
#!/bin/bash
echo '***************************************************'
echo ' '
echo 'To shell into the Docker container, do this:'
echo ' docker exec -it learn-mongo-server-1 /bin/bash'
echo ' '
echo '(1) To restore sample data run this script:'
echo ' /path/to/repo/restore_data_inside.sh'
echo '(2) To restart Apache run this script:'
echo ' /path/to/repo/restart_apache_inside.sh'
echo ' '
echo '***************************************************'
The second script referenced starts MongoDB and Apache and then loops. This ensures both services remain running. Here is the other script, found at /path/to/repo/docker/run_services.sh. Since this script is long, let's take it block by block. The first block of scripting starts a mongod (that is, a MongoDB daemon) running in the background. If it fails to start, the script exits, stopping the Docker container from running. The code for this can be seen in the following snippet:
#!/bin/bash
/usr/bin/mongod -f /etc/mongod.conf --bind_ip_all &
status=$?
if [ $status -ne 0 ]; then
echo "Failed to start mongod: $status"
exit $status
fi
In a similar vein, the second block of code starts Apache running, as follows:
/usr/sbin/apachectl -k start &
status=$?
if [ $status -ne 0 ]; then
echo "Failed to start apache: $status"
exit $status
fi
The remainder of the script simply loops, and checks the process status for both mongod and Apache 2. If either of the process checks fail to return results, the script ends, causing the container to stop. The code can be seen in the following snippet:
while sleep 60; do
ps aux |grep httpd |grep -v grep
PROCESS_1_STATUS=$?
ps aux |grep apache2 |grep -v grep
PROCESS_2_STATUS=$?
# If the greps above find anything, they exit with 0 status
# If they are not both 0, then something is wrong
if [ -f $PROCESS_1_STATUS -o -f $PROCESS_2_STATUS ]; then
echo "One of the processes has already exited."
exit 1
fi
done
Now, let's have a look at getting the demo environment up and running.
To bring the demo environment online, open a Terminal window, and change to the directory where you restored the source code for the book. To build the new Docker image to be used to test examples from Chapter 3, Essential MongoDB Administration Techniques, to Chapter 12, Developing in a Secured Environment, run this command:
docker-compose build
To run the image, simply issue this command (the -d option causes the container to run in the background):
docker-compose up -d
After that, you can issue this command to open up a command shell inside the running container:
docker exec -it learn-mongo-server-1 /bin/bash
You should see a /repo directory that contains all the source code from the repository. If you then change to the /repo/chapters/02 directory, you can run the Python test program that confirms whether or not a connection to MongoDB is successful. Here is a screenshot of what you might see:

Finally, we will have a look at establishing communications between your local computer that is hosting the Docker container, and the container's Apache web server.
In a production environment (that is, on a server that is directly accessible via the internet), you would normally add a Domain Name Service (DNS) entry using the services of your Internet Service Provider (ISP). For development purposes, however, you can simulate a DNS address by adding an entry to the hosts file on your own local computer. For Linux and macOS computers, the file is located at /etc/hosts. For Windows computers, the file is located at C:\Windows\System32\drivers\etc\hosts. For our fictitious company Book Someplace, we add the following entry to the local hosts file:
172.16.0.11 learning.mongodb.local
At this point, from your host computer, in a Terminal window or Command Prompt from outside the Docker container, test the connection using ping learning.mongodb.local, as shown here:

And that concludes the setup instructions for the book.
In this chapter, you learned to recognize some of the hardware prerequisites for a MongoDB 4.x installation. Among the considerations discussed were how much memory was required, the type and nature of storage, and the file and operating systems. You then learned some of the differences between a MongoDB 3 and a MongoDB 4.x installation. The most important consideration is that support for some of the older operating systems (for example, Ubuntu 14.04 and Windows 8.1) has been—or will shortly be—removed.
You were given a general overview of how to install MongoDB on Linux, Windows, and macOS. You then learned about the more important configuration options and the location and nature of the configuration file. After that, you learned how to install the recommended Python driver, PyMongo. You learned that although there are many ways in which this driver can be installed, the preferred method is to use the pip3 Python package manager. The last section showed you how to download the source code and sample data for the book. You also learned how to set up a demo environment using Docker technology, to be used as a reference throughout the remainder of the book.
In the next chapter, you learn how to use the mongo shell to access the MongoDB database.
This chapter is aimed primarily at system administrators and database administrators (DBAs), but the information contained within it will also prove useful to developers. In this chapter, you learn how to perform administration critical to a properly functioning database. In this chapter, we will learn to use the mongo shell and generate ad hoc queries for management. We will then cover the validation and cleanup of data, and, by the end of the chapter, we will learn to perform backup and restore operations, as well as performance monitoring.
The following topics are addressed in this chapter:
Minimum recommended hardware:
Software requirements:
Installation of the required software and how to restore the code repository for the book are explained in Chapter 2, Setting up MongoDB 4.x. To run the code examples in this chapter, open a Terminal window (Command Prompt) and enter these commands:
cd /path/to/repo
docker-compose build
docker-compose up -d
docker exec -it learn-mongo-server-1 /bin/bash
When you are finished practicing the commands covered in this chapter, be sure to stop Docker as follows:
docker-compose down
The code used in this chapter can be found in the book's GitHub repository at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/chapters/03.
After installing MongoDB on a server, a separate executable named mongo is also installed. Here is an example of opening the shell without any parameters from the Docker container mentioned at the start of this chapter:

From this prompt, you are now able to view database information and query collections and perform other vital database operations.
There are a number of command-line switches associated with the mongo shell (https://docs.mongodb.com/manual/mongo/#the-mongo-shell), the most important of which are summarized here. You should note that the mongo shell is used to connect to a mongod server instance, which is typically running as a system daemon or service.
The following table presents a summary of the more important mongo shell command-line switches:
| Option | Example | Notes |
| --port | mongo --port 28888 | Lets you connect to a mongod instance that uses a non-default port. Note that the port can also be specified when using the --host flag (described next). |
| --host | mongo --host 192.168.2.57:27017 mongo --host mongo.unlikelysource.net |
This option lets you connect to a mongod instance running on another server. :port is optional. If left off, the port value defaults to 27017. |
| --username --password |
mongo --username doug --password 12345 | Connects to a mongod instance as an authenticated user. If the --password switch is left off, you will be prompted for the password. Use the --authenticationDatabase switch if you are not authenticating via the default admin database. |
| --tls | mongo --tls | Makes a secure connection using Transport Layer Security (TLS) (version 4.2 and above). |
| --help | mongo --help | Displays a help screen for running the mongo command. |
| --nodb | mongo --nodb | Lets you enter the shell without connecting to a database. |
| --shell | mongo --shell <javascript.js> | Enters the mongo shell after running an external script. |
All of the command-line switches summarized in the preceding table can be mixed together. Here is an example that connects to a remote mongod instance on IP address 192.168.2.57, which listens on port 28028. The user doug is authenticated using the security database. The connection is secured by TLS. Have a look at the following code snippet:
mongo --host 192.168.2.57:28028 --username doug \
--authenticationDatabase security --tls
There are a certain number of customization options you can perform on the mongo shell. Simply by creating a .mongorc.js file (https://docs.mongodb.com/manual/reference/program/mongo/#mongo-mongorc-file) in your user home directory, you can customize the prompt to display dynamic information such as the uptime, hostname, database name, number of commands executed, and so forth. This is accomplished by defining a JavaScript function to the prompt reserved variable name, whose return value is echoed to form the prompt.
Here is a sample .mongorc.js script that displays the MongoDB version, hostname, database name, uptime, and command count:
host = db.serverStatus().host;
cmd = 1;
prompt = function () {
upt = db.serverStatus().uptime;
dbn = db.stats().db;
return "\nMongoDB " + host + "@" + dbn
+ "[up:" + upt + " secs]\n" + cmd++ + ">";
}
By placing this file in the user home directory, subsequent use of the mongo command will display the modified prompt, as shown:

Let's now look at the shell command helpers.
When using the mongo shell interactively, there is a handy set of shell command helpers that facilitate discovery and navigation while in the shell. The command helpers are summarized here:
| Command | Options | Notes |
| use | <dbname> | Switches default database to <dbname> |
| help | Displays a list of commands with brief descriptions | |
| help | admin | List of administrative commands |
| help | connect | Help pertaining to the database connection |
| help | keys | Keyboard shortcuts |
| help | misc | Other uncategorized help |
| help | mr | Displays help on Map-Reduce |
| show | dbs | Shows database names |
| show | collections | When using a database, shows a list of collections |
| show | users | List of users for the current database |
| show | logs | List of accessible logger names |
| show | log <name> | Displays last entry of log <name>; global is the default |
| db.help() | Help on database methods | |
| db.<coll>.help() | Help on collection methods, where <coll> is a collection in the currently used database | |
| sh.help() | Help with sharding | |
| rs.help() | Help with replica set commands |
In the following example, we use shell helper commands to get a list of databases, use the admin database, and list collections in this database:

Let's now learn to use the mongo shell methods.
The mongo shell makes available a rich set of shell methods that include the following categories:
| Category | Includes methods that affect ... | Examples |
| Database | ... an entire database | db.serverStatus() |
| Collections | ... a collection within a database | db.<collection_name>.find() |
| Cursor | ... a cursor (result of a query) | db.<collection_name>.find().sort() |
| User management | ... database users | db.createUser() |
| Role management | ... the ability of a user role to perform operations | db.grantPrivilegesToRole() |
| Replication | ... servers that are members of a replica set | rs.initiate() |
| Sharding | ... servers that hold fragments (shards) of a collection | sh.addShard() |
| Free monitoring | ... monitoring of your overall MongoDB deployment | db.enableFreeMonitoring() |
| Constructors | ... the production of commonly needed information | ObjectId.toString() |
| Connection | ... the connection to the MongoDB instance | Mongo.getDB() |
| Native | ... commands that give limited access to the local server | hostname() |
In this chapter, we will focus on basic database, collection, cursor, and monitoring methods. In other chapters in this book, we will cover user and role management, replication, and sharding methods. Examples of shell methods that allow you to perform common database operations are covered in the next section of this chapter.
You can use the mongo shell to run scripts that use JavaScript syntax. This is a good way to permanently save complex shell commands or perform bulk operations. There are two ways to run JavaScript-based scripts using the mongo shell: directly from the command line, or from within the mongo shell.
Here is an example of running a shell script that inserts documents into the sweetscomplete database from the command line:
mongo /path/to/repo/sample_data/sweetscomplete_customers_insert.js
Here is the resulting output:

Let's now run a script from inside the shell.
You can also run a shell script from inside the mongo shell using the load(<filename.js>) command helper. After having first returned to the mongo shell, issue the use sweetscomplete; command. You can run the sweetscomplete_products_insert.js script that is located in the /path/to/repo/sample_data directory, as illustrated in the following code snippet:
mongo --quiet
use sweetscomplete;
show collections;
load("/path/to/repo/sample_data/sweetscomplete_products_insert.js");
show collections;
When you examine the output from this command next, note that you first need to use the database. We then look at the list of collections before and after running the script. Notice in the following screenshot that a new products collection has been added:

Let's now look at the shell script syntax.
It's very important to understand that you cannot use shell command helpers from an external JavaScript script, even if you run the script from inside the shell! Many of the shell methods you may wish to use reference the db object. When you are running shell methods from inside the shell, this is not a problem: when you issue the use command, this anchors the db object to the database currently in use. Since the use command is a shell command helper, however, it cannot be used inside an external script. Accordingly, the first set of commands you need to add to an external shell script is needed to set the db object to the desired database.
This can be accomplished as follows, where <DATABASE> is the name of the target database you wish to operate upon:
conn = new Mongo();
db = conn.getDB("<DATABASE>");
From this point on, you can use the db object as you would with any other shell method. Here is a sample shell script that assigns the db object to the sweetscomplete database, removes the common collection, and then inserts a document:
conn = new Mongo();
db = conn.getDB("sweetscomplete");
db.common.drop();
db.common.insertOne(
{
"key" : "gender",
"data" : { "M" : "male", "F" : "female" }
});
We now turn our attention to common database operations: inserting, updating, deleting, and querying.
Among the most important shell methods you will need to master, whether a database administrator or a developer, is to be able to create a database, add documents to collections, and perform queries. We will start by creating a new learn database.
Oddly, the shell method used to create a new database is not listed among the database commands! Instead, a new database is created by simply inserting a document into a collection. Using two simple commands, you will create the database, create the collection within the new database, and add a document!
To insert a document, you can use either the db.collection.insertOne() or the db.collection.insertMany() command. The syntax is quite simple: you specify the name of the collection into which you plan to insert a document, followed by the document to be inserted, using JavaScript Object Notation (JSON) syntax, as illustrated in the following diagram:

In this example, you will create a new learn database and a collection called chapters, as follows:
use learn;
db.chapters.insertOne({
"chapterNumber" : 3,
"chapterName" : "Essential MongoDB Administration Techniques"
});
Here is the output:

If the featureCompatibilityVersion parameter is set to 4.2 or less (for example, you're running MongoDB 4.2 or below), the maximum size of the combined database name plus the collection name cannot exceed 120 bytes. If, however, it is set to 4.4 or greater (for example, you're running MongoDB 4.4 or above), the maximum size of the combined database name plus the collection name can go up to 255 bytes.
The main command used to query a collection from the mongo shell is db.<collection_name>.find(). If you are only interested in a single result, you can alternatively use db.<collection_name>.findOne(). The find() and findOne() commands have two basic parameters: the query (also referred to as filter) and the projection, as illustrated in the following diagram:

The result of a query is a cursor. The cursor is an iteration, which means you need to type the it helper command in the mongo shell to see the remaining results.
As you can see from the preceding diagram, the query (also referred to as query criteria or filter) can be as simple as using JSON syntax to identify a document field, and the value it equals. As an example, we will use the customers collection in the sweetscomplete database. When you specify a query filter, you can provide a hardcoded value, as shown here:
db.customers.findOne({"phoneNumber":"+44-118-652-0519"});
However, you can also simply wrap the target value into a regular expression by surrounding the target string with delimiters ("/"). Here is a simple example, whereby we use the db.collection.findOne() command to return a single customer record where the target phone number is located using a /44-118-652-0519/ regular expression:

Let's say that the management has asked you to produce a count of customers. In this case, you can use a cursor method to operate on the results. To form this query, simply execute db.collection.find().count() without any parameters, as shown here:

Below the dotted line in the preceding diagram, you will note a more complex form of stating the query. In the diagram, $op would represent one of the query operators (also referred to as query selectors) that are available in MongoDB.
Query operators fall into the following categories:
As an example, let's say that the management wants a count of customers from non-English-speaking majority countries over the age of 50. The first thing we can do is create a JavaScript array variable of majority-English-speaking country codes. As a reference, we draw a list of countries from a document produced by the British government (https://www.gov.uk/english-language/exemptions). The code can be seen in the following snippet:
maj_english = ["AG","AU","BS","BB","BZ","CA","DM","GB","GD",
"GY","IE","JM","NZ","KN","LC","VC","TT","US"];
Next, we create two sub-phrases. The first addresses customers not in ($nin) the preceding list, as follows:
{"country" : { "$nin": maj_english }}
The second clause addresses the date of birth. Note the use of the less-than ($lt) operator in the following code snippet:
{"dateOfBirth" : {"$lt":"1968-01-01"}}
We then join the two clauses, using the $and query operator, for the final complete query, as follows:
db.customers.find(
{
"$and" : [
{"country" : { "$nin": maj_english }},
{"dateOfBirth" : {"$lt":"1968-01-01"}}
]
}).count();
Here is the result:

The second argument to the db.collection.find() and db.collection.findOne() commands, also using JSON syntax, is the projection argument. The projection argument allows you to control which fields appear or do not appear in the final output.
Here is a summary of the projection options:
| Projection expression | What appears in the output |
| { } (or blank) | All fields |
| { "field1" : 1 } | Only field1 and the _id fields |
| { "field1" : 0 } | All fields except for field1 |
| { "field1" : 1, "_id" : 0 } | Only field1 |
| { "field1" : 1, "field2" : 0 } | Not allowed! Cannot have a mixture of include and exclude (except for the _id field) |
For this example, we will assume that the management has asked for the name and email address of all customers from Quebec. Accordingly, you construct a query that uses the projection argument to only include fields pertinent to the name and address. You also suppress the _id field as it would only confuse those in management.
As an example, we first define a JavaScript variable that represents the query portion. Next, we define a variable that represents the projection. Finally, we issue the db.collection.find() command, adding the pretty() cursor modifier (discussed next) to improve the output appearance. Here is how that query might appear:
query = { "stateProvince" : "QC" }
projection = { "_id":0, "firstName":1, "lastName":1, "email":1, "stateProvince":1, "country":1 }
db.customers.find(query,projection).pretty();
Here is the output:

In addition to values of 1 or 0, you can also use projection operators to perform more granular operations on the projection. These are especially useful when working with embedded arrays, covered in later chapters of this book.
Updating a document involves two mandatory arguments: the filter and the update document arguments. The third (optional) argument is options. The primary shell methods used are either db.collection.updateOne(), which—as the name implies—only updates a single document, or db.collection.updateMany(), which lets you perform updates on all documents matching the filter. The former argument is illustrated in the following diagram:

The filter has exactly the same syntax as the query (that is, the first argument to the db.collection.find() command) discussed previously, so we will not repeat that discussion here. The update usually takes the form of a partial document, containing only the fields that need to be replaced with respect to the original. Finally, options are summarized here:
| Option | Values | Notes |
| upsert | true | false | If set to true, a new document is created if none matches the filter. The default is false. |
| writeConcern | write concern document | Influences the level of acknowledgment that occurs after a successful write. This is covered in more detail in Chapter 14, Replica Set Runtime Management and Development. |
| collation | collation document | Used to manage data from an international website capturing locale-aware data. |
| arrayFilters | array | Lets you create an independent filter for embedded arrays. |
In order to perform an update, the update document needs to use update operators. At a minimum, you need to use the $set update operator to assign the new value. The update operators are broken out into categories, as follows:
In addition, there are modifiers ($each, $position, $slice, $sort) that are used in conjunction with $push so that updates can affect all or a subset of an embedded array. Further, the $each modifier can be used with $addToSet to add multiple elements to an embedded array. A more detailed discussion of the array and modifier operators is presented in Chapter 10, Working with Complex Documents across Collections. In this section, we will only deal with the fields update operators.
The simplest—and probably most common—type of update would be to update contact information. First, it's extremely important to test your update filter by using it in a db.collection.find() command. The reason why we use db.collection.find() instead of db.collection.findOne() is because you need to ensure that your update only affects the one customer! Thus, if we wish to change the email address and phone number for a fictitious customer named Ola Mann, we start with this command:
db.customers.findOne({ "email" : "omann137@Chunghwa.com" });
Now that we have verified the existing customer email is correct and that our filter only affects one document, we are prepared to issue the update, as follows:
db.customers.updateOne(
{"email" : "omann137@Chunghwa.com"},
{ $set: {
"email" : "ola.mann22@somenet.com",
"phoneNumber" : "+94-111-222-3333" }
}
);
Here is the result:

Let's now cover data cleanup.
Updates are also often performed to clean up data. As an example, you notice that for some customers, the buildingName, floor, and roomApartmentCondoNumber fields are set to null. This can cause problems when customer reports are generated, and the management wants you to replace the null values with an empty string instead.
Because you will be using the updateMany() command, the potential for a total database meltdown disaster is great! Accordingly, to be on the safe side, here are the actions you can perform:
As mentioned previously, you should first craft a query to ensure that only those documents where the type is null pass the filter. Here is the query for the buildingName field:
db.customers.find({"buildingName":{"$type":"null"}},{"customerKey":1,"buildingName":1});
Here is the output:

Make a note of the customerKey field for the first entry on the list produced from the query. You can then run an updateOne() command to reset the null value to an empty string. You then confirm the change was successful using the customerKey field. Here is a screenshot of this sequence of operations:

Before the final update, perform a query to get a count of how many documents remain to be modified, as follows:
db.customers.find({"buildingName":{"$type":"null"}}).count();
You are now ready to perform the final update, using this command:
db.customers.updateMany({"buildingName":{"$type":"null"}}, \
{"$set":{"buildingName":""}});
And here is the result:

The command used to delete a single document is db.collection.deleteOne(). If you want to delete more than one document, use db.collection.deleteMany() instead. If you wish to delete an entire collection, the most efficient command is db.collection.drop().
Document deletion has features in common with both update and query operations. There are two arguments: the filter, which is the same as described in the preceding section on updates. The second argument consists of options, which are a limited subset of the options available during an update: only the writeConcern and collation options are supported by a delete operation.
The following diagram maps out the generic syntax for the db.collection.deleteOne() shell method:

For the purposes of illustration, let's turn our attention back to the learn database featured in the first section in this chapter on inserting documents. First, we use the load() shell helper method to restore the sample data for the learn database, as illustrated in the following screenshot:

You will notice that there is a duplicate document for Chapter 3. If we were to specify either the chapter number or chapter name as the query filter, we would end up deleting both documents. The only truly unique field in this example is the _id field. For this illustration, we frame a deleteOne() command using the _id field as the deletion criteria. However, as you can see from the following screenshot, you cannot simply state the value of the _id field: the deletion fails, the deleteCount value is 0, and the duplicate entry still remains:

Granted—we could frame a delete query filter using the chapter number and rely upon deleteOne() to only delete the first match; however, that's a risky strategy. It's much better to exactly locate the document to be deleted.
The reason why the delete operation fails is due to the fact that the _id field is actually an ObjectId instance, not a string! Accordingly, if we reframe the delete query filter as an ObjectId, the operation is successful. Here is the final command and result:

Let's now have a look at the essential operations of backup and restore.
Close your eyes, and picture for a moment that you are riding a roller-coaster. Now, open your eyes and have a look at your MongoDB installation! Even something as simple as backup and restore becomes highly problematic when applied to a MongoDB installation that contains replica sets and where collections are split up into shards!
The primary tools for performing backup and restore operations are mongodump and mongorestore.
The primary backup tool is mongodump, which is run from the command line (not from the mongo shell!). This command is able to back up the local MongoDB database of any accessible host. The output from this command is in the form of Binary Serialized Object Notation (BSON) documents, which are compressed binary equivalents of JSON documents. When backing up, there are a number of options available in the form of command-line switches, which affect what is backed up, and how.
To invoke a given option, simply add it to the command line following mongodump. Options can be combined, although some will be mutually exclusive. Here is a list of the mongodump command-line options:
|
Category |
Option |
Category |
Option |
|
General |
--help |
Namespace |
--db=<database-name> |
| Connection | --host <hostname> --port <port_number> |
URI (connection string) | --uri=mongodb-uri |
| Secure Sockets Layer (SSL) (MongoDB version 4.0 and below) |
--ssl --sslCAFile=<filename> --sslPEMKeyFile=<filename> --sslPEMKeyPassword=<password> --sslCRLFile=<filename> --sslAllowInvalidCertificates --sslAllowInvalidHostnames --sslFIPSMode |
Query | --query="<JSON string forming a filter>" --queryFile=<file containing a query filter> --readPreference=<string>|<json> --forceTableScan |
| TLS (MongoDB version 4.2 and above) | --tls --tlsCertificateKeyFile=<filename> --tlsCertificateKeyFilePassword=<value> --tlsCAFile=<filename> --tlsCRLFile=<filename> --tlsAllowInvalidHostnames --tlsAllowInvalidCertificates --tlsFIPSMode --tlsCertificateSelector <parameter>=<value> --tlsDisabledProtocols <string> |
Verbosity | --verbose=<1-4> or --quiet |
| Authentication | --username=<username> --password=<password> --authenticationDatabase= <database-name> --authenticationMechanism= <mechanism> |
Output | --out=<directory-path; default: "./dump"> --gzip --repair --oplog --archive=<file-path> --dumpDbUsersAndRoles --excludeCollection=<collection-name> --excludeCollectionsWithPrefix= <collection-prefix> --numParallelCollections=<N; default: 4) --viewsAsCollections |
Some options are available as single letters. For example, instead of using the --verbose option, you could alternatively add -v.
Here are two important considerations you should address before performing a backup:
--host=<replica_set_name>/<primary_host_address>:<port>
Here is an example of the sweetscomplete database being backed up to a ~/backup directory at verbosity level 2:

The primary MongoDB command to restore data is mongorestore. The command-line options are identical to those listed previously for mongodump, with the following additions:
|
Input options |
Restore options |
|
--objcheck
|
--drop |
Before restoring data, here are a few important considerations:
In this example, we restore the purchases collection to the sweetscomplete database, adding a verbosity level of 2, while maintaining database integrity, as follows:
mongorestore --db=sweetscomplete --collection=purchases --drop --objcheck \
--verbose=2 --dir=/root/backup/sweetscomplete/purchases.bson
Here is the output from this command:

An easier way to restore is to use the --nsInclude switch. This allows you to specify a namespace pattern, which could also include an asterisk (*) as a wildcard. Thus, if you wanted to restore all collections of a single database, you could issue this command:
mongorestore --nsInclude sweetscomplete.*
Here is an example of the products collections being restored from the sweetscomplete database:

In the last section of this chapter, we have a look at performance monitoring.
Determining what is considered normal for your database requires gathering statistics over a period of time. The period of time varies widely, depending upon the frequency and volume of database usage. In this section, we examine two primary means of gathering statistics: the db*stats() and db.serverStatus() methods. First, we have a look at how to monitor MongoDB performance using built-in mongo shell methods that produce statistics.
The most readily available monitoring command is the stats() shell method, available at both the database and collection levels. At the database level, this shell method gives important information such as the number of collections and indexes, as well as information on the average document size, average file size, and information on the amount of filesystem storage used.
Here is an example of output from db.stats() using the sweetscomplete database:

Information given by db.<COLLECTION>.stats() (substitute the name of the collection in place of <COLLECTION>), a wrapper for the collStats database command, easily produces 10 times the amount of information as db.stats(). The output from db.<COLLECTION>.stats() gives the following general information:
| Output key | Data type | Notes |
| ns | string | Shows the namespace of the collection. The namespace is a string that includes the database and collection, separated by a period. |
| size | integer | The size of the collection. |
| count | integer | The number of documents in the collection. |
| avgObjSize | integer | The average size of a document in this collection. |
| storageSize | integer | The storage size allocated to this collection. |
| capped | Boolean | Indicates whether or not this is a capped collection. A capped collection, as the name implies, is only allowed to grow to a specified size. |
| nindexes | integer | The number of indexes associated with this collection. |
| indexBuilds | list | An array of indexes currently being built. Once the index build process completes, that index name is removed from this list. |
| totalIndexSize | integer | Total size in bytes that the indexes occupy on this server. |
| indexSizes | document | JSON document giving a breakdown of the size for each individual index associated with this collection. |
| scaleFactor | integer | Indicates the value of scale, provided as an argument to stats() in this form: db.collection.stats({"scale":N}). If N = 1, the values returned are in bytes. If N = 1024, the values returned are in kilobytes (KB), and so on. |
| ok | integer | Status code for this collection. A value of 1 indicates everything is functioning properly. |
If all you need to do is to find information on the amount of storage space used by MongoDB collection components, a number of shortcut methods have been created that leverage db.collection.stats(). The following fall into this category:
| db.collection method | Outputs |
| db.collection.dataSize() | db.collection.stats().size |
| db.collection.storageSize() | db.collection.stats().storageSize |
| db.collection.totalIndexSize() | db.collection.stats().totalIndexSize |
Let's now turn our attention to server status.
Another extremely useful shell method used to gather information about the state of the database server is db.serverStatus(). This shell method is a wrapper for the serverStatus. database command. As with the stats() method described in the previous sub-section, this utility provides literally hundreds of statistics. An explanation of all of them is simply beyond what this book is able to cover.
Here is a screenshot of the first few statistics:

Here is a brief summary of the more important statistics:
| db.serverStatus() statistic | Notes |
| version | The version of MongoDB running on this server. |
| uptime | The number of seconds this server has been running. |
| electionMetrics | This pertains to the functioning of a replica set and is covered in more detail in Chapter 13, Deploying a Replica Set. |
| freeMonitoring | Covered in the next sub-section. |
| locks | Returns a detailed breakdown on each lock currently existing in the database. |
| network | Information on database network usage. |
| op* and repl | Details on replication and the oplog. This is covered in more detail in Chapter 13, Deploying a Replica Set. |
| security | Gives information on security configuration and status. |
| sharding | Details on the sharded cluster(s). This is covered in more detail in Chapter 15, Deploying a Sharded Cluster. |
| transactions | Information on the status of transactions: currently active, committed (that is, successful), aborted (that is, failed), and so on. |
| wiredTiger | Gives information on the status of the WiredTiger engine driving this database. This information is only available if your mongod instance is using the WiredTiger (default) storage engine. As of this writing, this category returns 425 individual statistics! |
| mem | Returns information on MongoDB memory usage. |
| metrics | General metrics in all areas of database operations. This set of statistics is extremely useful in establishing a performance benchmark. |
This table provides statistics that might indicate problems with your database:
| db.serverStatus() statistic | Notes |
| asserts.msg | The number of warning messages that have been issued. An excessive number of messages could indicate problems with the database. Consult the database log for more information. The location of the database log is configured in the mongod.conf file. |
| connections.current | The number of users (including applications and mongo shell connections) currently connected. |
| connections.available | The number of remaining connections. A value of less than 1000 indicates serious connection problems. Check to see if applications are keeping connections open needlessly. Also, check to see if any applications are tying up the database in endless loops. |
| extra_info.page_faults | A large value here indicates potential problems with memory and large datasets. Memory issues can be resolved through improvements in application program code, or by reducing the size of individual documents through a process of refining your database design. Large dataset issues can be handled through sharding, covered in Chapter 15, Deploying a Sharded Cluster. |
| metrics.commands.<command>.failed | If this number is high, you need to look for the applications that are issuing the <command> that failed and determine wherein lies the error. |
| metrics.getLastError | A JSON document giving details of the last error. |
| metrics.operation.writeConflicts | If this value is high, it indicates a bottleneck whereby write operations are preventing queries from being executed. |
| metrics.cursor.timedOut | Represents the number of times a cursor (that is, the return value from a find operation) timed out. A high number might indicate an application issue or a bottleneck where writes are blocking read operations. |
And that concludes our coverage of essential MongoDB administration techniques.
In this chapter, you learned how to get into the mongo shell using a set of command-line options. You also learned how to use the .mongorc.js file to customize the shell. You learned how to perform basic database operations, such as conducting queries, and inserting, updating, and deleting documents. You learned the basics of how to construct a query filter using query operators, and how to control the output by creating a projection. You also learned that the same filter created for a query can also be applied to update and delete operations. In the process of learning about performing queries, you were shown various examples of common ad hoc reports required by management.
You now know that there is a set of db.collection.XXX shell methods to perform operations on single documents, including findOne(), insertOne(), updateOne(), replaceOne(), and deleteOne(). There is a parallel set of db.collection.XXX shell methods that operate on one or more documents, including find(), insertMany(), updateMany(), and deleteMany(). You can use the xxxOne() set of methods to ensure only a single document is affected, which might be useful when performing critical updates. You also learned how these tools can be used to spot irregularities in the data, and how to make adjustments.
You then learned how to perform backup and restore using the mongodump and mongorestore tools. You learned about the various command-line switches available for these utilities and were presented with a number of points to consider in the interest of maintaining data integrity, especially when operating upon servers in a replica set. Finally, you were given an overview and some practical techniques on how to monitor database performance. As you learned, the two major methods are db.collection.stats() and db.serverStatus(). In addition to a summary of the major statistics available to monitor database operations, you also learned about which parameters to look to that might indicate impaired database performance.
In the next section of the book, you will learn how to build a dynamic database-driven web application using Python, based upon MongoDB. The next chapter will address database design for a typical small enterprise, Sweets Complete, Inc., a fictitious company offering confections for sale online.
Each chapter in this section covers a series of practical hands-on tasks that teach you how to perform routine DevOps tasks involving MongoDB. The program code examples provided are in the popular Python programming language. Staying with the initial scenario involving the fictitious company Sweets Complete, Inc., this section takes you through a progression of major concepts ultimately leading to a dynamic database-driven website.
This section contains the following chapters:
This chapter introduces a fictitious company called Sweets Complete Inc., and presents a database model based upon their needs. Special focus is placed upon moving from a set of customer requirements to a working MongoDB database design. The basic technique you will learn in this chapter is to first model the proposed document using JSON syntax, which can be tested in the mongo shell. You will then learn how to adapt the newly designed document to a Python entity class that will be put to use in the next chapter.
The topics that we will cover in this chapter are as follows:
The minimum recommended hardware required for this chapter are as follows:
The software requirements for this chapter are as follows:
The installation of the required software and how to restore the code repository for the book is explained in Chapter 2, Setting Up MongoDB 4.x. To run the code examples in this chapter, open a Terminal window (or Command Prompt) and enter these commands:
cd /path/to/repo
docker-compose build
docker-compose up -d
docker exec -it learn-mongo-server-1 /bin/bash
When you are finished practicing the commands covered in this chapter, be sure to stop Docker as follows:
docker-compose down
The code used in this chapter can be found in the book's GitHub repository at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/chapters/04.
It is very important to note that this is not a book on software design or application architecture. Rather, our focus is on developing a useful set of classes that allow you to gain database access regardless of which Python framework you are using. In this section, we cover how to come up with a good database design based on customer requirements.
Sweets Complete Inc. is a fictitious company representing small enterprises that sell products online. They sell specialized confections, including cake, candy, and donuts, to an international clientele. People who use their website fall into one of four groups, each with different needs:
Sweets Complete will be our example company for this part of the book. Let's now dive into what we need to know to produce a solid database design.
As you review customer requirements, one of the first things you need to ascertain is what information is the application expected to produce? This is referred to as the output and can take many forms and encompass many different technologies. Some examples of the output include the following:
The data required to satisfy these various forms of response will often be grouped in a certain logical manner, which will lead you to the database document structures definition. Different groups of users have different requirements for output:
| Group | Desired Output |
| Casual website visitors | Ability to search lists of products Purchase confirmation |
| Members | Purchase history with totals Product availability information |
| Administrative assistants | Product inventory information Updating costs and prices |
| Management | Monthly sales reports broken down by product and geographic area Analyzing costs versus sales Setting prices |
Continuing your review, the next piece of information you might consider is what information goes into the application. As with outputs, inputs to your application can take many forms, including the following:
It is useful to examine what form of input to expect from members of the different groups outlined in the previous section. Here is an example of what activities each group might perform to produce inputs:
| Group | Input-Producing Activities |
| Casual website visitors | All clicks are registered as product-interest data. Making purchases, generating purchase data. |
| Members | Reading up on products, giving management an idea of which products are trending upward. Making purchases, generating purchase data. |
| Administrative assistants | Entering product information. Updating inventory information as new stock arrives. |
| Management | Placing orders for the raw materials needed to produce products. Defining product lines. Creating sales promotions. |
Once you have determined the outputs (described in the previous subsection) that the application is expected to produce, you then have an idea of what data needs to go into the system. At first glance, it would seem logical to conclude that for every piece of output data, there needs to be a corresponding input source. For example, if one of the printed reports includes a list of product titles, somehow that information needs to be entered into the system.
In some cases, however, this theory does not hold true: your application might massage input data to produce the desired output. Not all inputs will appear directly as outputs and vice versa. For example, if the printed report is expected to produce a total for each customer purchase, this total does not need to be entered manually by an administrative assistant. Instead, the totals are calculated from the product price and quantity purchased, along with any other fees and taxes that might apply.
One simple technique is to visualize customer requirements, as illustrated in the following diagram:

Imagine placing all the inputs on the left and all the outputs on the right. The difference between the two is produced by the application software.
So, at last, we arrive at the big question: what needs to be stored in our database? You might be tempted to go with the simple answer: everything. A lot depends on how many unique individual data items we deal with and how many of each can we expect. Your initial tendency might be to take the easy route and just store everything: storage is cheap, and a MongoDB installation can be scaled up easily through the process of sharding.
It is in your interest to economize, however, if for no other reason than to avoid having the database engine clogged up with massive amounts of unneeded data. Also, no matter how cheap it is, there will be only so many times management will grant your request for a purchase order to buy more storage! Furthermore, the less data your application needs to sort through, the more efficient it will be.
So, to answer the question, you should store any data items that cannot be generated from existing information. This means that the cost of a product and the quantity of items that are purchased needs to be stored. The extended price of a purchase does not need to be stored, however, as it can be easily generated from the quantity and price. Other items that need to be stored are identifying information, such as transaction numbers, purchase dates, and so forth.
Although we are not trying to teach you software design, it might be worth noting that Martin Fowler, a very influential thinker in the IT world, had this to say:
In this book, we will not go over software design patterns, but encourage you to have an awareness of these patterns. In an absolutely shameless plug, here are two good books on that subject from Packt Publishing:
You could also, of course, read Martin Fowler's famous book, Patterns of Enterprise Application Architecture (https://martinfowler.com/books/eaa.html).
For the purposes of this book, we will not focus on actual accounting database storage: most small enterprises either outsource their accounting to a separate firm or use a canned software accounting package, which has its own database and storage capabilities. Instead, we will focus on defining the following collections:
In addition, we will include one more collection called common, which holds the defaults for drop-down menus, product categories, units of measurement, social-media codes (for example, FB is the code for Facebook), and so forth.
Following the preceding suggestions, after reviewing the customer requirements and interviewing stakeholders, you might discover that each of the groups defined previously needs the following information about products:
| Group | Information Needed |
| Casual Website Visitors | Product photo, category, title, price, and description |
| Members | Same as website visitors + unit of measurement and units on hand |
| Administrative Assistants | Same as members + cost per unit |
| Management | Same as administrative assistants |
A decision that you need to make for this example is how to store information about the photo. One obvious approach is to simply store a URL that points to the photo. Another possibility is to store the photo itself directly in the database, using Base64 encoding. Yet a third approach would be to store the actual photo file in MongoDB using GridFS. For our illustration, in this section, we will assume that product photo information is stored as either a URL or a Base64-encoded string. The next chapter, Chapter 5, Mission-Critical MongoDB Database Tasks, will discuss how to deliver an image directly from the database. Chapter 10, Working with Complex Documents across Collections, discusses how to use GridFS.
The information that should be stored for each product could be summarized in the following JSON document:
product =
{
"productPhoto" : "URL or base 64 encoded PNG",
"skuNumber" : "4 letters from title + 4 digit number",
"category" : "drawn from common.categories",
"title" : "Alpha-numeric + spaces",
"description" : "Detailed description of the product",
"price" : "floating point number",
"unit" : "drawn from common.unit",
"costPerUnit" : "floating point number",
"unitsOnHand" : "integer"
}
For those of you used to working with RDBMS, you will notice that a critical field is missing: the famous primary key. In MongoDB, when a document is inserted into the database, a unique key is generated and given the reserved field name _id. Here is an example drawn from the products collection:
"_id" : ObjectId("5c5f8e011a2656b4af405319")
It is an instance of BSON data type ObjectId, and is generated as follows:
This means that, in MongoDB, there is no need to define a unique key; however, it is often convenient to store a unique key of your own creation that is somehow tied to the data entered into the collection. For our purposes, we will create a unique field, productKey, which is a condensed version of the product title.
The following is an example of the first product entered into the collection, along with our completed document structure:
db.products.findOne();
{
"_id" : ObjectId("5c7ded398c3a9b9b9577b7e9"),
"productKey" : "apple_pie",
"productPhoto" : "iVBORw0KGgoAAAANSUhEUgAAASwAAADCCAYAAADzRP8zA ... ",
"skuNumber" : "APPL501",
"category" : "pie",
"title" : "Apple Pie",
"description" : "Donec sed dui hendrerit iaculis vestibulum ... ",
"price" : "4.99",
"unit" : "tin",
"costPerUnit" : "4.491",
"unitsOnHand" : 693
}
Following the preceding suggestions, after reviewing the customer requirements and interviewing stakeholders, you might discover that each of the groups defined previously needs the following information about purchases:
| Group | Information Needed |
| Casual Website Visitors | Purchase history, including the date, products purchased, and the total price paid |
| Members | Same as website visitors |
| Administrative Assistants | Same as members + customer contact information |
| Management | Same as administrative assistants + customer geospatial and address information in order to generate sales reports by geographic region |
You will note from the preceding table that documents in the purchases collection need three distinct pieces of information, which are summarized in the next table:
| Information Category | Distinct Pieces of Information |
| Purchase Data | Purchase date |
| Customer Data |
Name, address, city, state or province, postal code, country, and geospatial information |
| Products Purchased | For each product: quantity purchased, SKU number, category, title, and price |
As always, there are decisions that you must make regarding how much customer and product information you will store. In legacy RDBMS circles, you run the risk of violating the cardinal rule: no duplication of data. Accordingly, assuming that you are accustomed to the RDBMS way of thinking, the tendency would be to establish a relationship between the products, purchases, and customers collections (that is, a SQL JOIN). Alternatively, you might decide to embed the associated customer and products directly into each purchase document.
In Chapter 7, Advanced MongoDB Database Design, we will cover how to embed documents. For the purposes of this chapter, however, in order to keep things simple (for now), we will simply copy the appropriate pieces of customer and product data directly into the purchase document without the benefit of embedded objects.
Furthermore, in order to avoid having to create a join table, joining multiple products to a single purchase, we will simply define an embedded array of products. In this part of the book, we will use very simple operators to access embedded array information. In Chapter 8, Using Documents with Embedded Lists and Objects, we will go into further detail on how to actually manipulate embedded array information.
Another decision that needs to be made is whether or not to define a unique identifying field. As we mentioned in the previous section, MongoDB already creates a unique identifier, the mysterious _id field, which is an ObjectId instance. For our purposes, we add a new identifying field, transactionId, which is useful for accounting purposes. Although we can extract information on the date and time from the _id field, for the purposes of this illustration, we will use the date to form our new identifier, followed by two letters representing customer initials, followed by a random four-digit number.
Finally, we need to decide whether or not to include the extended price in each purchase document. An argument in favor of this approach is that by precalculating this information and storing it, we experience faster speed when it's time to generate sales reports. Also, by storing this value at the time the purchase document is created, we avoid possible errors that might come from overly complicated reporting programming code. For the purposes of this example, we have decided to precalculate the extended price and store it in each purchase document.
Therefore, the final structure for purchase would appear as shown here from a find() query:
purchase = {
"_id" : ObjectId("5c8098d90f583b515e4d3f41"),
"transactionId" : "20181008VC5473",
"dateOfPurchase" : "2018-10-08 11:03:37",
"extendedPrice" : 303.36,
"customerKey" : "VASHCARS8772",
"firstName" : "Vashti",
"lastName" : "Carson",
"phoneNumber" : "+1-148-236-8772",
"email" : "vcarson148@Swisscom.com",
"streetAddressOfBuilding" : "5161 Green Mound Ride",
"city" : "West Haldimand (Port Dover)",
"stateProvince" : "ON",
"locality" : "Ontario",
"country" : "CA",
"postalCode" : "N0A",
"latitude" : "42.9403",
"longitude" : "-79.945",
"productsPurchased" : [
{
"productKey" : "glow_in_the_dark_donut",
"qtyPurchased" : 384,
"skuNumber" : "GLOW437",
"category" : "donut",
"title" : "Glow In The Dark Donut",
"price" : "0.79"
}
]
}
When creating an application based upon a traditional RDBMS, you would normally define a plethora of tiny tables that are consulted when presenting the website user with HTML select options, radio buttons, or checkboxes. The only other alternative would be to hardcode such defaults and options into configuration files.
When using MongoDB, you can take advantage of two radical differences from a legacy RDBMS:
The following table summarizes the options needs for Sweets Complete:
| Item | Key | Value |
| gender | M | Male |
| F | Female | |
| X | Other | |
| social media | FB | |
| TW |
As you can see, for these two items, a dictionary is defined with key–value pairs. For other items, summarized in the following table, there are lists for which there is no key:
| Item | Values |
| categories | Cake, chocolate, pie, cookie, donut |
| unit | Box, tin, piece, item |
To add flexibility, we can simply add each of the aforementioned items as a separate document, consisting of a single list. Later, when we need to display options or to confirm form submissions against the options, we perform a lookup based on the item. So, finally, here is how the JavaScript to populate the common collection might appear:
db.common.insertMany( [
{ "key" : "gender",
"data" : { "M" : "male", "F" : "female", "X" : "Other" }
},
{ "key" : "socialMedia",
"data" : {"FB":"facebook", "GO":"google", "LI":"linkedin",
"LN":"line", "SK":"skype", "TW":"twitter"}
},
{ "key" : "categories",
"data" : ["cake","chocolate","pie","cookie","donut"]
},
{ "key" : "unit",
"data" : ["box","tin","piece","item"]
}]);
We will now turn our attention to developing application code that takes advantage of these data structures.
We are now ready to start defining a Python module containing a class definition equivalent to each MongoDB document described previously. Following the same order as the previous section, let's go collection by collection, starting with products. First, we need to determine what every single entity class needs and put this into a base class.
We first define a directory structure that will contain information pertinent to sweetscomplete. Under this, we create a subdirectory entity where our new base module will be placed. We then create an empty __init__.py file at each directory level to indicate the presence of a Python module. In the entity directory, we create a base.py file to contain our new base class. Eventually, we will also define entity classes for customers, products, and purchases.
The resulting directory structure appears as follows:
/path/to/repo/chapters/04/src/
└── sweetscomplete
├── entity
│ ├── base.py
│ ├── common.py
│ ├── customer.py
│ ├── __init__.py
│ ├── product.py
│ └── purchase.py
└── __init__.py
In the Base class, we import the JSON module and define a property field that defaults to an empty dictionary. Subclasses inheriting from the Base class will contain precise field definitions specific to customers, products, and purchases:
# sweetscomplete.entity.base
import json
class Base(dict) :
fields = dict()
Next, we define the core set() and get() methods that are used to assign values to properties or retrieve those values:
def set(self, key, value) :
self[key] = value
def get(self, key) :
if key in self :
return self[key]
else :
return None
We can then define two methods setFromDict() and setFromJson() that populate properties from a Python dictionary or a JSON document, respectively. You will note that these methods rely upon the current value of fields:
def setFromDict(self, doc) :
for key, value in self.fields.items() :
if key in doc : self[key] = doc[key]
def setFromJson(self, jsonDoc) :
doc = json.loads(jsonDoc)
self.setFromDict(doc)
We make use of these two methods in the __init__ constructor method. We first check to see whether a parameter has been provided to the constructor. If so, we initialize the fields to their defaults. Following this, if the document argument that is provided is of a dict type, we use setFromDict(); otherwise, if the document provided is a string, we use setFromJson(). If no document is provided to the constructor, then an empty object instance is created. We are then free to use the set() method described earlier to populate the selected properties:
def __init__(self, doc = None) :
if doc :
for key, value in self.fields.items() : self[key] = value
if isinstance(doc, dict) :
self.setFromDict(doc)
elif isinstance(doc, str) :
self.setFromJson(doc)
Finally, just in case we need to produce a JSON document from the entity, we add a toJson() method:
def toJson(self, filtered = []) :
jsonDoc = dict()
for k,v in self.items() :
if k in filtered :
pass
elif k == '_id' :
jsonDoc[k] = str(v)
else :
jsonDoc[k] = v
return json.dumps(jsonDoc)
We are now ready to look at the entity class sweetscomplete.entity.product.Product.
The most important thing to do at this point is to define the fields property. You will note that we also assign defaults in this dictionary structure. Here is how the core class might appear:
# sweetscomplete.entity.product
from sweetscomplete.entity.base import Base
class Product(Base) :
fields = dict({
'productKey' : '',
'productPhoto' : '',
'skuNumber' : '',
'category' : '',
'title' : '',
'description' : '',
'price' : 0.00,
'unit' : '',
'costPerUnit' : 0.00,
'unitsOnHand' : 0.00
})
We then add a few useful methods to set and retrieve the unique key, and also the product title:
def getKey(self) :
return self['productKey']
def setKey(self, key) :
self['productKey'] = key
def getTitle(self) :
return self['title']
Let's look at a demo script for product.
In the src directory (the directory that contains the sweetscomplete directory), we define a demonstration script that tests our new class. We call the script /path/to/repo/chapters/04/demo_sweetscomplete_product_entity.py. In the first few lines of code, we tell Python where to find the source code and import classes needed for the demo, as shown here:
# sweetscomplete.entity.product.Product
# tell Python where to find module source code
import os,sys
sys.path.append(os.path.realpath('src'))
import pprint
from datetime import date
from sweetscomplete.entity.product import Product
Next, we define a demo key and the initial block of data in the form of a dictionary:
key = 'TEST' + date.today().isoformat().replace('-', '')
doc = dict({
'productKey' : key,
'productPhoto': 'TEST',
'skuNumber' : 'TEST0000',
'category' : 'test',
'title' : 'Test',
'description' : 'test',
'price' : 2.22,
'unit' : 'test',
'costPerUnit' : 1.11,
'unitsOnHand' : 333
})
Finally, we use the entity class, providing the sample dictionary as an argument:
product = Product(doc)
print("\nProduct Entity Initialized from Dictionary")
print('Title: ' + product.getTitle())
print('Category: ' + product.get('category'))
print(product.toJson())
We execute it using the following command:
python /path/to/repo/chapters/04/demo_sweetscomplete_product_entity.py
From this, we get the following output:

Next, let's look at a unit test for the product entity.
Rather than creating demo scripts, it is considered a best practice to create a unit test. First, you need to import unittest and set up a test class that inherits from unittest.TestCase:. Create a file named test_product.py and place it in a directory structure named test/sweetscomplete/entity. As with our demo script mentioned previously, we use the os and sys modules to tell Python where to find the source code. We also import json, unittest, and our Product class:
# sweetscomplete.entity.product.Product test
import os,sys
sys.path.append(os.path.realpath("../../../src"))
import json
import unittest
from sweetscomplete.entity.product import Product
class TestProduct(unittest.TestCase) :
productFromDict = None
Next, you define a dictionary with fields that are populated with test data. The productFromDict property is then initialized in the test method, setUp():
testDict = dict({
'productKey' : '00000000',
'productPhoto': 'TEST',
'skuNumber' : 'TEST0000',
'category' : 'test',
'title' : 'Test',
'description' : 'test',
'price' : 1.11,
'unit' : 'test',
'costPerUnit' : 2.22,
'unitsOnHand' : 333
})
def setUp(self) :
self.productFromDict = Product(self.testDict)
You can now define methods that test whether the setKey(), set(), and get() methods return expected values. This also incidentally tests whether or not the object was correctly populated (that is, it indirectly tests the constructor):
def test_product_from_dict(self) :
expected = '00000000'
actual = self.productFromDict.getKey()
self.assertEqual(expected, actual)
def test_product_from_dict_get_and_set(self) :
self.productFromDict.set('skuNumber', '99999999')
expected = '99999999'
actual = self.productFromDict.get('skuNumber')
self.assertEqual(expected, actual)
Finally, you add the following lines to the end of the file to allow command-line execution:
def main() :
unittest.main()
if __name__ == "__main__":
main()
Here is the result from the test script located at /path/to/repo/chapters/04/test/sweetscomplete/entity/test_product.py:

The remaining classes to be defined follow the same logic as described in the previous section. Again, the main challenge is to correctly define the fields that you plan to use. For this purpose, we simply refer back to the original data structures described earlier in the chapter. The key classes that we need include classes to represent the purchases and customers collections. Some example Python entity classes for these can be found as follows:
The common collection is a simple sequence of documents, each of which has a key and an associated list or dictionary. The Python class associated with this collection can be found in the purchases collection, at /path/to/repo/chapters/04/src/sweetscomplete/entity/common.py.
In this chapter, you learned how to break down customer requirements by determining what needs to come out of the proposed application. From this breakdown, you were able to deduce what needs to go into the application by way of form submissions, data obtained from web service calls, and so on. Finally, you were given guidance on how to determine what needs to be stored.
The next section showed you how to create MongoDB document structures in JSON format after analyzing the needs of Sweets Complete Inc. From there, you went on to learn how to convert the JSON documents into Python classes. In each case, the Python entity classes included a sample demo script showing you how the entity class can be used. In addition, you were shown how to develop an appropriate unit test for the new class.
In the next chapter, you will learn how to apply your Python knowledge to the task of performing critical database tasks, such as adding, editing, and deleting documents, as well as basic queries.
This chapter builds upon the work from the previous chapter, Chapter 4, Fundamentals of Database Design. The reason why the tasks in this chapter are considered mission-critical is because they will teach you how to perform vital database operations that will need to be performed every day. These tasks include reading, writing, updating, and deleting documents from a database.
First, you will be presented with a series of common tasks based upon the Sweets Complete dataset. You'll then learn how to define domain classes, which perform critical database access services, including performing queries and adding, editing, and deleting documents. These capabilities are then applied to practical tasks, including updating product information and generating a sales report that details total purchases by month and by year.
In the process, you will be shown how to formulate queries in JavaScript, and then translate that query into its equivalent using Python. Finally, you'll learn how to integrate MongoDB into a simple web infrastructure in order to have a customer log in, make a purchase, see the results, and confirm or cancel the purchase.
In this chapter, we will cover the following topics:
Let's get started!
The following is the minimum recommended hardware for this chapter:
The following are the software requirements for this chapter:
Installation of the required software and how to restore the code repository for this book is explained in Chapter 2, Setting Up MongoDB 4.x. To run the code examples in this chapter, open a Terminal window (Command Prompt) and enter these commands:
cd /path/to/repo
docker-compose build
docker-compose up -d
docker exec -it learn-mongo-server-1 /bin/bash
To run the demo website described in this chapter, follow the setup instructions given in Chapter 2, Setting Up MongoDB 4.x. Once the Docker container for Chapters 3 through 12 has been configured and started, there are two more things you need to do:
172.16.0.11 learning.mongodb.local
http://learning.mongodb.local/
When you are finished working with the examples covered in this chapter, return to the Terminal window (Command Prompt) and stop Docker, as follows:
cd /path/to/repo
docker-compose down
Now, let's start creating a connection class.
The code used in this chapter can be found in this book's GitHub repository at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/chapters/05, and also at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/www/chapter_05.
It is extremely useful to have a generic connection class available that can be used throughout the application. This class needs to have the following capabilities:
First, we must discuss the role of the pymongo.MongoClient class.
pymongo.MongoClient is actually an alias for pymongo.mongo_client.MongoClient. This class accepts a URI style connection string as an argument or individual parameters, as shown here. The arguments in square brackets [] are optional:
from pymongo.mongo_client import MongoClient
client = MongoClient(<host>[,<port>,<document_class>,<tz_aware>,<connect>,**kwargs])
The main parameters are summarized in this table:
| Parameter | Notes |
| host | Can be localhost, a DNS address (for example, db.unlikelysource.com), or an IP address. |
| port | If not specified, defaults to 27017; otherwise, specify the MongoDB listening port. |
| document_class | Defaults to dict. This parameter controls what gets returned when you run a database query. This parameter ties the entity classes discussed in the previous chapter into MongoDB. |
| tz_aware | If set to True, any datetime instances reflect the server's time zone. |
| connect | If set to True, it causes the client to immediately attempt a database connection; otherwise, the connection is only made when results are required. |
**kwargs stands for a series of optional keyword arguments (key = value pairs) that can be specified. As an example, if you wish to supply the username and password authentication keyword arguments, you would create the client instance as follows:
client = pymongo.mongo_client.MongoClient(host='localhost', port=27017,
document_class=dict, tz_aware=False, connect=True,
username='fred', password='very_secret_password');
The other keyword arguments are listed here:
Although the pymongo client is extremely easy to use, it is much more convenient to wrap its functionality into a Connection class. We start by importing the MongoClient class. We then define two properties: one to represent the database and another to represent the client. Most importantly, take note of the third argument when creating a MongoClient instance – the name of the entity class associated with this connection:
# db.mongodb.connection
from pymongo import MongoClient
class Connection :
db = None
client = None
def __init__(self, host = 'localhost', port = 27017, \
result_class = dict) :
self.client = MongoClient(host, port, result_class)
Next, we define a number of useful methods to grant outside access to the client and database connection instances:
def getClient(self) :
return self.client
def getDatabase(self, database) :
self.db = self.client[database]
return self.db
Finally, we add one more method that allows us to gain access to both the database and a specific collection:
def getCollection(self, collection) :
if collection.find(".") :
database, collection = collection.split(".",1)
self.getDatabase(database)
return self.db[collection]
Here is a demo script that uses the Connection class to gain access to the products collection in the sweetscomplete database. Note that after the import statements, we create a Connection instance by specifying the hostname, port, and result class:
# /path/to/repo/chapters/05/demo_db_connection_sweetscomplete_product.py
from db.mongodb.connection import Connection
from sweetscomplete.entity.product import Product
conn = Connection('localhost', 27017, Product)
prodCollect = conn.getCollection("sweetscomplete.products")
After that, we use the pymongo collection's find_one() method to retrieve a single result:
result = prodCollect.find_one()
print("\nResult from Query:")
print('Class: ' + str(type(result)))
print('Key: ' + result.getKey())
print('Title: ' + result.getTitle())
print('Category: ' + result.get('category'))
print('JSON: ' + result.toJson(['productPhoto']))
As shown in the following output, the result is a sweetscomplete.entity.product.Product instance. We are using the methods we defined in the entity class we discussed in Chapter 4, Fundamentals of Database Design. You can also see that the toJson() method includes a filter that's used here to preclude productPhoto from the output:

Now that you have an idea of what a Connection class looks like, we'll have a look at developing a domain service class.
If your main interest is only writing short demo scripts, you could just use the Connection class discussed in the previous section. However, it is a best practice to wrap actual database access into a domain or service component. The domain is where the business logic is placed. In this context, business logic would include database access methods that allow us to perform critical database tasks such as adding to the database, making updates, removing documents, and performing queries.
We will start our discussion with a domain service that represents the products collection.
As with our entity classes, we start by defining a base class for all the domain services. What the base class provides for us is a property that represents instances of pymongo.database.Database and db.mongodb.connection.Connection.
The constructor initializes these two properties using the connection to retrieve the database instance:
# sweetscomplete.domain.base
class Base :
db = None
conn = None
def __init__(self, conn, database) :
self.conn = conn
self.db = conn.getDatabase(database)
We then define a domain service class called sweetscomplete.domain.product.ProductService that inherits from the base class:
# sweetscomplete.domain.product
from sweetscomplete.domain.base import Base
from sweetscomplete.entity.product import Product
class ProductService(Base) :
# useful methods definitions go here
We are then free to add useful methods as needed. These methods are wrappers for core pymongo collection methods including the following:
If you dig through the documentation of these methods (https://api.mongodb.com/python/current/api/pymongo/collection.html), it will not surprise you to learn that they mimic the functionality of the equivalent mongo shell methods described in Chapter 3, Essential MongoDB Administration Techniques. As an example of a method useful for classes consuming the service, consider fetchByKey(), shown here. It performs a simple query, using find_one(), and returns a single Product instance:
def fetchByKey(self, key) :
query = dict({"productKey" : key})
return self.db.products.find_one(query)
In a similar manner, we could return an iteration of Product instances that only contain productKey and title:
def fetchAllKeysAndTitles(self) :
projection = dict({"productKey":1,"title":1,"_id":0})
for doc in self.db.products.find(None, projection) :
yield doc
And, of course, where would we be without the ability to add and delete? Examples of such methods are shown here:
def addOne(self, productEntity) :
return self.db.products.insert_one(productEntity)
def deleteOne(self, query) :
return self.db.products.delete_one(query)
Finally, here is a demo script that makes use of the domain service. After performing the appropriate imports, you can see that we create a connection by specifying a Product as the result class. With the Connection instance, we are then able to create a ProductService instance:
# /path/to/repo/chapters/05
# demo_sweetscomplete_product_read_add_edit_delete.py
import os,sys
sys.path.append(os.path.realpath("src"))
import pprint
import db.mongodb.connection
from datetime import date
from sweetscomplete.entity.product import Product
from sweetscomplete.domain.product import ProductService
conn = db.mongodb.connection.Connection('localhost',27017,Product)
service = ProductService(conn, 'sweetscomplete')
Next, we initialize a JSON document with sample data. As you may recall, our entity class is able to accept either a dict or a JSON document as a constructor argument:
key = 'test' + date.today().isoformat().replace('-', '')
doc = '''\
{
"productKey" : "%key%",
"productPhoto": "TEST",
"skuNumber" : "TEST0000",
"category" : "test",
"title" : "Test",
"description" : "test",
"price" : 1.11,
"unit" : "test",
"costPerUnit" : 2.22,
"unitsOnHand" : 333
}
'''.replace('%key%',key)
We can then add the new product, find it using the demo key, delete it, and display a list of all product titles:
print("\nAdding a Single Test Product\n")
product = Product(doc)
if service.addOne(product) :
print("\nProduct " + key + " added successfully\n")
print("\nFetch Product by Key\n")
doc = service.fetchByKey(key)
if doc :
print(doc.toJson())
query = dict({"productKey" : key})
if service.deleteOne(query) :
print("\nProduct " + key + " deleted successfully\n")
print("\nList of Product Titles and Keys\n")
for doc in service.fetchAllKeysAndTitles(6) :
print(doc['title'] + ' [' + doc['productKey'] + ']')
Here is the output from the demo script:

Next, we'll examine a more practical application: producing a sales report.
Management has come to you with a request for a sales report for 2018, by country, broken down by month. You decide on a simple strategy: perform a query on the purchases collection that returns the total extended price of each purchase, along with the purchase date and country. Before getting into the actual code, we will look at the core pymongo collection command; that is, find().
The core class used for report queries is pymongo.collection.Collection.find().
Here is the generic syntax:
find(filter=None, projection=None, skip=0, limit=0, \
no_cursor_timeout=False, cursor_type=CursorType.NON_TAILABLE, \
sort=None, ... /* other options not shown */)
The find() arguments match their mongo shell equivalents. The more widely used parameters are summarized here:
| Parameter | Default | Notes |
| filter | None | A dictionary consisting of key/value pairs, where the key represents the field to search and the value represents the search criteria. If set to None, all documents in the collection are selected. |
| projection | None | A dictionary consisting of key/value pairs, where the key represents the field to show or suppress from the output. To show a field, set the value to 1. To suppress the field, set the value to 0. If set to None, all fields in the selected documents are returned. |
| skip | 0 | During results iteration, this parameter causes MongoDB to skip this many documents before starting to return query results. This is the equivalent to the SQL OFFSET keyword. |
| limit | 0 | This parameter causes MongoDB to return this many documents in the query result. This is equivalent to the SQL LIMIT keyword. |
| sort | None | Lets you specify which fields are used to order the results. This parameter is a dictionary of key/value pairs. The key is the field to influence the sort. The value is 1 for ascending and -1 for descending. |
| collation | None | You can supply a pymongo.collation.Collation instance that allows you to influence how search and sort operate. This is especially useful if you are collecting information on a website that services an international clientele. |
| hint | None | Allows you to recommend which index MongoDB should use when performing the query. |
| max_time_ms | None | Sets a time limit in milliseconds for how long a query is allowed to run. |
| show_record_id | False | If set to True, adds the internal records identifier to the output in the form of a recordId field. |
| session | None | If you supply a pymongo.client_session.ClientSession instance, you are able to initiate transaction support, which allows a set of database operations to be treated as a single atomic block. The session also gives you control over read and write concerns, which are important when operating on a replica set. |
In addition to these parameters, other parameters that are supported but not discussed here include no_cursor_timeout, cursor_type, allow_partial_results, oplog_replay, batch_size, max, min, return_key.
A really great technique that you can use to formulate complex queries is to model the query using the mongo shell or use MongoDB Compass (covered in Chapter 9, Handling Complex Queries in MongoDB). That way, you are able to get an idea of what data is returned, which might lead to further refinements. You can then adapt the query to Python and the pymongo.collection.Collection.find() method.
In the mongo shell, we use the sweetscomplete database. After that, we can formulate our query document in the form of a JavaScript variable query. Next, we define the projection, which controls which fields appear in the output. We can then execute this query:
query = {"dateOfPurchase":{"$regex":/^2018/}};
projection = {"dateOfPurchase":1,"extendedPrice":1,"country":1,"_id":0};
db.purchases.find(query, projection). \
sort({"country":1,"dateOfPurchase":1});
We will achieve this result:

Having successfully modeled the query using the mongo shell, we will now turn our attention back to the purchases domain service class, where we adapt the JavaScript query to Python code.
There are two ways to go about modeling queries in a domain service class. One approach is to create a method that explicitly produces the results for a report. In this approach, the complex logic needed to produce the report goes into the domain service class. This approach, which we call the Custom Query Method approach, is useful in situations where it is called frequently, and where you wish to hide the complexity of the logic from any calling program.
An alternative approach is to simply create a wrapper for existing pymongo functionality. We call this the Generic Query Method approach. This approach is useful if you wish to extend the flexibility of your domain service class, but places the burden on the calling program to formulate the appropriate query and logic to process the results.
We'll start by discussing the Generic Query Method approach.
First, we define the generic method in the domain service class. Note that we supply defaults for the find() method arguments; that is, skip, limit, no_cursor_timeout, and cursor_type:
# sweetscomplete.domain.purchase
from sweetscomplete.domain.base import Base
from sweetscomplete.entity.purchase import Purchase
from bson.objectid import ObjectId
from pymongo.cursor import CursorType
class PurchaseService(Base) :
def genericFetch(self, query, proj = dict(), order = None) :
for doc in self.db.purchases.find(query, proj,
0, 0, False, CursorType.NON_TAILABLE, order) :
yield doc
# other methods not shown
As you can see from the following code, the onus for results processing is on the calling program. As with other demo scripts, we first establish imports and set up the connection:
# /path/to/repo/chapters/05/demo_sweetscomplete_purchase_sales_report.py
import os,sys
sys.path.append(os.path.realpath("src"))
import db.mongodb.connection
import re
from bson.regex import Regex
from sweetscomplete.entity.purchase import Purchase
from sweetscomplete.domain.purchase import PurchaseService
conn = db.mongodb.connection.Connection('localhost',27017,Purchase)
service = PurchaseService(conn, 'sweetscomplete')
We then define the variables that will be used in the script, including the query and projection dictionaries:
year = 2018
pattern = '^' + str(year)
regex = Regex(pattern)
query = dict({'dateOfPurchase':{'$regex':regex}})
proj = dict({'dateOfPurchase':1,'extendedPrice':1,'country':1,'_id':0})
order = [('country', 1),('dateOfPurchase', 1)]
results = {}
Here is the logic that produces final results totals in the form of a dictionary – {country:{month:total}}:
for doc in service.genericFetch(query, proj, order) :
country = doc['country']
dop = doc['dateOfPurchase']
month = str(dop[5:7])
if country in results :
temp = results[country]
if month in temp :
price = temp[month] + doc['extendedPrice']
else :
price = doc['extendedPrice']
temp.update({month:price})
else :
results.update({country:{month:doc['extendedPrice']}})
And finally, here is the logic used to produce the final report's output:
print("\nSales Report for " + str(year))
import locale
grandTotal = 0.00
for country, monthList in results.items() :
print("\nCountry: " + country)
print("\tMonth\tSales");
for month, price in monthList.items() :
grandTotal += price
showPrice = locale.format_string("%8.*f", (2, price), True)
print("\t" + month + "\t" + showPrice)
showTotal = locale.format_string("%12.*f", (2, grandTotal), True)
print("\nGrand Total:\t" + showTotal)
The following screenshot shows the partial result:

Now, let's look at the Custom Query Method approach.
In this approach, the complex logic shown previously in the calling program is transferred to the domain service class in a custom method called salesReportByCountry() instead. As with the logic shown previously, first, we initialize the variables:
def salesReportByCountry(self, year) :
from bson.regex import Regex
pattern = '^' + str(year)
regex = Regex(pattern)
query = dict({'dateOfPurchase':{'$regex':regex}})
proj = dict({'dateOfPurchase':1,'extendedPrice':1, \
'country':1,'_id':0})
order = [('country', 1),('dateOfPurchase', 1)]
results = {}
We then implement exactly the same logic shown in the calling program prior:
for doc in self.genericFetch(query, proj, order) :
country = doc['country']
dop = doc['dateOfPurchase']
month = str(dop[5:7])
if country in results :
temp = results[country]
if month in temp :
price = temp[month] + doc['extendedPrice']
else :
price = doc['extendedPrice']
temp.update({month:price})
else :
results.update({country:{month:doc['extendedPrice']}})
return results
The calling program is now greatly simplified:
# /path/to/repo/chapters/05
# demo_sweetscomplete_purchase_sales_report_custom.py
# imports and setting up the connection are the same as shown above
# running a fetch based on date
year = 2018
results = service.salesReportByCountry(year)
print("\nSales Report for " + str(year))
import locale
grandTotal = 0.00
for country, monthList in results.items() :
print("\nCountry: " + country)
print("\tMonth\tSales");
for month, price in monthList.items() :
grandTotal += price
showPrice = locale.format_string("%8.*f", (2, price), True)
print("\t" + month + "\t" + showPrice
showTotal = locale.format_string("%12.*f", (2, grandTotal), True)
print("\nGrand Total:\t" + showTotal)
The results are not shown here as they are identical to the results provided in the previous subsection. Now, we'll learn how to develop classes and methods to perform update operations.
The core classes used for updates are update_one() and update_many(). The difference between the two is that the latter potentially affects multiple documents. Here is the generic syntax. The parameters in square brackets [] are optional:
update_one(<filter>, <update>, [<upsert>, \
<bypass_document_validation>, <collation>, \
<array_filters>, <session>])
The arguments match their mongo shell equivalents and are summarized here:
| Parameter | Default | Notes |
| filter | -- | A query dictionary that is identical to that used for the find() method. |
| update | -- | A $set : { key / value pairs } dictionary, where the key corresponds to the field and the value is the updated information. |
| upsert | False | If set to True, a new document is added if the query filter does not find a match. |
| bypass_document_validation | False | If set to True, the write operation is instructed not to validate. This improves performance at the cost of the potential loss of data integrity. |
| collation | None | You can supply a pymongo.collation.Collation instance, which allows you to influence how search and sort operate. This is especially useful if you are collecting information on a website that services an international clientele. |
| array_filters | None | Gives you control over which updates are applied to the elements of an embedded array. |
| session | None | If you supply a pymongo.client_session.ClientSession instance, you are able to initiate transaction support, which allows a set of database operations to be treated as a single atomic block. The session also gives you control over read and write concerns, which are important when operating on a replica set. |
The return value after an update is a pymongo.results.UpdateResult object, from which we need to extract information. The most pertinent piece of information is modified_count. If this value is 0, it means the update failed. Here is how the update method might appear in the ProductService class:
def editOneByKey(self, prodKey, data) :
query = dict({"productKey" : prodKey})
updateDoc = dict({"$set" : data})
result = self.db.products.update_one(query, updateDoc)
return self.db.products.find_one(query) \
if (result.modified_count > 0) else False
We can then modify the ProductService demo script shown earlier by adding the following code:
updateDoc = {
'productPhoto' :'REVISED PHOTO',
'price' : 2.22
};
result = service.editOneByKey(key, updateDoc)
if not result :
print("\nUnable to find this product key: " + key + "\n")
else :
print("\nProduct " + key + " updated successfully\n")
print(result.toJson())
Here is the modified output:

As you can see, the original values for productPhoto and price were TEST and 1.11. After the update, the same fields now show REVISED PHOTO and 2.22. The code shown previously is located at /path/to/repo/chapters/05/demo_sweetscomplete_product_read_add_edit_delete.py.
Now, let's learn how to handle a purchase.
At first glance, adding a purchase seems like a trivial process. Looking deeper, however, it becomes clear that we need to somehow bring together information about the customer making the purchase, as well as the products to be purchased.
Altogether, the process of adding a new purchase entails the following:
Before we can accomplish all of this, however, we need to introduce three new classes that provide web support: web.session.Session, web.auth.Authenticate, and web.responder.Html.
Later in this book, starting with Chapter 7, Advanced MongoDB Database Design, when we introduce our next company, Book Someplace, Inc., we'll cover MongoDB integration into the popular Python framework known as Django (https://www.djangoproject.com/). For the small company Sweets Complete, however, all that is required is an extremely simple web environment. The first class to investigate is Session.
As you might be aware, the HTTP protocol (see https://tools.ietf.org/html/rfc2616), upon which the World Wide Web is based, is stateless in nature. What that means is that each web request is treated as if it's the first request ever made. Accordingly, the protocol itself provides no way to retain information between requests. Therefore, here, we will define a classic authentication mechanism that uses information stored in an HTTP cookie to provide a reference to more information stored on the server.
The web.session.Session class (/path/to/repo/chapters/05/src/web/session.py) provides this support by way of the methods discussed here:
| Method | Description |
| genToken() | Generates a random token that serves to uniquely identify the session. |
| getToken() | Returns the generated token. If it's not available, it reads the token from the token cookie. |
| getTokenFromCookie() | Uses http.cookie.SimpleCookie to read a cookie called token, if it exists. |
| buildSessFilename() | Builds a filename that consists of the base directory (supplied during class construction), the token, and a .sess extension. |
| writeSessInfo() | Writes the information supplied to the session file in JSON format. |
| readSessInfo() | Retrieves information from the session file and converts it from JSON into dict(). |
| setSessInfoByKey() | Uses readSessInfo() to retrieve the contents of the session file. Updates the dictionary with the new information based upon the key supplied. The method then executes writeSessInfo(). |
| getSessInfoByKey() | Performs readSessInfo() to retrieve the contents of the session file. Returns the value of the key supplied, or False if the key doesn't exist. |
| cleanOldSessFiles() | This is called upon object construction to remove old session files. |
The Session class is then used by the SimpleAuth class (described next) to store information when a user successfully logs in.
As the name implies, this class is used to perform user authentication. The class constructor accepts the domain service class and a base directory as arguments. The base directory is then used to create a Session instance that the SimpleAuth class consumes. You can also see a getSession() method, which grants access to the internally stored Session instance. Likewise, the getToken() method is used to retrieve the token generated by Session.
The workhorse of this class is authByEmail(). It uses the domain service class to perform a lookup based on the customer's email address, used as part of the web login process (described later in this chapter). The method uses bcrypt.checkpw() to check the provided plaintext, UTF-8 encoded password, against the stored hashed password.
The authenticate() method brings everything together. It accepts an email address and plaintext password. The first thing to do is run authByEmail(). If successful, this method retrieves a Customer instance, the session class is used to generate a token, and the customer key is stored in the session. Otherwise, the method returns False. You see that the username argument, in this usage, is the user email.
What happens on the next request cycle after a successful login? As you may recall, successful authentication creates a token. The token needs to be set in a cookie. This action is performed by another class, discussed next. On the next request cycle, the user's browser presents the token cookie to the application. There needs to be a getIdentity() method that presents the customer key, stored in the session, to the domain service class to retrieve customer information.
The full Python code can be found at /path/to/repo/chapters/05/src/web/auth.py. The last class we'll cover here is used to respond to a web request. It contains methods that are useful when publishing an HTML web page. It also handles setting the token cookie mentioned previously.
The class constructor accepts the filename of the specified HTML document and serves as a template. As this class deals strictly with HTML (as compared to JSON output, for example), the Content-Type header is set to text/HTML.
The next two methods populate the headers and inserts lists. Headers represent HTTP headers that need to be sent upon output. The class constructor, shown previously, adds the first header to the list: the Content-Type header represents a list of placeholders in the HTML template document, which are subsequently replaced by a given value.
Finally, the render() method pulls everything together. First, the headers are assembled, separated by a carriage-return linefeed (\r\n), as mandated by the HTTP protocol. Next, the inserts are processed: the placeholders in the seed HTML template are replaced with their respective values. The rendered HTML is then returned for output by the calling program.
This concludes the discussion of support web classes. We will now turn our attention to the PurchaseService domain service class. Here, it's important to address the pymongo.collection methods, which can add documents to the database.
Adding a document to the database involves using either the insert_one() or insert_many() method from the pymongo.collection.Collection class. The generic syntax for insert_one() is as follows. The parameters enclosed in square brackets [] are optional:
insert_one(<document>, [<bypass_document_validation>, <session>])
The generic syntax for insert_many() is also shown here. The parameters enclosed in square brackets [] are optional:
insert_many(<iteration of documents>, [<ordered>, \
<bypass_document_validation>, <session>])
The more important parameters are summarized here:
| Parameter | Default | Notes |
| document(s) | -- | For insert_one(), this is a single document. For insert_many(), this is an iteration of documents. |
| bypass_document_validation |
False | If set to True, the write operation is instructed not to validate. This improves performance at the cost of the potential loss of data integrity. |
| session | None | If you supply a pymongo.client_session.ClientSession instance, you are able to initiate transaction support, which allows a set of database operations to be treated as a single atomic block. The session also gives you control over read and write concerns, which are important when operating on a replica set. |
| ordered | True | This parameter is only available for the insert_many() method. If set to True, the documents are inserted in the literal order presented in the iteration. In case of an error, all remaining inserts are not processed. If set to False, all documents inserts are attempted independently, regardless of the status of other inserts in the iteration. In this case, parallel processing is possible. |
Now, let's learn how to use this method in the PurchaseService domain service.
In the sweetscomplete.domain.purchase.PurchaseService class, we need to define a method that adds a document to the database. As with the other domain service classes described earlier in this chapter, we import the Base domain service class, as well as the Purchase entity class. In addition, we need access to the ObjectId and CursorType classes, as shown here:
from sweetscomplete.domain.base import Base
from sweetscomplete.entity.purchase import Purchase
from bson.objectid import ObjectId
from pymongo.cursor import CursorType
class PurchaseService(Base) :
As you may recall from a discussion earlier in this chapter, a decision needs to be made about the extended price of a purchase. In this implementation, we calculate the extended price just prior to storage, as shown here. The addOne() method then returns the new ObjectId, or a boolean False value if the insertion failed:
def addOne(self, purchaseEntity) :
extPrice = 0.00
for key, purchProd in purchaseEntity['productsPurchased'].items() :
extPrice += purchProd['qtyPurchased'] * purchProd['price']
purchaseEntity['extendedPrice'] = extPrice
result = self.db.purchases.insert_one(purchaseEntity)
return result.inserted_id \
if (isinstance(result.inserted_id, ObjectId)) else False
As a follow-up to a purchase, in this class, we also define a custom fetchLastPurchaseForCust() method that does a lookup of purchases by customer key in descending order. It then returns the first document, which represents the latest purchase:
def fetchLastPurchaseForCust(self, custKey) :
query = dict({"customerKey" : custKey})
projection = None
return self.db.purchases.find_one(
query, projection, 0, 0, False,
CursorType.NON_TAILABLE, [('dateOfPurchase', -1)])
Now, we will turn our attention to the web, where we'll do the following:
In this section, we'll discuss how to incorporate the domain services class with the web support classes to produce a simple web application. To accomplish this, we'll create three Python scripts to handle different aspects of the three main phases. Each Python web script makes use of one (or more) of the domain service classes. The flow for this discussion is illustrated in the following diagram:

Before we dive into the coding details, let's have a quick look at configuring Apache for simple Python Common Gateway Interface (CGI) script processing.
Using the CGI process is a quick and simple way to get your Python code on the web. For the purposes of this illustration, we'll use Apache as an example. Please bear in mind that 40% of internet web servers are now running Nginx; however, due to space considerations, we will only present the Apache configuration here.
First, you need to install the Apache mod_wsgi module. Although this can be done through the normal package management for your operating system, it's recommended to install it using pip3. If you see a warning about missing operating system dependencies, make sure these are installed first before proceeding with this command:
pip3 install mod-wsgi
You then need to create an Apache configuration for CGI. Here is a sample configuration file. You can see the full version at /path/to/repo/docker/learning.mongodb.local.conf:
AddHandler cgi-script .py
<Directory "/path/to/repo/www/chapter_05">
Options +FollowSymLinks +ExecCGI
DirectoryIndex index.py
AllowOverride All
Require all granted
</Directory>
At the beginning of any Python script you plan to run using CGI, you need to place an operating system directive indicating the path to the executable needed to run the script. In the case of Linux, the directive might appear as follows:
#!/usr/bin/python3
Finally, make sure the web server user has the rights to read the script and that the script is marked as executable. Now, let's have a look at creating a web authentication infrastructure.
When a customer wishes to make a purchase on the Sweets Complete website, they need to log in. For the purposes of this chapter, we'll present a very simple login system that accepts a username and password from an HTML form and verifies it against the customer's collection.
We will start with a Python script called /path/to/repo/www/chapter_05/index.py that's designed to run via the CGI process. This script creates an instance of our customer domain service and uses the web.auth.Authenticate class described earlier in this section. The very first line is extremely important when running Python via CGI. It allows the web server to pass off processing to the currently installed version of Python.
You'll notice that we tell Python where the source code is located and initialize important variables:
#!/usr/bin/python3
import os,sys
src_path = os.path.realpath("../../chapters/05/src")
sys.path.append(src_path)
success = False
message = "<b>Please Enter Appropriate Login Info</b>"
sess_storage = os.path.realpath("../data")
Next, we enable error handling using the cgitb library, which displays errors in an easily readable format, complete with backtrace and suggestions. We also create an instance of the web.responder class, described earlier in this section:
import cgitb
cgitb.enable(display=1, logdir=os.path.realpath("../data"))
from web.responder import Html
html_out = Html('templates/index.html')
Next, we use the cgi library to parse the form posting, looking for username and password fields:
import cgi
form = cgi.FieldStorage()
if 'username' in form and 'password' in form :
If these are present in the post data, we retrieve the values and set up a customer domain service instance:
username = form['username'].value
password = form['password'].value
from web.auth import SimpleAuth
from db.mongodb.connection import Connection
from sweetscomplete.domain.customer import CustomerService
from sweetscomplete.entity.customer import Customer
conn = Connection('localhost', 27017, Customer)
cust_service = CustomerService(conn, 'sweetscomplete')
We can then use the SimpleAuth class to perform authentication, as described earlier in this section. You can see that if authentication is successful, we add a header to the output responder class, which then causes a cookie containing the authentication token to be set. This is extremely important for the next request, where we want the user to be identified:
auth = SimpleAuth(cust_service, sess_storage)
if auth.authenticate(username, password) :
success = True
html_out.addHeader('Set-Cookie: token=' + \
auth.getToken() + ';path=/')
message = "<b>HOORAY! Successful Login.</b>"
else :
message = "<b>SORRY! Unable to Login.</b>"
The template used for this script is /path/to/repo/www/templates/index.html. Here is the body:
<div style="width: 60%;">
<h1>Simple Login</h1>
<hr>
<div style="width:100%;float:left;">
<a href="/chapter_05/select.py">PRODUCTS</a></div>
<form name="simple_auth" method="post" action="/chapter_05/index.py">
<div class="labels">Email Address:</div>
<div class="inputs"><input type="email" name="username" /></div>
<div class="labels">Password:</div>
<div class="inputs"><input type="password" name="password" /></div>
<div class="labels"><input type="submit" value="Login" /></div>
</form>
<hr>
<br>%message%
</div>
From a web browser on your local computer, enter the following URL:
http://learning.mongodb.local/chapter_05/index.py
The rendered output appears as follows:

After clicking the Login button, a success message will appear. If you examine the /path/to/repo/ww/data folder, you will see that a new session file has been created. The contents of the file might appear as follows:

The next subsection deals with presenting a list of products for the customer to purchase.
Now, we'll turn our attention to the main page, following a successful login. The first bit of coding that needs to be done is in the sweetscomplete.domain.product.ProductService class. We need to add a method that returns product information in a format suitable for web consumption. The objective is to be able to display product titles and prices. Once selected, the web form returns the selected product key and quantity to purchase.
Here is the new fetchAllKeysAndTitlesForSelect() method. It uses the pymongo.collection.Collection.find() method and supplies a projection argument that includes only the productKey, title, and price fields. The return value is stored as a Python dictionary in the keysAndTitles class variable:
# sweetscomplete.domain.product.ProductService
def fetchAllKeysAndTitlesForSelect(self) :
if not self.keysAndTitles :
projection = dict({"productKey":1,"title":1,"price":1,"_id":0})
for doc in self.db.products.find(None, projection) :
self.keysAndTitles[doc.getKey()] = doc.get('title') + \
' [ $' + str(doc.get('price')) + ' ]'
return self.keysAndTitles
Next, we make an addition to the web.responder.Html class described earlier in this chapter. The buildSelect() method accepts a parameter's name, which is used to populate the name attribute of the HTML select element. The second argument comes directly from the product domain service method we just described; that is, fetchAllKeysAndTitlesForSelect(). The return value of the new buildSelect() method is an HTML select element. The product key is returned when a product is selected:
def buildSelect(self, name, dropdown) :
output = '<select name="' + name + '">' + "\n"
for key, value in dropdown.items() :
output += '<option value="' + key + '">' + value
output += '</option>' + "\n"
output += '</select>' + "\n"
return output
We can now examine the Python /path/to/repo/www/chapter_05/select.py web script. As with the login script described earlier, we include an opening tag that tells the operating system how to execute the script. We follow that by including our source code in the Python system path and enabling errors to be displayed:
#!/usr/bin/python3
import os,sys
src_path = os.path.realpath('../../chapters/05/src')
sys.path.append(src_path)
import cgitb
cgitb.enable(display=1, logdir='../data')
Next, we perform imports and set up two database connections – one to the customers collection and another to products:
from web.responder import Html
from web.auth import SimpleAuth
from db.mongodb.connection import Connection
from sweetscomplete.domain.customer import CustomerService
from sweetscomplete.entity.customer import Customer
from sweetscomplete.domain.product import ProductService
from sweetscomplete.entity.product import Product
cust_conn = Connection('localhost', 27017, Customer)
cust_service = CustomerService(cust_conn, 'sweetscomplete')
prod_conn = Connection('localhost', 27017, Product)
prod_service = ProductService(prod_conn, 'sweetscomplete')
We then initialize the key variables and set up our output responder class:
sess_storage = os.path.realpath("../data")
auth = SimpleAuth(cust_service, sess_storage)
cust = auth.getIdentity()
html_out = Html('templates/select.html')
html_out.addInsert('%message%', '<br>')
Now for the fun part! First, we use the authentication class to retrieve the user's identity. As you may recall from our discussion earlier, if the user has successfully authenticated, a session is created and propagated via a cookie. An unauthenticated website visitor sees nothing.
For customers, we pull our list of product keys and titles from the products domain service. Here, we make an assumption that a typical purchase has a maximum of 6 line items. In the real world, of course, we would most likely use a JavaScript frontend that allows customers to add additional purchase fields at will. That discussion is reserved for the next chapter, Chapter 6, Using AJAX and REST to Build a Database-Driven Website. For the purposes of this chapter, we'll keep things simple and limit the selection to 6. Finally, the output is rendered:
cust = auth.getIdentity()
if cust :
html_out.addInsert('%name%', cust.getFullName())
keysAndTitles = prod_service.fetchAllKeysAndTitlesForSelect()
for x in range(0, 6) :
htmlSelect = html_out.buildSelect('dropdown' + str(x+1), \
keysAndTitles)
html_out.addInsert('%dropdown' + str(x+1) + '%', htmlSelect)
else :
html_out.addInsert('%name%', 'guest')
print(html_out.render())
The HTML template we used is /path/to/repo/www/templates/index.html, the body of which is shown here:
<h1>Welcome to Sweets Complete</h1>
You are logged in as: %name%
<a href="/chapter_05/index.py">LOGIN</a>
<form name="products" method="post" action="/chapter_05/purchase.py">
Choose Product(s) to Purchase:
<br>%dropdown1% Quantity:
<input type="number" name="qty1" value="1"/>
<br>%dropdown2% Quantity:
<input type="number" name="qty2" value="0"/>
<!-- other inputs not shown -->
<br><input type="submit" name="purchase" value="Purchase" />
</form>
Here is how the output appears:

Obviously, you would use much cleverer styling to improve the appearance! For the purposes of this book, however, we are using only basic HTML and minimal CSS. The last thing we need to do is purchase selections and process them.
In order to provide the customer with information on their last purchase, in the domain service class for purchases, we need to add the following method. The technique for finding the last purchase involves including a sort parameter in the pymongo.collection.Collection.find_one() method, which tells the driver to sort by dateOfPurchase in descending order:
def fetchLastPurchaseForCust(self, custKey) :
query = dict({"customerKey" : custKey})
projection = None
return self.db.purchases.find_one(query, projection, 0, 0, \
False, CursorType.NON_TAILABLE, [('dateOfPurchase', -1)])
Returning to the HTML responder class, we add another custom method that pulls information out of the Purchase object that's presented as an argument. Note that productsPurchased is itself a dictionary of ProductPurchase objects. Accordingly, a for loop is needed for extraction:
def buildLastPurchase(self, purchInfo) :
output = ''
output += '<h3>Purchase Info</h3>' + "\n"
output += '<table width="50%">' + "\n"
output += '<tr><th>Date</th><td>'
output += purchInfo.get('dateOfPurchase')+'</td></tr>' + "\n"
output += '<tr><th>Total</th><td>'
output += str(purchInfo.get('extendedPrice'))+'</td></tr>' + "\n"
output += '</table>' + "\n"
output += '<h3>Product(s)</h3>' + "\n"
output += '<table width="60%">' + "\n"
output += '<tr><td><i>Title</i></td>'
output += '<td><i>Quantity</i></td>'
output += '<td><i>Price</i></td></tr>' + "\n"
for key, prodInfo in purchInfo.get('productsPurchased').items() :
output += '<tr>'
output += '<td>' + prodInfo['title'] + '</td>'
output += '<td>' + str(prodInfo['qtyPurchased']) + '</td>'
output += '<td>' + str(prodInfo['price']) + '</td>'
output += '<tr>' + "\n"
output += '</table>' + "\n"
return output
The /path/to/repo/www/chapter_05/purchase.py web script starts much like the others shown previously:
#!/usr/bin/python3
import os,sys
src_path = os.path.realpath("../../chapters/05/src")
sys.path.append(src_path)
import cgitb
cgitb.enable(display=1, logdir=".")
The imports are more complicated. In order to build a Purchase instance, we need access to all three domain service classes for customers, products, and purchases:
from web.responder import Html
from web.auth import SimpleAuth
from db.mongodb.connection import Connection
from sweetscomplete.domain.customer import CustomerService
from sweetscomplete.entity.customer import Customer
from sweetscomplete.domain.product import ProductService
from sweetscomplete.entity.product import Product
from sweetscomplete.entity.product_purchased import ProductPurchased
from sweetscomplete.domain.purchase import PurchaseService
from sweetscomplete.entity.purchase import Purchase
Then, we need to build instances of these services, as shown here. Note that, in each case, we are creating a connection to a mongod instance running on the localhost. Each connection instance represents a connection to a different MongoDB collection: customers, products, and purchases. The third argument causes the PyMongo driver to return results in the form of the corresponding Python entity classes we defined previously, each associated with a specific collection. Here is the code that creates these connections:
cust_conn = Connection('localhost', 27017, Customer)
cust_service = CustomerService(cust_conn, 'sweetscomplete')
prod_conn = Connection('localhost', 27017, Product)
prod_service = ProductService(prod_conn, 'sweetscomplete')
purch_conn = Connection('localhost', 27017, Purchase)
purch_service = PurchaseService(purch_conn, 'sweetscomplete')
Next, we set up session storage, create a web.authenticate.SimpleAuth instance, and check the customer's identity. An instance of our output HTML responder class is also created. Note that if there is no identity, we redirect back home:
sess_storage = os.path.realpath("../data")
auth = SimpleAuth(cust_service, sess_storage)
cust = auth.getIdentity()
html_out = Html('templates/purchase.html')
if not cust :
print("Location: /chapter_05/index.py\r\n")
print()
quit()
We now scan the CGI input to see if a field called purchase, which is the name of the submit button, has been posted:
import cgi
form = cgi.FieldStorage()
if 'purchase' in form :
If so, we are now ready to start building the Purchase instance. We start by populating dateOfPurchase with today's date and time. We get customer information from the identity provided by the authenticator. This information is used to form the transactionId:
import datetime
import random
today = datetime.datetime.today()
dateOfPurchase = today.strftime('%Y-%m-%d %H:%M:%S')
ymd = today.strftime('%Y%m%d')
fname = cust.get('firstName')
lname = cust.get('lastName')
transactionId = ymd + fname[0:4] + lname[0:4] + \
str(random.randint(0, 9999))
The Purchase instance is created and populated with Customer information first:
purchase = Purchase()
purchase.set('transactionId', transactionId)
purchase.set('dateOfPurchase', dateOfPurchase)
purchase.set('customerKey', cust.getKey())
purchase.set('firstName', cust.get('firstName'))
purchase.set('lastName', cust.get('lastName'))
purchase.set('phoneNumber', cust.get('phoneNumber'))
purchase.set('email', cust.get('email'))
purchase.set('streetAddressOfBuilding',
cust.get('streetAddressOfBuilding'))
purchase.set('city', cust.get('city'))
purchase.set('stateProvince', cust.get('stateProvince'))
purchase.set('locality', cust.get('locality'))
purchase.set('country', cust.get('country'))
purchase.set('postalCode', cust.get('postalCode'))
purchase.set('latitude', cust.get('latitude'))
purchase.set('longitude', cust.get('longitude'))
We then loop through the purchase information that's posted, only posting items where the quantity is greater than zero:
productsPurchased = dict()
for x in range(1,7) :
qtyKey = 'qty' + str(x)
if qtyKey in form :
qty = int(form[qtyKey].value)
if qty > 0 :
dropKey = 'dropdown' + str(x)
prodKey = form[dropKey].value
prodInfo = prod_service.fetchByKey(prodKey)
if prodInfo :
prodPurch = ProductPurchased(prodInfo.toJson())
prodPurch.set('qtyPurchased', qty)
productsPurchased[prodKey] = prodPurch
The newly created dictionary of ProductPurchased instances is inserted into the Purchase object and saved to the database:
purchase.set('productsPurchased', productsPurchased)
purch_service.addOne(purchase)
Finally, some output is produced using the HTML output responder class:
if cust :
html_out.addInsert('%message%', '<b>Thanks for your purchase!</b>')
html_out.addInsert('%name%', cust.getFullName())
mostRecent = html_out.buildLastPurchase( \\
purch_service.fetchLastPurchaseForCust(cust.getKey()))
html_out.addInsert('%purchase%', mostRecent)
else :
html_out.addInsert('%name%', 'guest')
print(html_out.render())
The body of the associated web template, /path/to/repo/www/chapter_05/templates/purchase.html, is shown here:
<div style="width: 60%;">
<h1>Welcome to Sweets Complete</h1>
You are logged in as: %name%
<a href="/chapter_05/select.py">SELECT</a>
<h2>Here is a your most recent purchase:</h2>
<hr>
%purchase%
<hr>
<br>%message%
</div>
And finally, here is the output:

Now, let's turn our attention to the cancellation process.
There are two ways to handle canceling a purchase. One technique is to hold purchase information in a temporary file, or in session storage, until the customer is ready to confirm this. Once confirmed, the purchase information can be stored permanently in the database after payment confirmation. Another technique we will discuss here is storing the purchase information immediately and then providing the customer with a cancel option. If a purchase is canceled, we simply delete the document from the database.
Before we approach the changes that need to be made to the /path/to/repo/www/chapter_05/purchase.py web script, we must first discuss the pymongo.collection.Collection.delete_* methods.
Oddly, both the delete_one() and delete_many() methods have a lot in common with find(): they both accept a filter dictionary, which is identical to a query document. Here is the syntax summary for these commands:
delete_one(filter, collation=None, session=None)
delete_many(filter, collation=None, session=None)
The main difference between these two commands is how many documents they are capable of deleting: one or many. Here is a brief summary of the command syntax:
| Parameter | Default | Notes |
| filter | None | A dictionary consisting of key/value pairs, where the key represents the field to search and the value represents the search criteria. If set to None, all the documents in the collection are selected. |
| collation | None | You can supply a pymongo.collation.Collation instance, which allows you to influence how search and sort operate. This is especially useful if you are collecting information on a website that services an international clientele. |
| session | None | If you supply a pymongo.client_session.ClientSession instance, you are able to initiate transaction support, which allows a set of database operations to be treated as a single atomic block. The session also gives you control over read and write concerns, which are important when operating on a replica set. |
The first order of business is to add a Cancel button to the last page in the web flow described earlier. We add this to the /path/to/repo/www/chapter_05/templates/purchase.html HTML template:
<form action="/chapter_05/purchase.py" method="post">
<input type="submit" name="cancel" value="Cancel" /></form>
We then modify purchase.py to scan for this extra input in the form post data. The main changes involve changing the message inserts to account for a cancellation. We also scan for the presence of cancel in the form:
if cust :
html_out.addInsert('%name%', cust.getFullName())
lastPurch = purch_service.fetchLastPurchaseForCust(cust.getKey())
if not lastPurch :
html_out.addInsert('%purchase%', 'No recent purchase found')
else :
mostRecent = html_out.buildLastPurchase(lastPurch)
html_out.addInsert('%purchase%', mostRecent)
if 'cancel' in form :
if purch_service.deleteByKey(lastPurch.getKey()) :
html_out.addInsert('%message%', \
'<b>Purchase cancelled.</b>')
else :
html_out.addInsert('%message%', \
'<b>Sorry! Unable to cancel purchase.</b>')
else :
html_out.addInsert('%message%', \
'<b>Thanks for your purchase!</b>')
else :
html_out.addInsert('%name%', 'guest')
print(html_out.render())
The resulting output reverts to the purchase prior to the recently canceled one:

This brings us to the end of this chapter. Let's summarize what we have covered in this chapter.
In this chapter, you learned how to create a set of database classes that can then be used throughout your application. The Connection class provides access to a pymongo.mongo_client.MongoClient instance, from which you can access a database and collection. You then learned how to define domain service classes, each of which provides methods that are pertinent to a MongoDB collection. In many cases, these classes provide wrappers for various pymongo.collection.Collection.* methods, including find(), find_one(), update_one(), update_many(), insert_one(), insert_many(), delete_one(), and delete_many().
You were then shown how to model a query first in the mongo shell using JavaScript and then how to adapt it to Python using the domain services classes. First, you learned how to generate a product sales report. Then, you were shown how to use the domain service classes to update a product.
Finally, the last two sections introduced a set of classes that provide support for a web application. In the example covered in this chapter, you learned how to authenticate a customer using CustomerService. You then learned how to use ProductService to generate a list of products available for purchase. Finally, you learned how to use all three domain service classes to build a Purchase instance that was then saved to the database. After that, we provided a short section on canceling a purchase.
The next chapter moves you to the next level of web development by showing you how to use a responder class to produce JavaScript Object Notation (JSON) output, and a web environment that can accept Asynchronous JavaScript and XML (AJAX) queries or Representation State Transfer (REST) requests.
In this chapter, you will move on to the next level, away from a standard web application and into what is needed to have the application respond to AJAX queries and REpresentational State Transfer (REST) requests. The tasks presented in this chapter build on the example presented in the previous chapter, Chapter 5, Mission-Critical MongoDB Database Tasks.
In this chapter, we will first introduce you to the concept of pagination, and how it might be applied to a purchase history. You will then learn how to incorporate jQuery DataTables into a list of products for purchase. You will also be shown how to serve graphics directly from the database. Finally, you will learn how to handle a request coming from a fictitious external partner using REST.
In this chapter, we will cover the following topics:
The minimum recommended hardware is as follows:
The software requirements are as follows:
The installation of the required software and how to restore the code repository for this book is explained in Chapter 2, Setting Up MongoDB 4.x. To run the Python code examples in this chapter from the command line, open a Terminal window (Command Prompt) and enter these commands:
cd /path/to/repo
docker-compose build
docker-compose up -d
docker exec -it learn-mongo-server-1 /bin/bash
To run the demo website described in this chapter, follow the setup instructions given in Chapter 2, Setting Up MongoDB 4.x. Once the Docker container for chapters 3 through 12 has been configured and started, there are two more things you need to do:
172.16.0.11 learning.mongodb.local
http://learning.mongodb.local/
When you are finished working with the examples covered in this chapter, return to the Terminal window (Command Prompt) and stop Docker, as follows:
cd /path/to/repo
docker-compose down
The code used in this chapter can be found in this book's GitHub repository at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/chapters/06, as well as https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/www/chapter_06.
Fortunately, gone are the bad old days where developers would just display all the results of a database query, regardless of the number of lines. The problem with this approach is that website's visitors were forced to scroll endlessly to find what they were looking for among the results. In order to provide a better experience for your website visitors, you can apply pagination to your query results. The idea is simple: on any given web page, only display a preset number of lines. At the bottom of the page, the website visitors see a navigation control that allows them to move between pages of data.
To implement pagination, you need to set a parameter reflecting how many items you want to appear on a theoretical page. A common setting is 20 items per page. The objective is to scan through the query result set 20 results at a time, with options to go forward and backward.
Returning to the pymongo.collection.Collection.find() method, you might recall two options we have not yet used: skip and limit. These two options allow us to achieve pagination. In the sweetscomplete.domain.purchase.Purchase domain service, we add a new method, fetchPaginatedByCustKey(). In the new method, we accept a customer key as an argument in order to build a query:
# sweetscomplete.domain.purchase.Purchase
def fetchPaginatedByCustKey(self, custKey, skip, limit) :
query = {'customerKey' : custKey}
for doc in self.db.purchases.find(query, None, skip, limit) :
yield doc
We also accept arguments for skip and limit, which is crucial for pagination to occur properly.
The next step is to add a method to the web.responder.html.Html class that accepts the output produced by fetchPaginatedByCustKey() and returns HTML. We also need to accept the current page number and a base URL to create links to the previous and next pages. Note that if the previous page number drops below 0, we simply reset it to 0.
The method might appear as follows:
# web.responder.html.Html
def buildPurchaseHistory(self, purchHist, pageNum, baseUrl) :
import locale
nextPage = pageNum + 1
prevPage = pageNum - 1
if prevPage < 0 : prevPage = 0
Next, we begin developing the output by defining some styles and displaying a banner:
output = ''
output += '<style>' + "\n"
output += '.col1 { width:20%;float:left;font-size:10pt; }' + "\n"
output += '.col2 { width:10%;float:left;font-size:10pt; }' + "\n"
output += '.col3 { width:70%;float:left;font-size:10pt; }' + "\n"
output += '.full { width:100%;float:left;font-size:10pt; }' + "\n"
output += '</style>' + "\n"
output += '<h3>Purchase History</h3>' + "\n"
output += '<div class="col1"><b>Date</b></div>'
output += '<div class="col2"><b>Total</b></div>'
output += '<div class="col3"><b>Products</b></div>' + "\n"
The outer loop iterates through the results of the query to the purchases collection. The inner loop goes through the products purchased. The final output is appended to a string, represented by the output variable, as shown here:
for purchase in purchHist :
output += '<div class="col1">' + \
purchase.get('dateOfPurchase') + '</div>'
output += '<div class="col2">'
output += locale.format('%.*f', \
(2, purchase.get('extendedPrice')), True)
output += '</div>'
output += '<div class="col3">'
output += '<div class="col3"><i>Title</i></div>'
output += '<div class="col2"><i>Qty</i></div>'
output += '<div class="col1"><i>Price</i></div>' + "\n"
for key, prodInfo in purchase.get('productsPurchased').items() :
output += '<div class="col3">'
output += prodInfo['title'] + '</div>'
output += '<div class="col2">'
output += locale.format('%4d', \
prodInfo['qtyPurchased']) + '</div>'
output += '<div class="col1">'
output += locale.format('%.*f', \
(2, prodInfo['price']), True) + '</div>'
output += '</div>' + "\n"
Finally, we present the navigation controls in the form of simple links with a page parameter:
# output control
output += '<div class="full">'
output += '<a href="' + baseUrl + '?page='
output += str(prevPage) + '">Previous</a>'
output += ' | ' + str(pageNum)
output += ' | '
output += '<a href="' + baseUrl + '?page='
output += str(nextPage) + '">Next</a>'
output += '</div>'
return output
Obviously, the output could use refinements in the form of JavaScript functions to show or hide details on the products purchased. Background colors and styling can improve the appearance as well. We forego that to simplify the discussion and retain focus on our main goal: demonstrating how to get a Python web script to produce paginated output generated by MongoDB.
Before we get to the action script, we will introduce the concept of storing configuration in one place. This is a useful technique as it allows you to define a dictionary of values that change from site to site. This fosters reusability and makes maintenance easier as all such parameters are in one place. For this purpose, here is the path, /path/to/repo/chapters/06/src/config/config.py, in which a class, Config, is defined. It has a config property and a single method, getConfig():
# config.config
import os
class Config :
config = dict({
'db' : {
'host' : 'localhost',
'port' : 27017,
'database' : 'sweetscomplete'
},
'pagination' : {
'lines' : 4,
'baseUrl' : '/chapter_06/history.py'
},
'session' : {
'storage' : os.path.realpath('../../www/data')
}
})
def getConfig(self, key = None) :
if key :
return self.config[key]
return self.config
We then use this class and its configuration in the new script, described next.
We will now return to the web application developed in Chapter 5, Mission-Critical MongoDB Database Tasks. To keep things simple, we add a new web action script, /path/to/repo/www/chapter_06/history.py, which presents a paginated list of a customer's purchase history using the classes and methods described previously.
As with the other scripts, we import the classes described in the previous chapter, the web.responder.html.Html class, as well as web.auth.SimpleAuth. The cgitb Python module is used to display errors and should be removed from a live server:
#!/usr/bin/python
import os,sys
src_path = os.path.realpath("../../chapters/06/src")
sys.path.append(src_path)
import cgitb
cgitb.enable(display=1, logdir=".")
from config.config import Config
from web.auth import SimpleAuth
from web.responder.html import Html
from db.mongodb.connection import Connection
from sweetscomplete.domain.customer import CustomerService
from sweetscomplete.entity.customer import Customer
from sweetscomplete.domain.purchase import PurchaseService
from sweetscomplete.entity.purchase import Purchase
Next, we create a CustomerService instance used for authentication. Of course, PurchaseService is used to return the customer's purchase history. Note how we use the new Config class to supply parameters instead of hardcoding them in the following code:
config = Config()
db_config = config.getConfig('db')
cust_conn = Connection(db_config['host'], db_config['port'], Customer)
cust_service = CustomerService(cust_conn, db_config['database'])
purch_conn = Connection(db_config['host'], db_config['port'], Purchase)
purch_service = PurchaseService(purch_conn, db_config['database'])
sess_config = config.getConfig('session')
auth = SimpleAuth(cust_service, sess_config['storage'])
cust = auth.getIdentity()
response = Html('templates/history.html')
We use cgi to pull the page number. As you will recall, the web responder class uses this page number in order to generate links for the previous and next pages. You might note that as a security measure, we force the page number to int. This prevents attackers from attempting a cross-site scripting attack (https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)):
import cgi
info = cgi.FieldStorage()
if 'page' in info :
pageNum = int(info['page'].value)
else :
pageNum = 0
response.addInsert('%message%', \
'You are currenty viewing page: ' + str(pageNum))
As you can see, the web script code is quite simple, mainly because all the work is being done by the purchases domain service and responder classes. The only real tricky bit is to calculate the skip value by multiplying the page number by the limit value:
if cust :
response.addInsert('%name%', cust.getFullName())
page_config = config.getConfig('pagination')
limit = page_config['lines']
skip = int(pageNum * limit)
purchHist = purch_service.fetchPaginatedByCustKey(
cust.getKey(), skip, limit)
if not purchHist :
response.addInsert('%history%', 'No recent purchase found')
else :
historyHtml = response.buildPurchaseHistory(
purchHist, pageNum, page_config['baseUrl'])
response.addInsert('%history%', historyHtml)
else :
response.addInsert('%name%', 'guest')
response.addInsert('%history%', \
'Need to login to view purchase history')
As with the other web scripts presented so far, we create a simple HTML template to be used with the script. That template is located in /path/to/repo/www/chapter_06/templates/history.html, and the body of it appears as follows:
<div style="width: 60%;">
<h1>Welcome to Sweets Complete</h1>
You are logged in as: %name%
<hr>
<a href="/chapter_06/index.py">LOGIN</a> |
<a href="/chapter_06/select.py">SELECT</a> |
<a href="/chapter_06/purchase.py">RECENT</a>
<hr>
%history%
<hr>
<br>%message%
</div>
Here is the output for a fictitious customer, Yadira Conway (login email: yconway172@Turkcell.com):

In the pagination config, we set the number of lines to 4 to better illustrate how the pagination works. From the preceding screen, we then click Next, the page parameter in the URL goes to 1, and we see the remaining set of products:

In the next section, you will learn a slightly different way of designing your web application by introducing jQuery DataTables, which makes an AJAX request to the web application and expects a JSON response.
AJAX stands for Asynchronous JavaScript and XML. It is not a protocol in and of itself, but is rather a mixture of technologies that includes HTML and JavaScript. This mixture of technologies represents a major paradigm shift away from the traditional approach where a person sitting in front of a desktop PC opens their browser and makes a request of a website, which in turn delivers static HTML content.
Here is a diagram of the traditional request-response sequence:

AJAX, on the other hand, turns the tables on the traditional request-response sequence illustrated in the preceding diagram. Instead, what happens is that the initial request delivers HTML content, including one or more JavaScript applications. The JavaScript applications run locally on the person's browser or smart device. From that point on, in most cases, it is the JavaScript applications that make small-scale asynchronous requests in the background.
Here is a diagram of an AJAX request-response sequence:

There are two primary advantages to this approach:
In order to illustrate how this might play out using MongoDB, let's turn our attention back to the product selection page from the basic web application for Sweets Complete described in the previous chapter. Wouldn't it be nice to have a neatly styled, searchable, and paginated table of products for purchase, rather than the current, rather unsightly clump of HTML select elements? This is where jQuery DataTables comes into play.
jQuery is one of the most popular JavaScript libraries. It includes classes and functions that allow you to quickly traverse and manipulate a web page, without having to force a full-page refresh every time something changes. A minimal version is available to reduce the amount of overhead. It is actively maintained and is supported by a large variety of browsers.
If you look at the actual statistics on https://github.com/jquery/jquery, you will see that the library has almost 300 contributors, over 53,500 star ratings, and has been forked over 19,400 times (that means 19,400 other projects are using this library). In terms of the market share of all the websites surveyed, W3Techs places jQuery at over 75% as of July 2020. The next highest comparable JavaScript library is Bootstrap, which comes in at 21%.
DataTables is a jQuery plugin, requiring jQuery in order to operate. It creates a widget in which there are rows of data displayed. The widget is fully configurable in terms of size, fonts, styles, and so forth. It can include a search box, supports pagination, and lets you set what columns can be used as sort criteria.
Documentation references:
In order to incorporate jQuery and DataTables into the simple web application developed in the previous chapter, we turn our attention to /path/to/repo/www/chapter_06/templates/select.html. This is the web template associated with the product selection process. In the template, we need to do the following:
In the HEAD portion of the HTML page, we include links to a datatables stylesheet:
<link rel="stylesheet" type="text/css" \\
href="https://cdn.datatables.net/1.10.19/css/jquery.dataTables.css">
In addition, we need to create a skeleton template that DataTables uses to create the widget. There are two important things to note here. The first is that the entire data table is enclosed in an HTML form. The second is that unless you perform some manipulation in the JavaScript code used to initialize the data table, the number of columns must match the number being sent to the data table following an AJAX request:
<form id="form_products" method="post" action="/chapter_06/purchase.py">
<p>Choose Product(s) to Purchase:</p>
<table id="data_table" class="display">
<thead>
<tr>
<th>Title</th>
<th>Price</th>
<th>Qty</th>
</tr>
</thead>
<tbody>
<tr>
<td>Row 1 Data 1</td>
<td>Row 2 Data 2</td>
<td>Row 3 Data 3</td>
</tr>
</tbody>
</table>
<br><input type="submit" name="purchase" value="Purchase" />
</form>
Finally, we add JavaScript to initialize the data table. This is where we designate the data source as an AJAX request. The %ajax_url% parameter is substituted for an actual URL by the web action select.py script. In this illustration, we set the number of rows displayed by the data table to 6 in order to see the effect on pagination:
<script charset="utf8" src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script charset="utf8" src="https://cdn.datatables.net/1.10.19/js/jquery.dataTables.js"></script>
<script type="text/javascript" charset="utf8">
$(document).ready( function () {
table = $('#data_table').dataTable({
'ajax' : {'url':'%ajax_url%','type': 'GET'},
'pageLength' : 6,
'autoWidth' : false
});
});</script>
Finally, we include JavaScript to ensure that all the data table elements are returned when the form is posted. Otherwise, the only data returned is the page currently showing on the screen. What this script does is add data table inputs that are not in the current DOM (that is, not showing up on the screen) as hidden form elements:
<script type="text/javascript" charset="utf8">
$('#form_products').on('submit', function(e){
var form = this;
var params = table.$('input').serializeArray();
$.each(params, function(){
if(!$.contains(document, this)){
$(form).append(
$('<input>').attr('type', 'hidden')
.attr('name', this.name).val(this.value)
);}
}); }); </script>
Unfortunately, however, the hidden elements end up serialized as lists, which we need to deal with when form results are processed. Next, we will define a JSON responder class.
As you have seen earlier in this chapter, we used a responder class that produced HTML. For the purposes of answering AJAX requests, we develop a responder class that produces JSON. For this purpose, we create a file, /path/to/repo/chapters/06/src/web/responder/json.py, in which we define a JsonResponder class.
As we plan to return JSON, in the __init__() method, we can add a Content-Type header set to application/json:
# web.responder.json
import json
class JsonResponder :
headers = []
data = []
def __init__(self) :
self.addHeader('Content-Type: application/json')
As with the Html responder class, we define a method that allows us to add additional headers. Instead of the addInsert() method used in the Html responder class, we define a method, addData(), which simply adds to an internal dictionary:
def addHeader(self, header) :
self.headers.extend([header])
def addData(self, key, value) :
self.data[key] = value
We also add a custom method that returns data in a format acceptable to DataTables. The title and price fields are straight out of the database query. You might note that the qty field, on the other hand, is actually HTML that ends up getting rendered inside the data table as a form input field. We append the product key to the qty_ prefix and extract it later. Also, note that we append each entry for the products as a straight list, which is what the data table is expecting:
def addProductKeyTitlePrice(self, key, data) :
temp = []
for product in data :
title = product.get('title')
price = product.get('price')
qty = '<input class="qty" name="qty_'
qty += product.get('productKey')
qty += '" value="0" type="number" />'
temp.append([title,price,qty])
self.data[key] = temp
The internal dictionary, data, is then returned in JSON format in the render() method:
def render(self) :
output = ''
for item in self.headers :
output += item + "\r\n"
output += "\r\n"
output += json.dumps(self.data)
return output
Let's now learn how to deliver data from MongoDB.
To deliver data from MongoDB, we modify the sweetscomplete.domain.product.ProductService domain service class. As per the action-domain-responder pattern, the domain service has no knowledge of what format the output takes. The only concern of the new method is to make sure it delivers a list of sweetscomplete.entity.product.Product entities that contain the information needed. Formatting is handled by the responder class.
The new method, fetchAllKeysTitlesPricesForAjax(), returns a list of products that includes productKey, title, and price:
# sweetscomplete.domain.product.ProductService
def fetchAllKeysTitlesPricesForAjax(self, skip = 0, limit = 0) :
projection = dict({"productKey":1,"title":1,"price":1,"_id":0})
result = []
for doc in self.db.products.find(None, projection, skip, limit) :
result.append(doc)
return result
In addition, we need a very simple method that only returns product keys:
def fetchKeys(self) :
result = dict()
projection = dict({"productKey":1,"_id":0})
for doc in self.db.products.find(None, projection) :
result[doc.getKey()] = doc.getKey()
return result
We will use the preceding method later to reconstruct the names of data table form elements associated with the quantity purchased.
The next step is to create a script that jQuery DataTables can call, and subsequently returns the requested data in JSON format. This action script (/path/to/repo/www/chapter_06/ajax.py) makes use of both the products domain service as well as the new JSON responder class discussed in the previous sections.
As with the other web scripts we've described so far, we add an initial tag that tells the web server's CGI process how to run the script. We then perform the appropriate imports:
#!/usr/bin/python
import os,sys
src_path = os.path.realpath('../../chapters/06/src')
sys.path.append(src_path)
from config.config import Config
from web.responder.json import JsonResponder
from web.auth import SimpleAuth
from db.mongodb.connection import Connection
from sweetscomplete.domain.product import ProductService
from sweetscomplete.entity.product import Product
Next, we use the new Config class to create the Connection, Product, and ProductService instances:
config = Config()
db_config = config.getConfig('db')
prod_conn = Connection(db_config['host'], db_config['port'], Product)
prod_service = ProductService(prod_conn, db_config['database'])
Finally, we run a query off ProductService and use the result to populate the data property of the JsonResponder class. We then output the rendered output:
response = JsonResponder()
data = prod_service.fetchAllKeysTitlesPricesForAjax()
response.addProductKeyTitlePrice('data', data)
print(response.render())
In order to test whether or not this script works, you can run it directly from your browser. Assuming you have followed the directions for creating a test environment, use this URL: http://learning.mongodb.local/chapter_06/ajax.py
Here is the current output:

Next, we will learn how to modify the product select web action script.
In order to activate the data table, we now need to make some modifications to the original script that presents a list of products to be purchased. As you recall from the last chapter, we simply presented six HTML select elements, with separate inputs for the quantities purchased. I think we can all agree this looks pretty bad! We now replace that logic with a single data table.
There are three main differences between /path/to/repo/www/chapter_05/purchase.py and the one we're examining for this illustration. The first is that we use the new Config class to provide a common configuration. The second major difference is that the responder class has been moved one level deeper. Instead of from web.responder.Html, the purchase script now defines this class as web.responder.html.Html:
from config.config import Config
from web.responder.html import HtmlResponder
The third difference is that instead of using the responder class addInsert() method to substitute six HTML select elements, we only need to provide a URL for the data table AJAX source, represented in the template as %ajax_url%:
response = HtmlResponder('templates/select.html')
response.addInsert('%message%', '<br>')
response.addInsert('%ajax_url%', config.getConfig('ajax_url'))
The remainder of the script remains the same, as described in the previous chapter. The output, however, is radically different, as shown here using the http://learning.mongodb.local/chapter_06/select.py URL:

In the next section, we will learn how to modify the product purchase web action script.
We can now turn our attention to the web script that processes purchases: /path/to/repo/www/chapter_06/purchase.py. This script was first introduced in Chapter 5, Mission-Critical MongoDB Database Tasks. As with the modified select.py script, described in the previous section of this chapter, we use the new Config service, as well as having moved the responder class, as shown:
from web.responder.html import HtmlResponder
html_out = HtmlResponder('templates/purchase.html')
The main difference in this version is how a list of the products purchased is built. As before, we initialize an empty dictionary, productsPurchased. We then add a dictionary obtained from the products domain service that contains product keys:
prodKeys = prod_service.fetchKeys()
productsPurchased = dict()
We loop through the list of product keys and recreate the quantity purchased form element name sent to the data table: qtyKey = 'qty_' + key. We then check to see whether this key is present in the form's HTTP POST data. If so, we retrieve that value, returned in the form of a cgi.MiniFieldStorage instance.
In this block of code, we loop through return values from the product keys database lookup. In the loop, we recreate the form of the field name by prepending the qty_ prefix. We then check to see whether this is present in the form posting. If so, it's retrieved into a variable, item:
for key, value in prodKeys.items() :
qtyKey = 'qty_' + key
if qtyKey in form :
item = form[qtyKey]
As you learned from the discussion on how to recover all the data table elements not present in the DOM, they are appended as list items to the form. Accordingly, we need to check to see whether the item is an instance of list. If so, we retrieve element 0. We then force the quantity to the int data type, which has the effect of removing any potential XSS markup:
if isinstance(item, list) :
item = item[0]
qty = int(item.value)
Finally, once we have the quantity, we check to make sure the value is greater than 0. If so, we look up the product information using the product key and the products domain service, and create a ProductPurchased instance:
if qty > 0 :
prodInfo = prod_service.fetchByKey(key)
if prodInfo :
prodPurch = ProductPurchased(prodInfo.toJson())
prodPurch.set('qtyPurchased', qty)
productsPurchased[key] = prodPurch
The remaining code from the original purchase.py script is exactly as presented in the previous chapter. The ProductPurchased instance is added to the Purchase instance, and then saved to the database. Also, all the output logic remains the same. The output from a completed purchase is not shown as it is exactly the same as presented in the previous chapter.
Next, we will have a look at how to serve binary data directly from MongoDB.
When browsing through the sample data for products, you might have noticed a field, productPhoto, with an exceptionally large amount of seemingly gibberish data. Here is a screenshot from the first product in the collection showing part of the first lines of data for this field:

This data represents Base64-encoded binary data, which, when decoded, is a photo of the product. In this section, we will show you how to cause a web script to display the product photo directly out of the database.
There are two approaches to rendering a Base64-encoded image directly from the database. One technique is to create a standalone Python script that accepts a product key as a parameter, looks up the product, Base64 decodes the data, and outputs an appropriate Content-Type header for that image, along with the actual binary image data.
The second approach is to output the image using the following format:
<img src="data:image/xxx;base64,yyy" />
Here, xxx is the image type (for example, image/png) and yyy is the actual Base64 string. Although this approach puts the burden on the browser to perform the decoding, it minimizes the impact on the database and web server. For the purposes of illustration, we will use the second approach.
Accordingly, we return to our HTML responder class and add a method that produces the appropriate HTML image tag:
# web.responder.html.Html
mimeTypes = dict({'png':'image/png','jpg':'image/jpeg'})
mimeDefault = 'jpg'
def buildBase64Image(self, name, mimeType, contents) :
tag = '<img name="' + name + '" src="data:'
if mimeType in self.mimeTypes :
tag += self.mimeTypes[mimeType]
else :
tag += self.mimeTypes[mimeDefault]
tag += ';base64,' + contents + '" />'
return tag
Next, we will create an HTML template for the product details.
We can now create an HTML template that gives us the product details. The body is shown here:
<h2>%title%</h2>
<table width="80%">
<tr>
<td align="top">%photo%</td>
<td align="top">
<table>
<tr><th>Product Key</th><td> </td><td>%productKey%</td></tr>
<tr><th>SKU Number</th><td> </td><td>%skuNumber%</td></tr>
<tr><th>Category</th><td> </td><td>%category%</td></tr>
<tr><th>Price</th><td> </td><td>%price%</td></tr>
<tr><th>Unit</th><td> </td><td>%unit%</td></tr>
<tr><th>On Hand</th><td> </td><td>%unitsOnHand%</td></tr>
</table>
</td>
</tr>
</table>
<p>%description%</p>
Let's now create a display script.
The next thing we do is define a standalone Python action script, /path/to/repo/www/chapter_6/details.py, which uses the product domain service and the graphic responder to render the photo and details on a specific product. As with the other scripts, we include a hashtag that instructs the web server's CGI process on how to handle the script. After that is a set of import statements:
#!/usr/bin/python
# tell python where to find source code
import os,sys
src_path = os.path.realpath('../../chapters/06/src')
sys.path.append(src_path)
import cgitb
cgitb.enable(display=1, logdir='../data')
from config.config import Config
from web.responder.html import HtmlResponder
from db.mongodb.connection import Connection
from sweetscomplete.domain.product import ProductService
from sweetscomplete.entity.product import Product
Next, we set up the database connection and product service using the Config class. We also initialize HtmlResponder:
config = Config()
db_config = config.getConfig('db')
prod_conn = Connection(db_config['host'], db_config['port'], Product)
prod_service = ProductService(prod_conn, db_config['database'])
message = 'SORRY! Unable to find information on this product'
response = HtmlResponder('templates/details.html')
We can now check to see whether a product key was included in the URL. If so, we perform a lookup using the domain service:
import cgi
form = cgi.FieldStorage()
if 'product' in form :
key = form['product'].value
product = prod_service.fetchByKey(key)
If a product is found for this key, we use the responder's addInsert() method to perform replacements in the template:
if product :
title = product.get('title')
message = 'Details on ' + title
imgTag = response.buildBase64Image('photo', 'png', \
product.get('productPhoto'))
response.addInsert('%photo%', imgTag)
response.addInsert('%title%', title)
response.addInsert('%productKey%', product.get('productKey'))
response.addInsert('%skuNumber%', product.get('skuNumber'))
response.addInsert('%category%', product.get('category'))
response.addInsert('%price%', product.get('price'))
response.addInsert('%unit%', product.get('unit'))
response.addInsert('%unitsOnHand%', product.get('unitsOnHand'))
response.addInsert('%description%', product.get('description'))
response.addInsert('%message%', message)
print(response.render())
Finally, we bail out to the main page if a product is not found or is not in the URL:
else :
print("Location: /chapter_06/index.py\r\n")
print()
quit()
else :
print("Location: /chapter_06/index.py\r\n")
print()
quit()
Next, we will modify the AJAX response.
At this point, it's time to revisit web.responder.JsonResponder. The objective is to modify the output sent to the data table in such a manner that all product titles become links to the detail page for that product. Accordingly, in addProductKeyTitlePrice(), notice how title is now wrapped in the following:
<a href="/chapter_06/details.py?product=PRODKEY">TITLE</a>
In the new method, shown here, we loop through incoming and extract information from the product instances. We build a list, temp, which is ultimately added to the internal dictionary, data. Here is the modified block of code:
def addProductKeyTitlePrice(self, key, incoming) :
temp = []
for product in incoming :
prodKey = product.get('productKey')
title = '<a href="/chapter_06/details.py?product='
title += prodKey + '">' + product.get('title') + '</a>'
price = product.get('price')
qty = '<input class="qty" name="qty_' + prodKey
qty += '" value="0" type="number" />'
temp.append([title,price,qty])
self.data[key] = temp
Next, let's revisit the product selection action script.
Now, when we log in and choose the SELECT option, the title field in the data table is now a link. If we click on a link, we are taken to the new product details page, which displays the Base64-encoded photo, along with other pertinent details. The example shown here is the Apple Pie product:

If you then view the page source, you will see that the HTML img tag contains Base64 data. We have truncated the screenshot to conserve space in the book:

In the next section, we will turn our attention to a different way of accessing the web application, called REST.
In this section, we will assume a fictional scenario where Sweets Complete starts a partner channel. The partners need access to basic product information, including product keys, titles, and prices. For maximum flexibility, for the purposes of this illustration, let's say that Sweets Complete has decided to develop a simple Application Programming Interface (API).
All the partner needs to do is to make a REST request of the Sweets Complete website, and the partner can obtain information on a single product or all products. Before we get into the exact details, however, it's important to digress for a moment and examine what REST is.
REST was introduced in a doctoral thesis by Roy Fielding, best known for his work on HyperText Markup Language (HTML) and the extremely popular Apache web server. His thesis described a network of resources identified by a Uniform Resource Identifier (URI). As a user accesses these resources, the current state (that is, the contents of the page) is transferred to them.
The first implementation of this scheme was the World Wide Web. So, in a sense, by simply opening up a browser on a PC and making a request for a web page, you are making a REST request! More recently, the ideas behind Fielding's original thesis have been greatly expanded such that we now refer to RESTful web services, which can include software (or firmware) running on literally any device connected to the internet, making use of HyperText Transfer Protocol (HTTP).
In order to develop a RESTful web service, it's important to have a basic understanding of how HTTP works. In this section, we will not go into packet-level detail, but we will cover the general concepts you need to know in order to create an effective REST service.
RFC 2616 defines a set of status codes set when the RESTful web service responds to a REST request. The status code gives the client making the request a quick idea of the request's success or failure. The default response in most situations is status code 200, which simply means OK. More details on expected responses depend on the nature of the request (addressed in the next sub-section). Here is a brief summary of the family of status codes:
| Code family | General nature |
| 1xx | Informational |
| 2xx | Success |
| 3xx | Redirect |
| 4xx | Client error |
| 5xx | Server error |
In cases where the RESTful web service you are defining needs to perform a redirect, it's a good idea to set a status code of either 301 (moved permanently) or 307 (temporary redirect), depending on the nature of the redirect.
Use the 4xx family of status codes in situations where the request is bad for some reason. If the requester is not authenticated or authorized, you can set status code 401 (unauthorized) or 403 (forbidden). If the URI a client uses to make the request points to a resource that does not exist, set status code 404 (not found) or 410 (gone). Also, if the request just doesn't make sense (which is often a sign of an attack in progress!), you can set status code 400 (bad request) or 406 (not acceptable).
Finally, the 5xx family is used when there is an error within your RESTful web application. Status code 500 (internal server error) is the most widely used in such situations. If a resource that your RESTful application needs is not available (for example, the MongoDB server is down), you can set a status code of 501 (not implemented) or 503 (service unavailable).
Possibly the most important aspect of HTTP that you need to know is that each HTTP request identifies an HTTP method (also called an HTTP verb). The default method for most REST clients is GET. Another method you might have already encountered is POST, often used with HTML forms. The following table summarizes the methods most often handled by RESTful web services:
| Method | Description/Status code returned |
| GET | Requests information from the application. Think of GET as a database query. In most RESTful web services, if the GET request is accompanied by an ID, only the document matching that ID is returned. Otherwise, all documents are returned. If the request succeeds, typically status code 200 (OK) is returned. If only a subset of the requested information is returned, your RESTful application can set a status code of 206 (partial content). |
| POST | A POST request contains a block of data. The associated RESTful web service typically performs a database insert. Return status code 201 (created) if the insert succeeded. If the operation succeeded but nothing was created, your application can return status code 204 (no content). Otherwise, the default success code 200 (OK) is acceptable. |
| PUT | The outcome of a PUT request depends on whether the identified document already exists. If it exists, PUT ends up performing an update on the existing document. If the document doesn't exist, PUT is normally interpreted as an insert. As with POST, success status codes could be 200, 201, or 204. |
| DELETE | As the name implies, this method requests that the RESTful web application delete the identified document. Success status codes usually default to 200 (OK). 202 (accepted) can be used if the request has been accepted but not yet performed. 204 (no content) is used if the action was successfully performed, but there is no other information to be provided. |
There are a number of other methods used primarily for the purposes of the transmission. These include HEAD, TRACE, OPTIONS, and CONNECT. For more information on these other methods, refer to RFC 2616, Section 9 (https://tools.ietf.org/html/rfc2616#section-9).
To begin our discussion of how to develop a RESTful web application, we need to define a class that responds to REST requests.
Although not mandatory, the common practice is to return a JSON document in response to a REST request. It is worth noting, at this juncture, that many developers are opting to respond with a formalized JSON document structure based on an emerging standard. We will digress briefly to discuss some of the more commonly used standards.
The first question that doubtlessly comes to mind is why bother with standards for JSON? This is a very good question, for which there is a simple answer: if your RESTful application delivers a standards-based response, it's easier for REST clients to handle, easier to maintain, and extends the application's usefulness over time. The next question is: is there an "official" standard for JSON responses? Unfortunately, the answer to the latter question is no! There are, however, emerging standards, the more popular of which we summarize in this section.
JSend (https://github.com/omniti-labs/jsend) is an extremely simple standard that covers three main categories of response: success, fail, and error. Each type further defines mandatory keys. For example, all three types require a status key. Success and fail require a data key. The error type requires a key called message.
Here is an example of a successful response to a GET request:
{
status : "success",
data : {
"posts" : [
{ "id" : "1111AAAA", "productKey" : "1111AAAA", "title" : "Chocolate Bunny" },
{ "id" : "2222BBBB", "productKey" : "2222BBBB", "title" : "Chocolate Donut" }
]
}
}
Next, we will look at JSON:API.
JSON:API (https://jsonapi.org/format/1.1/) has actually been published but has not yet been ratified as of the time of writing. The formatting and available tags are more complex and diverse than with JSend. Reading through the documentation on JSON:API, you can see that the guidelines are much more rigid and precise than JSend.
JSON:API has the following accepted Content-Type header value:
Content-Type: application/vnd.api+json
This means that if the Accept header in a client request specifies this content type, your application is expected to respond with a JSON:API-formatted JSON response. If the Content-Type header of a client request uses the value shown, your REST application needs to understand this formatting.
Top-level keys in a JSON:API-formatted request or response must include at least one of the following: meta, data, or errors. In addition, the jsonapi, links, and included top-level keys are available for use. The resulting document can end up being quite complex. The advantage of this format is that the response becomes self-describing. The client has an idea where the request originated and where to go for related information.
Here is an example:
{
"links": {
"self": "http://sweetscomplete.com/products/AAAA1111"
},
"data": {
"type": "product",
"id": "AAAA1111",
"attributes": {
"title": "Chocolate Bunny"
},
"relationships": {
"category": {
"links": {
"related": "http://sweetscomplet.com/products/category/chocolate"
}
}
}
}
}
In the preceding example, links tells the client the URI used to retrieve this information, data is the information requested, relationships tells the client how to obtain more information of this nature.
Finally, in our brief roundup of emerging JSON standards, we have HAL, which stands for Hypertext Application Language. Although filed as an Internet Engineering Task Force (IETF) Request For Comment (RFC), the last such submission was for version 8, which expired in May, 2016 (https://tools.ietf.org/html/draft-kelly-json-hal-08). As with JSON:API, HAL has its own accepted Content-Type header:
Content-Type: application/hal+json
The proposal is much like JSON:API. In contrast to JSON:API, however, the specification for hal+json has fewer mandatory (that is, required) keys, and more recommended (that is, should include) ones. As with JSON:API, hal+json includes provisions for a self-referencing JSON response, such that the client can retrieve the same information and learn where to go for further information. Furthermore, again as with JSON:API, there is provision for multiple responses and embedded documents within the response.
Here is an example based on the one shown previously in the JSON:API discussion:
{
"_links" : {
"self" : { "href" : "http://sweetscomplete.com/products/AAAA1111" },
"find" : { "href" : "http://sweetscomplete.com/products/category/chocolate" }
},
"type" : "product",
"id" : "AAAA1111",
"title": "Chocolate Bunny"
}
Now, we will turn our attention to the actual responder class.
As with previously defined classes, we will define a responder class, RestResponder, which resides under web.responder.rest. One of our first tasks is to determine the HTTP request method and the content type accepted by the client making the request. We can do this easily by examining the headers in the incoming request.
Our new REST responder activates a strategy that returns the response using the desired JSON formatting. So, before diving into the responder class, let's digress and examine the base class and three strategy classes, which reside under web.responder.strategy.
Please refer to the following links for more information:
All of the response strategy classes return data. We need to decide what else should be returned. Here are the keys we return in the JSON response for the purposes of this discussion:
| Key | Description |
| status | HTTP status code |
| data or error | Data if the operation is successful, and there is data to be returned Error if a problem was encountered |
| link | The original page requested |
In the base class, we need to import the json module and define a few properties needed:
# web.responder.strategy.base
import json
class Base :
headers = []
data = {}
error = ''
status = 200
link = ''
Next, even though HTTP status codes are available in the http.HTTPStatus class (https://docs.python.org/3/library/http.html#http.HTTPStatus), the codes are configured as an enum, which proves awkward to use for our purposes. Accordingly, we define the HTTP status codes we use as a dictionary:
http_status = {
200 : 'OK',
400 : 'BAD REQUEST',
404 : 'NOT FOUND',
500 : 'INTERNAL SERVER ERROR',
501 : 'NOT IMPLEMENTED'
}
Next, we define a constructor method that accepts the data to be transmitted, any error messages, HTTP status code, and a link to this request:
def __init__(self, data, error, status, link) :
self.data = data
self.error = error
self.link = link
if status in self.http_status :
self.status = status
else :
self.status = 400
self.error = self.error
self.error += ', Supported status codes: 200,400,404,500,501'
Finally, we define a method, addHeader(), which simply adds to the list of headers, as with the other responder classes. Also, we add a new method, buildOutput(), called by the render() method implemented by the strategy classes that inherit from the base class. In the buildOutput() method, we set the status code using a separate header status. We also set the content type to that supplied by the strategy.
Thus, for example, HalJsonStrategy would specify application/hal+json. The payload argument includes the original data produced wrapped in the additional keys, as required by the content type:
def addHeader(self, header) :
self.headers.extend([header])
def buildOutput(self, content_type, payload) :
self.addHeader('Status: ' + str(self.status) + ' ' + \
self.http_status[self.status])
self.addHeader('Content-Type: ' + content_type)
output = ''
for item in self.headers :
output += item + "\r\n"
output += "\r\n"
output += json.dumps(payload)
return output
Next, we create strategy classes to match the three JSON formats outlined previously.
All of our strategy classes include a content_type property, set to the content type for that strategy:
# web.responder.strategy.jsend
from web.responder.strategy.base import Base
class JsendStrategy(Base) :
content_type = 'application/json'
The most critical method is render(). If self.error is set, the JSend top-level key, status, is set to error. If self.data is set, on the other hand, the status key changes to success. The data stored in self.data is included in the final payload. The final return value is the product of the buildOutput() method from the base class:
def render(self) :
if self.error :
payload = { 'status' : 'error', 'message' : self.error }
if self.data :
count = 1
post = {}
for key, value in self.data.items() :
post.update({ key : value })
if count == 1 :
payload = { 'status' : 'success',
'link' : self.link,
'data' : { 'post' : post } }
else :
payload = { 'status' : 'success',
'link' : self.link,
'data' : { 'posts' : post } }
return self.buildOutput(self.content_type, payload)
Next, let's look at the JSON:API strategy.
A very similar process occurs within the strategy class representing the JSON:API format:
# web.responder.strategy.json_api
from web.responder.strategy.base import Base
class JsonApiStrategy(Base) :
content_type = 'application/vnd.api+json'
Again, if self.error is set, an error key contains the error message. If there is data in self.data, it is wrapped in the appropriate syntax for this format. The base class buildOutput() method is called to assemble the final response:
def render(self) :
payload = dict({'links':{'self':self.link }})
if self.error :
payload['error'] = self.error
if self.data :
count = 1
data = {}
for key, value in self.data.items() :
if 'category' in value :
prodType = value['category']
else :
prodType = 'N/A'
data.update({count:{'type':prodType,'id':key,\
'attributes':value}})
count += 1
payload['data'] = data
return self.buildOutput(self.content_type, payload)
Next, lets look at the hal+json strategy.
Finally, the HalJsonStrategy class is configured in a similar manner:
# web.responder.strategy.hal_json
from web.responder.strategy.base import Base
class HalJsonStrategy(Base) :
content_type = 'application/hal+json'
You might notice that the render() method for this format is even more concise as data values are directly placed at the top level:
def render(self) :
payload = dict({'_links':{'self':{'href':self.link }}})
if self.error :
payload['error'] = self.error
if self.data :
for key, value in self.data.items() :
payload.update({ key : value })
return self.buildOutput(self.content_type, payload)
Let's now create a REST responder.
Since all the JSON formatting is done by the strategy classes, the job of the responder class becomes much simpler: all this class needs to do is to accept the incoming environment information, data, error message, and HTTP status code. All of this information is passed to the responder by the action script. We start by importing the strategies and initializing the key properties:
# web.responder.rest
from web.responder.strategy.jsend import JsendStrategy
from web.responder.strategy.json_api import JsonApiStrategy
from web.responder.strategy.hal_json import HalJsonStrategy
class RestResponder :
json_strategy = None
header_accept = None
The most important property definition determines what formatting types are accepted. This property is in the form of a dictionary and is used to match the information in the incoming HTTP Accept header (https://tools.ietf.org/html/rfc2616#section-14.1) against the supported MIME types:
accepted_types = {
'json' : { 'mime_type' : 'application/json',
'strategy' : 'JsendStrategy' },
'json_api' : { 'mime_type' : 'application/vnd.api+json',
'strategy' : 'JsonApiStrategy' },
'hal_json' : { 'mime_type' : 'application/hal+json',
'strategy' : 'HalJsonStrategy' }
}
In the constructor, from the supplied environment, we pull out the link and Accept header. If no Accept header is sent by the agent making the request, we default to application/json:
def __init__(self, environ, data, error, status) :
if 'REQUEST_URI' in environ :
link = environ['REQUEST_URI']
if 'HTTP_ACCEPT' in environ :
self.header_accept = environ['HTTP_ACCEPT']
else :
self.header_accept = 'application/json'
We can now determine which strategy class to invoke by simply looping through the list of accepted types, and testing to see whether the mime_type key is present in the Accept header. Note that we default to JSend if the Accept header is not present, or if the contents of this header do not match the types we support:
strategy = 'json'
for key, info in self.accepted_types.items() :
if info['mime_type'] in self.header_accept :
strategy = key
break
if strategy == 'hal_json' :
self.json_strategy = HalJsonStrategy(data, error, status, link)
elif strategy == 'json_api' :
self.json_strategy = JsonApiStrategy(data, error, status, link)
else :
self.json_strategy = JsendStrategy(data, error, status, link)
The render() method of the responder class is trivial. All it needs to do is to return the value of the render() method of the chosen strategy class!
def render(self) :
return self.json_strategy.render()
Now, we can look at what domain services are needed to handle REST requests.
We have mentioned a number of times that the simple web application defined in this chapter follows the action-domain-responder design pattern. Accordingly, the domain service just needs to keep doing the same job it has been doing all along. This class has no awareness of how the request is made, nor is it concerned with the output format. The action script handles the incoming request, and the responder class handles the output.
The domain service we need to deal with is sweetscomplete.domain.product.ProductService. We need to define a method that returns a dictionary of all products. The final result is a dictionary of products with productKey as the key and a partially populated Product entity as the value. The entity needs to include productKey, title, and price:
# sweetscomplete.domain.product.ProductService
def fetchAllKeyCategoryTitlePriceForRest(self, skip = 0, limit = 0) :
projection = dict({"productKey":1,"category":1,\
"title":1,"price":1,"_id":0})
result = dict({})
for doc in self.db.products.find(None, projection, skip, limit) :
result[doc.getKey()] = doc
return result
Likewise, for this illustration, the domain service needs a method that performs a MongoDB findOne() operation, returning a single Product entity instance based on the product key:
def fetchOneKeyCategoryTitlePriceForRest(self, prod_key) :
query = dict({"productKey":prod_key})
projection = dict({"productKey":1,"category":1,\
"title":1,"price":1,"_id":0})
doc = self.db.products.find_one(query, projection)
result = { doc.getKey() : doc }
return result
Finally, we define an action script to intercept the incoming REST request, and make use of the RESTful domain service and responder classes.
REST requests are handled via the web server's CGI gateway by means of a script, /path/to/repo/www/chapter_06/rest.py. Because this is a web script, we need the opening tag that identifies the location of the Python executable to the web server's CGI process. The script also needs to have its permissions set so that it is executable. After the opening tag is the expected series of import statements:
#!/usr/bin/python
import os,sys
src_path = os.path.realpath('../../chapters/06/src')
sys.path.append(src_path)
import cgi
from collections import Counter
from config.config import Config
from web.responder.rest import RestResponder
from db.mongodb.connection import Connection
from sweetscomplete.domain.product import ProductService
from sweetscomplete.entity.product import Product
Next, as with the other action scripts described in this chapter and the previous one, we use the Config class to create a database connection and define a ProductService instance:
config = Config()
db_config = config.getConfig('db')
prod_conn = Connection(db_config['host'], db_config['port'], Product)
prod_service = ProductService(prod_conn, db_config['database'])
We can now define variables that are acquired from the environment and domain service query:
data = None
error = None
link = '/'
status = 200
params = cgi.FieldStorage()
In this part of the action script, we determine the HTTP method. In this example, we will only define processing for a GET request. If the request is anything other than GET, we set the status code to 501, along with an error message. If the request is GET, we then look for a parameter named key in the incoming parameters captured by the Python cgi library.
If a parameter named key is present, we call fetchOneKeyCategoryTitlePriceForRest() from the domain service. Otherwise, we call fetchAllKeyCategoryTitlePriceForRest():
if 'REQUEST_METHOD' in os.environ :
method = os.environ['REQUEST_METHOD']
if method == 'GET' :
if 'key' in params :
data = prod_service.fetchOneKeyCategoryTitlePriceForRest( \
params['key'].value)
else :
data = prod_service.fetchAllKeyCategoryTitlePriceForRest()
If there are no results, we set the status code to 404 (not found) with an error message. Also, if there is no request method at all in the environment (which might mean that the script is called from the command line), we set the status code to 400 (bad request). Note that the default status code is 200 (OK), defined previously.
The block of code shown here shows how to handle situations with no data, an unsupported HTTP method, or a bad request:
if not data :
error = 'No results'
status = 404
else :
error = 'Set HTTP method to GET'
status = 501
else :
error = 'Unable to determine request method'
status = 400
The response is quite simple. We create a RestResponder class instance and pass it the environment, data, error, and status code. We then print the return value of render(), which, as you may recall, is actually the render() method of the chosen strategy:
response = RestResponder(os.environ, data, error, status)
print(response.render())
Here is the output from a REST client (Firefox plugin) where the Accept header indicates JSON:API formatting:

Finally, here is the output when a DELETE request is made:

In this chapter, you were shown implementations that follow the action-domain-responder software design. The first section discussed how pagination can be accomplished by using the skip and limit parameters, used in conjunction with the pymongo.collection.find* methods. The practical application shown was to paginate a customer's purchase history.
You then learned how to service product photos directly out of the MongoDB database using HTML image tags and Base64-encoded data. Next, you learned about jQuery and DataTables, and how to incorporate them into a web application. When configured, the data table makes AJAX requests of your application, which then performs a MongoDB lookup, returning the results as JSON.
Finally, you learned how to configure a web script to accept REST requests that detect the HTTP Accept header, and determine which JSON data format to produce: JSend, JSON:API, or hal+json.
In the next section, you will learn how to design advanced data structures, which includes embedded objects and arrays.
Section 3 introduces a medium-sized corporation, Book Someplace, Inc., with more complex data needs. This section moves out of the basic and into intermediate to advanced territory. You will learn how to define a dataset for a corporate customer with complex requirements that includes document structures with embedded arrays and objects. You will also learn how to handle tasks involving multiple collections, which require taking advantage of advanced MongoDB features, including aggregation and map-reduce.
This section contains the following chapters:
Part three of this book, Digging Deeper, introduces a large corporation called Book Someplace, Inc. that has complex data needs. This part moves us from the basics and clearly into intermediate to advanced territory. In this part, you'll learn how to define a dataset for a corporate customer with complex requirements, including document structures with embedded arrays and objects. You'll also learn how to handle tasks involving multiple collections, which requires taking advantage of advanced MongoDB features, including aggregation and map-reduce.
In this chapter, we'll expand on the design principles we discussed in Chapter 4, Fundamentals of Database Design, which also apply to this company. A major difference, however, is that in this chapter, you'll learn how to refine your data structures by using embedded documents and arrays. In addition, since web application requirements are more complicated, this chapter shows you how to use MongoDB domain service classes inside applications based on the popular Django web application framework.
In this chapter, we will cover the following topics:
Let's get started!
The following are the minimum recommended pieces of hardware for this chapter:
The following are the software requirements for this chapter:
Installation of the required software and how to restore the code repository for this book was explained in Chapter 2, Setting Up MongoDB 4.x. To run the Python code examples in this chapter from the command line, open a Terminal window (Command Prompt) and enter these commands:
cd /path/to/repo
docker-compose build
docker-compose up -d
docker exec -it learn-mongo-server-1 /bin/bash
To run the demo website described in this chapter, follow the setup instructions provided in Chapter 2, Setting Up MongoDB 4.x. Once the Docker container for chapters 3 through 12 has been configured and started, there are two more things you need to do:
172.16.0.11 booksomeplace.local
172.16.0.11 chap07.booksomeplace.local
http://chap07.booksomeplace.local/
When you are finished working with the examples covered in this chapter, return to the Terminal window (Command Prompt) and stop Docker, as follows:
cd /path/to/repo
docker-compose down
The code used in this chapter can be found in this book's GitHub repository at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/chapters/07 as well as https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/www/chapter_07.
The first step in defining database document structures, as discussed in Chapter 4, Fundamentals of Database Design, is to review the customer's requirements. To start, here is a brief overview of the new company we'll be working with, Book Someplace, Inc.
Book Someplace, Inc. is a fictitious web-based company whose core business is to handle hotel bookings for a large international customer base. The company makes its money by taking a small percentage of the payment made for every confirmed booking. In order to differentiate itself from run-of-the-mill online travel agencies, Book Someplace, Inc. offers these unique features:
The stakeholders in the application to be developed fall into categories very similar to that of Sweets Complete:
Now that we have a basic idea of the business and what is needed by the various stakeholders, we'll turn our attention to the data flow.
The following table summarizes the data that is produced by the application:
| Stakeholder | Outputs |
| Customers | The customer's main interest is to view a list of available properties based on location and date. |
| Partners | Partners are most interested in bookings; that is, which room types are booked on what dates. They are also interested in payments that have been processed. |
| Admins | Administrative assistants need to review statistics that might cause them to recommend partner revenue splits to be upgraded or a customer being given bigger booking discounts. |
| Management | Management is interested in financial reports as Book Someplace gets a percentage of every booking with a confirmed payment. In addition, Management needs reports that correlate earnings against customer demographics, and the same for partner demographics. Management can then make decisions about marketing to improve earnings among demographics that might have been overlooked. |
Now that we know what data is produced by the application, let's check what data goes into the application.
The following table summarizes the data that is consumed by the application:
| Stakeholder | Inputs |
| Customers | When customers first sign up as members, they enter their name and address, as well as contact information. Subsequently, as the customer uses the website, they enter booking information, including the property, room type, how many guests, as well as arrival and departure dates. After staying at a property, customers are asked to provide reviews and upload photos. |
| Partners | When a partner signs up, they enter the name, address, and contact information for the primary contact. After being approved, the partner then enters one or more properties they have on offer. Property details include the type of property (for example, hotel, resort, bed and breakfast), location, and photos. For each property, the partner needs to enter a description of the room types, as well as how many of that room type are available. |
| Admins | The administrative assistants handle any payment disputes and communications between the customer and the partner that the customer has a confirmed booking with. Admins also upgrade or downgrade the status of customers based on how often they use the website and how many reviews they provide. Likewise, Admins upgrade or downgrade the statuses of partners based on reviews and the earnings their properties produce for Book Someplace. Admins are also involved in any cancellations and full or partial refunds. |
| Management | Management makes decisions on retaining or removing partners. |
Now that you have an idea of how data flows in and out of the application, it's time to have a look at what needs to be stored in the database.
Based on the information provided previously, we need to more closely define what demographic information is needed for customers and partners. For both, it would make sense to store location information such as the street address, city, state or province, region, country, postal code, and geospatial coordinates. Other demographic information of interest might include gender, age, marital status, and how many children the customer has. It is extremely important that any demographic information is made confidential and optional in order not to violate customer privacy.
Of course, contact information is also needed: first and last name, email address(es), phone number(s), and social media (Facebook, Twitter, and so on). Demographics irrelevant to the needs of the company would include race, religion, and education level.
Information regarding properties would include, for each property, its location (for example, street address, city, and postcode), description, rating, photos, and rooms. Rooms can be grouped into types. How many of each type is extremely critical information that must be stored. Information on a room type should include which floor, view (for example, city view, poolside view, lake view, and river view), size, price, and bed types. Bed types could include single, double, twin, queen-sized, king-sized, and so on. It is important to store how many of each bed type is available for the given room types.
Financial information to be stored would come into play when a customer makes a booking. The number of each room type booked is recorded and payment is arranged. When payment is confirmed, the partner account needs to be credited, and the revenue split between Book Someplace and the partner is calculated.
Now that we have an idea of what information needs to be stored, we can start defining collections and MongoDB document structures.
In the database design for Book Someplace, there are many blocks of information that could potentially be redundant. One example is address information: this block of information can be applied to customers, partners, and also to properties! If we were dealing with a legacy two-dimensional RDBMS, we would end up with dozens of small tables with an overwhelming number of relationships and join tables.
Rather than thinking in terms of collections and document structures, let's step back for a moment and see whether we can define JSON objects that can be used as interchangeable Lego blocks. Let's start with Name.
Although it might not appear evident that a person's name might be defined using objects, reflect on what exactly goes into a person's name: their first, middle, and last names. In addition, a number of people have suffixes added to their names, such as Junior or III. Furthermore, traditional companies might include a title such as Mr., Miss, Mrs., or Ms.
Here is how the name structure appears for Book Someplace:
Name = {
"title" : <formal titles, e.g. Mr, Ms, Dr, etc.>,
"first" : <first name, also referred to as "given" name>,
"middle" : <middle name, optional>,
"last" : <last name, "surname" or "family name">,
"suffix" : <information included after the last name>
}
As in previous chapters, we'll continue with the convention of defining an additional collection called Common that contains small items to facilitate categorization and search. In this case, we add a key called title to the Common collection to ensure uniformity:
Common = [
{ "title" : ['Mr','Ms','Dr',etc.] }
]
Next, we will define a Location structure.
The information needed in a Location structure includes any information that allows us to send regular postal mail. Thus, we need to include fields such as street address, city, state or province, and so forth. This structure also includes geospatial data such as latitude and longitude, allowing us to perform geospatial inquiries to better gauge property distances.
Some addresses, however, also contain a state, province, and other identifiers. The postcode format varies widely from country to country. Other variations exist as well. In the UK, for example, it is not uncommon for the address to include a building name. If a building has apartments, condos, or flats, the floor number and flat number are needed as well.
With all this in mind, here is our first attempt at a JSON Location structure:
Location = {
"streetAddress" : <number and name of street>,
"buildingName" : <name of building>,
"floor" : <number of name of the floor>,
"roomAptCondoFlat": <room/apt/condo/flat number>,
"city" : <city name>,
"stateProvince" : <state or province>,
"locality" : <other local identifying information>,
"country" : <ISO2 country code>,
"postalCode" : <postal code>,
"latitude" : <latitude>,
"longitude" : <longitude>
}
In the preceding structure, we added all the possible fields that might apply to a customer, partner, or property location. Next, we'll look at defining a structure for Contact.
The Contact structure contains information that's most often used to contact the customer or partner. Examples of such information include a phone number and email address. Most people have multiple forms of contact that include additional phone numbers and email addresses, as well as various social media platforms such as LinkedIn, Facebook, and Twitter. Accordingly, it is of interest to create two structures: one for primary contact information and another for additional contact information.
Here is how these structures appear:
Contact = {
"email" : <primary email address>,
"phone" : <primary phone number>,
"socMedia" : <preferred social media contact>,
}
OtherContact = {
"emails" : [email_address2, email_address3, etc.],
"phoneNumbers" : [{<phoneType> : phone2}, etc.],
"socMedias" : [{<socMediaType> : URL2}, etc. ]
}
Common = [
{ "phoneType" : ['home', 'work', 'mobile', 'fax'] },
{ "socMediaType" : ['google','twitter','facebook', etc.] },
]
As we mentioned earlier, we break Contact information down into smaller object structures. Contact is for the primary email address, phone number, and so on. OtherContact is used to store secondary email addresses, phone numbers, and social media. Common, as mentioned previously, contains a set of static identifiers representing what types of phone numbers and what types of social media we plan to store.
Next, we'll have a look at User structures.
Additional structures needed for Book Someplace users include information needed to log in, and other information needed to facilitate demographic reports. Here is an example of two additional structures:
OtherInfo = {
"gender" : <genderType>,
"age" : <int>
}
LoginInfo = {
"username" : <name used to login>,
"oauth2" : <OAUTH2 login credentials>,
"password" : <BCRYPT password>
}
Common = [
{ "genderType" : [{'M' : 'Male'},{'F' : 'Female'},{'X' : 'Other'}] }
]
We provide an oauth2 option in order to allow users to authenticate using Google, Facebook, Twitter, and so on. The password is stored in a secure manner. Another politically sensitive issue is gender, so we add an option; that is, X.
Now, let's turn our attention to defining a Property structure.
A key part of what Book Someplace needs to store is information regarding properties available for booking. A large part of what goes into the property structure has already been defined as Location, which gives us the customer's address and geospatial coordinates. In addition, the property should be identified as a type and needs to define the local currency, description, and photos. Also, the property might pertain to a brand that is part of a larger organization. An example of this is Accor S.A., a French hotel chain that includes well-known subsidiaries such as Ibis, Red Roof Inn, Motel 6, Novotel, Mercure, Sofitel, and Fairmont, among others. Room information is defined as a separate structure.
Here is our first attempt at a structure pertaining to property information:
PropInfo = {
"type" : <propertyType>,
"chain" : <chain>,
"photos" : <stored using GridFS>,
"facilities" : [<facilityType>,<facilityType>,etc.],
"description" : <string>,
"currency" : <currencyType>
},
Customer review information needs to include key areas such as staff attitude and service, cleanliness, and comfort. These can be formulated using a simple 1 to 5 rating system. In addition, we need a place to store information about the positive and negative aspects of the customer's stay. Finally, we include a reference to the customer who made the review, which gives us flexibility for future ad hoc reports. For example, if a property has a large number of negative reviews, Management might decide to reach out to customers to gather more information before downgrading the property status.
Here is how a Review structure might appear:
Review = {
"customerKey" : <string>,
"staff" : <int 1 to 5>,
"cleanliness" : <int 1 to 5>,
"facilities" : <int 1 to 5>,
"comfort" : <int 1 to 5>,
"goodStuff" : <text>,
"badStuff" : <text>
}
Finally, as with other structures, there are corresponding common aspects that can be added to an eventual Common collection. For property information, this could include the following:
Common = [
{ "propertyType" : ['hotel','motel','inn','resort',etc.] },
{ "facilityType" : ['pool','parking','WiFi',etc.] },
{ "chain" : ['Accor','Hyatt','Hilton',etc.] },
{ "currencyType" : ['AUD','CAD','EUR','GBP','INR',etc.] }
]
As we mentioned earlier, of primary interest when defining properties is room; a property without rooms cannot be rented! Accordingly, we will now turn our attention to defining structures to represent a room.
Defining room information is another key aspect of the Book Someplace application. Properties feature a certain number of rooms, each of a certain type. The partner offering the property is responsible for defining what room types are available, and how many of each. Book Someplace then accepts bookings for the property based on room type. At any given moment, the list of room types for a property are marked as either available or booked.
To go even further, each RoomType has a certain number of bed types. Bed types include single, double, queen, king, and so forth. Each room type has one or more bed types. Finally, here is where we define the price. The partner is responsible for providing information about any local government taxes or fees that are imposed. To keep things simple, price information is stored in a single currency. Currency conversion occurs when price information is presented to customers.
Here is our first draft of the RoomType structure:
RoomType = {
"roomTypeKey" : <string>,
"type" : <roomType>,
"view" : <string>,
"description" : <string>,
"beds" : [<bedType>,<bedType>,etc.],
"numAvailable" : <int>,
"numBooked" : <int>,
"roomTax" : <float>,
"price" : <float>
}
Common = [
{ "bedType" : ['single','double','queen','king'] },
{ "roomType" : ['premium','standard','poolside',etc.] }
]
It's important to note that price is in the currency specified in PropInfo.currency. As with other the structures shown earlier, data types other than string, int, float, or boolean are either objects in and of themselves, or are represented by the Common collection. Thus, when a RoomType instance is created, the Common collection is consulted, and the property owner is presented with a list of room types to choose from: premium, standard, poolside, and more.
Now, it's time to have a look at the structure that's used to represent Booking.
Booking information includes information such as the arrival and departure dates, checkout time, the date after which a refund is not possible, as well as reservation and payment status. For simplicity, in this illustration, payments is in the currency associated with the property (see PropInfo.currency). Here is our first draft for BookingInfo:
BookingInfo = {
"arrivalDate" : <yyyy-mm-dd hh:mm>,
"departureDate" : <yyyy-mm-dd hh:mm>,
"checkoutTime" : <hh:mm>,
"refundableUntil" : <yyyy-mm-dd hh:mm>,
"reservationStatus" : <rsvStatus>,
"paymentStatus" : <payStatus>,
}
As with the other structures, certain common elements are defined. Here, we can see common elements associated with booking information:
Common = [
{ "rsvStatus" : ['pending','confirmed','cancelled'] },
{ "payStatus" : ['pending','confirmed','refunded'] }
]
Due to government regulations and security awareness, many hotels must also collect identification information on guests. This information can be collected upon check-in, however, and does not need to be part of the application we are developing, which is focused on bookings only. Now, we'll have a look at a set of intermediary structures needed to make the system work.
When a booking is made, there is no need to store the entire Customer document, nor is there a need to store the entire Property document. Instead, we define intermediary structures that leverage the lower level structures we just described. The first is a set of structures that tie a customer to a booking.
The first in this set of structures defines who is in the customer party. If the customer is the only room occupant, this structure is not used. Otherwise, when a booking is made, an array of CustParty documents is assembled. In the structure shown here, we are leveraging the lower-level structure known as OtherInfo:
CustParty = {
"name" : <Name>,
"other" : <OtherInfo>
}
When the booking is made, we need to store information about the customer making the booking. In the CustBooking document structure shown here, you can see that we are leveraging the previously defined Name, Location, and Contact structures. Also, this structure consumes the CustParty structure shown in the preceding code block:
CustBooking = {
"customerKey" : <string>,
"customerName" : <Name>,
"customerAddr" : <Location>,
"customerContact" : <Contact>,
"custParty" : [<CustParty>,<CustParty>,etc.]
}
Now, let's look at the property booking structures.
When a booking is made, we also need to tie the booking to a property. The PropBooking structure includes the property key (for later reference), as well as Name and Location, both of which were previously defined. Here is the structure:
PropBooking = {
"propertyKey" : <string>,
"propertyName" : <Name>,
"propertyAddr" : <Location>
"propertyContact" : <Contact>
}
In addition, when a booking is made, we need to identify the roomType that was booked, as well as the quantity of that room type the customer wishes to reserve.
By including the price property, we alleviate the need to perform an additional booking when referencing booking information. Note that we include the roomType field, drawn from Common.roomType, as well as the roomTypeKey field, drawn from RoomType.roomTypeKey. The reason why we include both is that roomType is for display, whereas roomTypeKey is used in a link that allows a customer to view more details about that room type. Here is the RoomBooking structure:
RoomBooking = {
"roomType" : <string>,
"roomTypeKey" : <string>,
"price" : <float>,
"qty" : <int>
}
Now, it's time to put all these pieces together. We'll use the basic data structures we defined previously to define document structures for the following collections: Customer, Partner, Property, and Booking. We'll start with Customer.
Each collection features a unique identifying key that's based on an arbitrary algorithm. In this case, the customerKey field includes the first four letters of the first name, the first four letters of the last name, and a phone number. This gives us an alternative way to uniquely identify a customer over and above the autogenerated MongoDB ObjectId property.
In addition, we'll leverage the following previously defined subclasses: Name, Location, Contact, OtherContact, OtherInfo, and LoginInfo. In addition, totalBooked and discount are defined to support the loyalty program. Here is the final collection structure:
Customer = {
"customerKey" : <string>,
"name" : <Name>,
"address" : <Location>,
"contact" : <Contact>,
"otherContact" : <OtherContact>,
"otherInfo" : <OtherInfo>,
"login" : <LoginInfo>,
"totalBooked" : 0,
"discount" : 0
}
Now, let's look at the Partner collection document structure.
The Partner structure is identical to Customer with the exception of the businessName field. It is important to differentiate between Customer and Partner because customers can make bookings whereas partners cannot. If a partner wishes to also make a booking, the partner must first register as a customer!
Another key difference is that management assigns each Partner a revenueSplit, representing a percentage of the total paid. The revenue split is based upon the property ratings, as well as its popularity. As customers make payments on bookings, the acctBalance of the partner increases. When the partner chooses to withdraw from their account, the request is passed to the Accounts Payables department of Book Someplace. When the withdrawal is processed, the account balance is adjusted.
Here is the Partner collection document structure:
Partner = {
"partnerKey" : <string>,
"businessName" : <string>,
"revenueSplit" : <float>,
"acctBalance" : <float>,
"name" : <Name>,
"address" : <Location>,
"contact" : <Contact>,
"otherContact" : <OtherContact>,
"otherInfo" : <OtherInfo>,
"login" : <LoginInfo>
}
Now, let's look at the Property collection document structure.
The Property collection structure actually shares some similarities with Partner in that it has its own contact information, which is most likely not the same as the contact information for the Partner owning the property. Property contact information is made available on the website, whereas customer and partner contact information is reserved for Book Someplace staff usage only.
In addition, each property has a Location and its own unique identifying propertyKey. Also, for reference, the owning partnerKey is provided. Each property document includes a list of RoomType documents, review documents, and statistical information, including rating and totalBooked:
Property = {
"propertyKey" : <string>,
"partnerKey" : <string>,
"propName" : <name of this property>,
"propInfo" : <PropInfo>,
"address" : <Location>,
"contactName" : <Name>,
"contactInfo" : <Contact>,
"rooms" : [<RoomType>,<RoomType>,etc.],
"reviews" : [<Review>,<Review>,etc.],
"rating" : <int>,
"totalBooked" : <int>
}
Now, let's look at the Booking collection document structure.
The Booking collection document structure leverages the intermediary classes discussed earlier in this chapter. It brings together the appropriate information from a customer and a property. The customer then adds information such as arrival and departure dates, people in the party (for example, a spouse and children), as well as which room type and how many of that type. The total price is then calculated based upon room type information, including any local taxes or fees.
Here is the Booking document structure:
Booking = {
"bookingKey" : <string>,
"customer" : <CustBooking>,
"property" : <PropBooking>,
"bookingInfo" : <BookingInfo>,
"rooms" : [<RoomBooking>,<RoomBooking>,etc.],
"totalPrice" : <float>
}
Now, let's look at the Common collection document structure.
The last collection to be detailed here is Common. Each document in this collection has this extremely simple structure:
Common = { "key" : [value] }
The keys and values that populate this collection were discussed earlier. We are now in a position to translate JSON document structures into Python entity classes.
The advantage of using an object-oriented design approach is that the document structures described earlier in this chapter can be simply converted into Python classes. We can then group related classes into modules. The disadvantage of this approach is that we can no longer rely on the Pymongo database driver to automatically populate our classes for us. The burden of producing instances of the final entity classes – Customer, Partner, Property, and Booking – devolves from the domain service classes.
For the purposes of this chapter, we'll focus only on the most complicated entity class: booking. The full implementations of the classes associated with the other collections can be found at /path/to/repo/chapters/07/src/booksomeplace/entity.
The booksomeplace.entity.booking module and its main class, Booking, is the most complicated of all. The reason is that a booking brings together subclasses from both Customer and Property. The class definition is located in a file called /chapters/07/src/booksomeplace/entity/booking.py. We start by defining a subclass called PropBooking to bring together property-related subclasses. In addition, we add propertyName for quick reference and propertyKey for future reference:
# booksomeplace.entity.booking
import os,sys
sys.path.append(os.path.realpath("../../../src"))
from booksomeplace.entity.base import Base, Name, Location, Contact
from booksomeplace.entity.user import OtherInfo
class PropBooking(Base) :
fields = {
'propertyKey' : '',
'propertyName' : '',
'propertyAddr' : Location(),
'propertyContact' : Contact()
}
Next, we define a subclass called CustBooking that allows us to reference customer information. This class, in turn, defines an array of CustParty instances that hold information about how many guests will show up, as well as their ages and genders:
class CustParty(Base) :
fields = {
'name' : Name(),
'other' : OtherInfo()
}
class CustBooking(Base) :
fields = {
'customerKey' : '',
'customerName' : Name(),
'customerAddr' : Location(),
'customerContact' : Contact(),
'custParty' : []
}
The BookingInfo subclass includes information pertinent to the booking itself, including dates, times, and the payment status, as well as the overall status of the reservation:
class BookingInfo(Base) :
fields = {
'arrivalDate' : '',
'departureDate' : '',
'checkoutTime' : '',
'refundableUntil' : '',
'reservationStatus' : '',
'paymentStatus' : '',
}
Although this has a small number of fields, the RoomBooking subclass brings together room types included in this booking, as well as the quantity of each type:
class RoomBooking(Base) :
fields = {
'roomType' : '',
'roomTypeKey' : '',
'price' : 0.00,
'qty' : 0
}
The main class is Booking, which consumes the subclasses described earlier. Its getKey() method returns the value of the bookingKey property. The rooms property is an array of RoomBooking instances. totalPrice is calculated from the quantity and price values:
class Booking(Base) :
fields = {
'bookingKey' : '',
'customer' : CustBooking(),
'property' : PropBooking(),
'bookingInfo' : BookingInfo(),
'rooms' : [],
'totalPrice' : 0.00
}
def getKey(self) :
return self['bookingKey']
And this concludes the code needed to represent a Booking. The other pertinent classes include the following:
Now, let's define the Common module.
As we mentioned in earlier chapters, the Common module and class are used to hold small bits of information used throughout the application. These primarily take the form of choices in forms, including drop-down select elements. They are also used to reduce duplicate definitions, as well as present a way of aggregating information when queries on properties are performed.
Another advantage of keeping such items in a common collection is to facilitate translation into other languages. Accordingly, here is the actual structure of the Common class:
# booksomeplace.entity.common
import os,sys
sys.path.append(os.path.realpath("../../../src"))
from booksomeplace.entity.base import Base
class Common(Base) :
fields = {
'key' : '',
'value' : []
}
The actual keys and values were defined earlier in this chapter when we discussed how to define document structures.
Now that all the entity classes and dependent subclasses have been defined, the next question that might arise is, how can we test such structures? The answer, of course, lies in developing an appropriate unit test. As in earlier chapters, we created a directory called /path/to/repo/chapters/07/test/booksomeplace/entity that we could define the test scripts in. It would take far too much space to delineate tests for all the classes discussed in this chapter, so we will only focus on developing a test for the booksomeplace.entity.user.Customer class.
As expected, we'll begin the test script with the appropriate import statements. Note that in order to perform imports from the classes we wish to test, we need to append /repo/chapters/07/src to the system path:
# sweetscomplete.entity.user.test_entity_customer
import os,sys
sys.path.append(os.path.realpath("/repo/chapters/07/src"))
import json
import unittest
from booksomeplace.entity.base import Name, Contact, Location
from booksomeplace.entity.user import Customer, OtherContact, OtherInfo, LoginInfo
We then define the class and two properties that represent a Customer entity derived from a set of subclasses, and another that is derived from the defaults:
class TestCustomer(unittest.TestCase) :
customerFromDict = None
customerDefaults = None
We then start the test data for the various subclasses that are consumed by Customer, beginning with testDict:
testDict = {
'customerKey' : '00000000',
'name' : Name(),
'address' : Location(),
'contact' : Contact(),
'otherContact' : OtherContact(),
'otherInfo' : OtherInfo(),
'login' : LoginInfo(),
'totalBooked' : 100,
'discount' : 0.05
}
We then add Location:
testLocation = {
'streetAddress' : '123 Main Street',
'buildingName' : '',
'floor' : '',
'roomAptCondoFlat' : '',
'city' : 'Bedrock',
'stateProvince' : 'ZZ',
'locality' : 'Unknown',
'country' : 'None',
'postalCode' : '00000',
'latitude' : '+111.111',
'longitude' : '-111.111'
}
This is followed by the subclasses that define the name and contact information for a customer:
testName = {
'title' : 'Mr',
'first' : 'Fred',
'middle' : 'Folsom',
'last' : 'Flintstone',
'suffix' : 'CM'
}
testContact = {
'email' : 'fred@slate.gravel.com',
'phone' : '+0-000-000-0000',
'socMedia' : {'google' : 'freddy@gmail.com'}
}
We then define the test data for the remaining subclasses that represent other contact, login, and demographic information:
testOtherContact = {
'emails' : ['betty@flintstone.com', 'freddy@flintstone.com'],
'phoneNumbers' : [{'home' : '000-000-0000'}, \
{'work' : '111-111-1111'}],
'socMedias' : [{'google' : 'freddy@gmail.com'}, \
{'skype' : 'fflintstone'}]
}
testOtherInfo = {
'gender' : 'M',
'dateOfBirth' : '0000-01-01'
}
testLoginInfo = {
'username' : 'fred',
'oauth2' : 'freddy@gmail.com',
'password' : 'abcdefghijklmnopqrstuvwxyz'
}
This is followed by exactly the same test data but rendered as a JSON string. We're only reproducing part of the structure here to conserve space. The three dots represent information that has been removed:
testJson = '''{
"customerKey" : "00000000",
"address" : { ... },
"name" : { ... },
"contact" : { ... },
"otherContact" : { ... },
"otherInfo" : { ... },
"login" : { ... },
"totalBooked" : 100,
"discount" : 0.05
}'''
The unit test setUp() method creates two instances of Customer – one populated with test data and another that's blank:
def setUp(self) :
self.testDict['name'] = Name(self.testName)
self.testDict['address'] = Location(self.testLocation)
self.testDict['contact'] = Contact(self.testContact)
self.testDict['otherContact'] = OtherContact(self.testOtherContact)
self.testDict['otherInfo'] = OtherInfo(self.testOtherInfo)
self.testDict['login'] = LoginInfo(self.testLoginInfo)
self.customerFromDict = Customer(self.testDict)
self.customerDefaults = Customer()
self.maxDiff = None
The first test asserts that the getKey() method returns the pre-programmed value:
def test_customer_key(self) :
expected = '00000000'
actual = self.customerFromDict.getKey()
self.assertEqual(expected, actual)
We then write a test that extracts the customer's last name. As you may recall, the name property is actually an instance of the Name subclass. Thus, we need to call the get() method twice – once on the customerFromDict dictionary and then again on the extracted Name instance:
def test_customer_from_dict(self) :
expected = 'Flintstone'
name = self.customerFromDict.get('name')
actual = name.get('last')
self.assertEqual(expected, actual)
We then add a test that ensures that the empty Customer instance behaves as expected:
def test_customer_from_blank(self) :
expected = True
actual = isinstance(self.customerDefaults.get('address'), Location)
self.assertEqual(expected, actual)
We then test the toJson() method to make sure it matches what is expected:
def test_customer_from_dict_to_json(self) :
expected = json.loads(self.testJson)
actual = json.loads(self.customerFromDict.toJson())
self.assertEqual(expected, actual)
The last few lines of the test script ensure we can run it from the command line:
def main() :
unittest.main()
if __name__ == "__main__":
main()
And here are the results:

Now, let's look at defining service classes.
Now that the entity classes have been defined, it's time to turn our attention to the domain service classes. As we mentioned earlier in this book, in conjunction with the Sweets Complete web application, domain service classes allow us to connect to the database. They contain methods that are useful for adding, editing, deleting, or querying the various MongoDB collections that have been defined.
Accordingly, as we have determined four primary collections for the Book Someplace web application, we will define four domain service classes. For illustration purposes, we'll discuss how to define a domain service class associated with the Customers collection.
The first class that needs to be defined is one that other domain service classes inherit from. The structure of this class is similar to the one shown earlier in this book. In this case, however, instead of requiring the calling program to define a MongoDB connection and supplying the connection to the constructor, we'll define a constructor that creates its own database and collection from the config.config.Config class described earlier in this book.
We'll start by defining the necessary import and class properties:
# booksomeplace.domain.base
from db.mongodb.connection import Connection
class Base :
db = None
collection = None
dbName = 'booksomeplace'
collectName = 'common'
As mentioned previously, the __init__() method creates an internal Connection, from which we can define database and collection properties:
def __init__(self, config) :
if config.getConfig('db') :
conn = Connection(config.getConfig('db'))
else :
conn = Connection()
self.db = conn.getDatabase(self.dbName)
self.collection = conn.getCollection(self.collectName)
Next, we'll define a domain service that represents properties.
As with other domain service classes described earlier in this book, we'll create a folder called /path/to/repo/chapters/07/src/booksomeplace/domain. The filename is property.py, while the class is PropertyService, which extends Base:
# booksomeplace.domain.property
import pymongo
from pymongo.cursor import CursorType
from booksomeplace.domain.base import Base
from booksomeplace.entity.base import Name, Contact, Location
from booksomeplace.entity.property import Property, PropInfo, Review, RoomType
class PropertyService(Base) :
collectName = 'properties'
Next, we define a method called fetchByKey() that returns a single Property instance based on key lookup:
def fetchByKey(self, key) :
query = {'propertyKey':key}
result = self.collection.find_one(query)
if result :
return self.assemble(result)
else :
return None
After that, we define a method called fetchTop10() that returns a list of the top 10 properties based upon rating and the number of bookings. This method emulates the following mongo shell query:
db.properties.find({"rating":{"$gt":4}},{}).\
sort({"totalBooked":-1})\.limit(10);
This method is ultimately called by a Django web application and displays the main booking page:
def fetchTop10(self) :
query = {'rating':{'$gt':4}}
projection = None
sortDef = [('totalBooked', pymongo.DESCENDING)]
skip = 0
limit = 10
result = self.collection.find(query, projection, \
skip, limit, False, CursorType.NON_TAILABLE, sortDef)
properties = []
if result :
for doc in result :
properties.append(self.assemble(doc))
return properties
Finally, and perhaps most importantly, we need to define a method called assemble(). It accepts a dictionary argument and assembles a Property instance using the subclasses discussed earlier in this chapter. As shown in the following code, this method simply checks for the presence of specific keys and creates an instance of the appropriate subclass:
def assemble(self, doc) :
prop = Property(doc)
if 'propInfo' in doc : prop['propInfo'] = PropInfo(doc['propInfo'])
if 'address' in doc : prop['address'] = Location(doc['address'])
if 'contactName' in doc : prop['contactName'] = Name(doc['contactName'])
if 'contactInfo' in doc : prop['contactInfo'] = Contact(doc['contactInfo'])
if 'rooms' in doc :
prop['rooms'] = []
for room in doc['rooms'] :
prop['rooms'].append(RoomType(room))
if 'reviews' in doc :
prop['reviews'] = []
for review in doc['reviews'] :
prop['reviews'].append(Review(room))
return prop
In the next section, we'll discuss how to use the MongoDB-related classes inside a standard Django web application.
Most Python developers, at some point, use a framework to facilitate rapid code development. Determining which Python web framework is the first task to be performed. As you can tell from the title of this section, we have settled on Django. However, the methodology used to make this determination is what we're interested in here.
Most code libraries now use GitHub to host a repository for their source code. Searching GitHub for statistics on various popular Python web frameworks reveals the statistics (at the time of writing) summarized in the following table:
| Framework | Used | Watch | Star | Fork | Contributors | Last Commit |
| django | 406,000 | 2,200 | 49,200 | 21,300 | 1,883 | -3 |
| flask | 443,000 | 2,300 | 50,300 | 13,500 | 575 | -25 |
| pyramid | -- | 176 | 3,400 | 867 | 266 | -3 |
| cherrypy | 5,300 | 66 | 1,200 | 289 | 105 | -12 |
Here is a brief explanation of what these statistics mean:
Accordingly, although a case could be made for choosing flask, the one chosen for this illustration is django as there are more than three times the number of contributors, and it appears the updates are more frequent.
The next logical step would seem to be to choose a MongoDB integration library. This would allow us to use all the native Django database access libraries. As with many frameworks, however, Django is oriented toward legacy, two-dimensional relational databases. For the most part, the MongoDB integration libraries ultimately simply translate legacy SQL statements into a MongoDB equivalent. This is not at all desirable if your objective is to leverage the speed and power of MongoDB. Accordingly, for the purposes of what we're trying to illustrate, we will not use the native Django database libraries and instead rely on the custom Python modules discussed here.
In this book, we will not cover how to install Django. For those of you who are new to Django, the best way to install Django is to follow the official installation documentation. For the purposes of this book, you should follow the installation guidelines documented in the Django pip installation documentation. In addition, we are using an Apache 2.4 web server.
For development purposes, you can simply use the Python built-in web server. In fact, after creating a Django project, a script is provided for that purpose. Simply change to the directory containing your Django project and run the following command:
python manage.py runserver <port>
This is not suitable for a production site, however. Accordingly, for the purposes of this book, we have configured the Apache web server installed in the demonstration Docker container so that it uses mod_wsgi to serve files from the Django project. You can find the Django documentation on the installation and configuration of mod_wsgi here: https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/modwsgi/#how-to-use-django-with-apache-and-mod-wsgi
In order to contain the required directives in a single location, we created a separate configuration file called booksomeplace.local.conf that defines an Apache virtual host that matches our hosts file entry, as discussed in the previous subsection. This file needs to be included in the primary Apache configuration file, which is either apache2.conf or httpd.conf, depending on the operating system and/or Linux distribution.
The first web server configuration directive loads the wsgi module. Please note that XX reflects the version of Python (for example, 37), while YY reflects the installation architecture (for example, x86_64):
LoadModule wsgi_module "/path/to/mod_wsgi-pyXX.cpython-XXm-YY-linux-gnu.so"
The document root for the virtual host is the main Django project directory, which, in this case, is under /path/to/repo/www/chapter_07/booksomeplace_dj in this book's repository. Note that we also need to define a Directory block that grants web server permissions to browse this directory:
<VirtualHost *:80>
ServerName booksomeplace.local
DocumentRoot "/repo/www/chapter_07/booksomeplace_dj"
<Directory "/repo/www/chapter_07/booksomeplace_dj">
AllowOverride All
Require all granted
</Directory>
Next, still within the virtual host definition, we'll include three mod_wsgi directives. The first, WSGIScriptAlias, maps all requests to this virtual host to the Django wsgi.py file for this project. We also include a WSGIDaemonProcess directive that tells mod_wsgi where Python is located, as well as where the main module for the project is located. In addition, we use this directive to indicate the user and group to use at runtime. Finally, we use WSGIProcessGroup to tie all the relevant WSGI directives together. In this case, of course, there is only one other directive that this applies to – WSGIDaemonProcess:
WSGIScriptAlias / /repo/www/chapter_07/booksomeplace_dj/wsgi.py
WSGIDaemonProcess booksomeplace python-home="/usr" \
user=www-data group=www-data \
python-path="/repo/www/chapter_07"
WSGIProcessGroup booksomeplace
Finally, we close the Directory block with an Alias directive, which allows us to serve static files out of a common directory called assets. This directory contains the CSS, JavaScript, and images needed for the website. We added another Directory directive that gives Apache the rights to browse through the assets directory:
Alias "/static" "/repo/www/chapter_07/assets"
<Directory "/repo/www/chapter_07/assets">
Require all granted
</Directory>
</VirtualHost>
The Django project for Book Someplace is located in the /path/to/repo/www/chapter_07 directory structure. The main project is booksomeplace_dj. This is where wsgi.py, the initial point of entry, is located. At this point, we have defined two Django apps: home and booking. The directory structure for booksomeplace_dj is shown on the left in the following diagram. In the middle and to the right of the diagram are the identical directory structures for the home and booking Django apps:

Modifications that are needed over and above the defaults will be discussed in the next section.
In order to provide mappings to the two URLs currently defined, modifications were made to the main project, booksomeplace_dj, and the two apps, home and booking. First of all, a urls.py file was created and placed in the directory for each of the two apps. The contents of the files are the same, as shown here:
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]
Also, in the main project directory, /path/to/repo/www/chapter_07/booksomeplace_dj, the urls.py file was modified to provide mappings to the two apps, as shown here:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('booking/', include('booking.urls')),
path('', include('home.urls')),
]
Still in the directory for booksomeplace_dj, another file called settings.py modified the ALLOWED_HOSTS constant, adding booksomeplace.local as a host. The BASE_DIR constant ends up pointing to /path/to/repo/www. The SRC_DIR constant is used to indicate the location of the source code, other than that associated with Django:
ALLOWED_HOSTS = ['booksomeplace.local']
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SRC_DIR = os.path.realpath(BASE_DIR + '/../../chapters/07/src')
We disable the Django cache by setting the built-in cache to DummyCache. This is only for development purposes:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}
In the same file, the TEMPLATES constant tells Django where to find HTML templates:
TEMPLATE_DIR = os.path.realpath(BASE_DIR + '/templates')
TEMPLATES = [ {
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [ TEMPLATE_DIR ],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
And finally, we set the STATIC_URL constant, which then rewrites template requests for images, CSS, and JavaScript by prepending the /static/ path. The Apache Alias directive (discussed earlier) maps such requests to the assets directory:
STATIC_URL = '/static/'
Now that the project modifications have been covered, let's have a look at the output: defining the view module.
Finally, to present the view for bookings, we modify /path/to/repo/www/chapter_07/booking/view.py. In this file, we create an instance of the PropertyService class described previously. We then call the fetchTop10() method and pass the resulting MongoDB\Cursor instance to the template associated with this app:
from django.template.loader import render_to_string
from django.http import HttpResponse
from config.config import Config
from booksomeplace.domain.property import PropertyService
def index(request):
config = Config()
propService = PropertyService(config)
main = render_to_string('booking.html', { 'top_10' : propService.fetchTop10() })
page = render_to_string('layout.html', {'contents' : main})
return HttpResponse(page)
Now that you have an understanding of what goes into the view class, its time to have a look at the actual view template.
As mentioned in the configuration, all view templates are located in the /path/to/repo/www/chapter_07/templates directory. We modify the booking.html template, iterating through the results of fetchTop10(). Note that we are not showing the entire template here. In the template, we define a for loop that displays only the property name, and 10 words from the description:
<h3>Top Ten</h3>
<ul>
{% for property in top_10 %}
<li>
<b>{{ property.propName }}</b>
<br>{{ property.propInfo.description|truncatewords:"10" }}
</li>
{% endfor %}
</ul>
Here are the results provided by the browser when using the http://booksomeplace.local/booking/ URL:

As you can see, the name and abbreviated description of the most popular properties are listed.
In this chapter, you learned how to review the requirements of an online booking agency with complex needs. You then learned that, due to the object-oriented nature of MongoDB, the document structure design process is completely free of the constraints imposed by legacy two-dimensional RDBMS tables. Accordingly, you can really dig deep into customer requirements and design subclasses from a very low level. These reusable data structures can then be group together like building blocks to form complex data structures. Once defined, you learned how easy it is to translate these structures into Python entity modules and classes.
You then learned how to define domain service classes that consume a MongoDB client and contain key methods needed by the web application. Finally, you learned how to modify a basic Django application so that it uses mod_wsgi under Apache. You were then shown how to incorporate the property domain service class to display a list of the top 10 properties.
In the next chapter, you'll learn how to work with the complex document structures defined in this chapter, especially embedded objects and arrays.
This chapter focuses on how to work with MongoDB documents that contain embedded lists (arrays) and objects. As you learned in the previous chapter, Chapter 7, Advanced MongoDB Database Design, the complex needs of Book Someplace, Inc. resulted in a database document structure built around a series of embedded documents. In addition, certain fields are stored as arrays. To further complicate application development, some fields consist of a list of objects! An example of this is the list of reviews that is associated with each property. In this chapter, you will learn how to handle create, read, update, and delete operations that involve embedded lists and objects. You will also learn how to perform queries involving embedded objects and lists.
This chapter covers the following topics:
The minimum recommended hardware is as follows:
The software requirements are as follows:
The installation of the required software and how to restore the code repository for the book is explained in Chapter 2, Setting Up MongoDB 4.x. To run the code examples in this chapter, open a terminal window (Command Prompt) and enter these commands:
cd /path/to/repo
docker-compose build
docker-compose up -d
docker exec -it learn-mongo-server-1 /bin/bash
To run the demo website described in this chapter, once the Docker container for Chapters 3 through 12 has been configured and started, there are two more things you need to do:
172.16.0.11 chap08.booksomeplace.local
http://chap08.booksomeplace.local/
When you are finished working with the examples covered in this chapter, return to the terminal window and stop Docker as follows:
cd /path/to/repo
docker-compose down
The code used in this chapter can be found in the book's GitHub repository at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/chapters/08 and https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/www/chapter_08.
Before we get into the technical details of the implementations described in this chapter, it's necessary to provide an overall view of what we plan to achieve. Django provides an object-to-form mapping by way of its ModelForm class. However, in order to use this class we would need to create Django Models, which correspond to the entity classes we have described to this point.
The problem with using Django Models and ModelForm classes is that we end up being tied to SQL and the RDBMS way of thinking, which imposes incredible constraints on a MongoDB implementation, and in effect defeats the purpose of using MongoDB in the first place. Such an approach introduces a massive amount of overhead, with additional functionality that is never used. Instead, our proposed solution for adding data to MongoDB documents with embedded objects and arrays is to do the following:
In the case of editing documents, the reverse operation is performed:
With this solution, we adhere to a strict object-oriented approach from beginning to end, which ensures flexibility, and the ability to perform adequate testing. The following diagram visually captures the flow:

In this chapter, we define four tasks that are critical to the day-to-day operations of Book Someplace: adding a new property, adding room types, updating property contact information, and removing a property listing. The corresponding skills these tasks require include inserting a document with embedded objects and lists, adding to an embedded list, updating an embedded object, and deleting a document.
Let's start by examining adding a new property, which involves inserting a document with embedded objects and arrays.
In this scenario, a partner has logged into the website and wants to add a new property. As you recall from our discussion on the data structure for Book Someplace, documents in the properties collection have the following data structure:
Property = {
"propertyKey" : <string>,
"partnerKey" : <string>,
"propName" : <name of this property>,
"propInfo" : <PropInfo>,
"address" : <Location>,
"contactName" : <Name>,
"contactInfo" : <Contact>,
"rooms" : [<RoomType>,<RoomType>,etc.],
"reviews" : [<Review>,<Review>,etc.],
"rating" : <int>,
"totalBooked" : <int>
}
You might note that each Property documents includes embedded object classes, all under booksomeplace.entity.*:, such as PropInfo, Location, Name, and Contact. In addition, there are two lists: rooms, a list of RoomType objects, and reviews, a list of Review objects. Because the property is new, we do not allow a partner to enter information pertaining to reviews, rating, nor totalBooked. These are ultimately generated by the customers who book the property. Further, the propertyKey and partnerKey fields are generated by the application, which leaves only basic property information and room types to be defined by the partner.
Continuing with our focus on the object-oriented approach, the next logical step is to define Django Form classes to match each subclass representing a single property. For this purpose, we create a new Django app called property. In that app we define property-related forms in the forms.py module, located in /path/to/repo/www/chapter_08/property.
As always, we start with a set of import statements:
from django import forms
from django.core import validators
from django.core.validators import RegexValidator, EmailValidator
import os,sys
from config.config import Config
from booksomeplace.entity.base import Name, Location, Contact
from booksomeplace.entity.property import Property, PropInfo, RoomType
from booksomeplace.domain.partner import PartnerService
from booksomeplace.domain.property import PropertyService
from booksomeplace.domain.common import CommonService
Next, we define a NameForm class that works with booksomeplace.entity.base.Name. In this class, we first create an instance of booksomeplace.domain.common.CommonService in order to retrieve values for titles (Mr, Ms, and so on). You see that we also implement RegexValidator for first and last names:
class NameForm(forms.Form) :
config = Config()
commonSvc = CommonService(config)
titles = [(item, item) for item in commonSvc.fetchByKey('title')]
alphaOnly = RegexValidator(r'^[a-zA-Z]*$', 'Only letters allowed.')
name_title = forms.ChoiceField(label='Title',choices=titles)
name_first = forms.CharField(label='First Name', \
validators=[alphaOnly])
name_middle= forms.CharField(label='M.I.',required=False)
name_last = forms.CharField(label='Last Name',validators=[alphaOnly])
name_suffix= forms.CharField(label='Suffix',required=False)
In a similar manner, we define a PropInfoForm form class to capture property information. The drop-down lists needed include property types, facilities, and currencies. These are drawn from the common collection:
class PropInfoForm(forms.Form) :
config = Config()
commonSvc = CommonService(config)
facTypes = [(item, '... ' + item) for item in \
commonSvc.fetchByKey('facilityType')]
propTypes = [(item, item) for item in \
commonSvc.fetchByKey('propertyType')]
currTypes = [(item, item) for item in commonSvc.fetchByKey('currency')]
alnumPunc = RegexValidator(r'^[\w\.\-\,\"\' ]+$',
'Only letters, numbers, spaces and punctuation are allowed.')
This class must also define fields associated with the document structure for the properties collection:
info_type = forms.ChoiceField(label='Property Type', \
choices=propTypes)
info_chain = forms.CharField(label='Chain', \
validators=[validators.validate_slug],required=False)
info_photos = forms.URLField(label='Photo URL',required=False)
info_other_fac = forms.CharField(label='Other Facilities', \
required=False)
info_currency = forms.ChoiceField(label='Currency',choices=currTypes)
info_taxFee = forms.FloatField(label='Taxes/Fees')
info_description= forms.CharField(
label='Description',validators=[alnumPunc],
widget=forms.Textarea(attrs={'rows' : 4, 'cols' : 80}))
Of special interest is how the property facilities are treated. These are displayed as a row of check boxes in the final form. However, we need to account for the fact that more than one can be marked, thus we need the Django MultipleChoiceField form class and the CheckboxSelectMultiple widget. When the form is submitted, this field, as it is a list, needs additional processing, described later in this section:
info_facilities = forms.MultipleChoiceField(
label='Facilities',choices=facTypes,
widget=forms.CheckboxSelectMultiple( \
attrs={'class': 'check_horizontal'}))
As you would expect, the form that corresponds to the Location subclass is uneventful, as shown here:
class LocationForm(forms.Form) :
iso2 = RegexValidator(r'^[A-Z]{2}$', \
'Country code must be a valid ISO2 code (2 letters UPPERCASE).')
location_streetAddress = forms.CharField(label ='Street Address')
location_buildingName = forms.CharField(label ='Building Name',\
required=False)
location_floor = forms.CharField(label ='Floor',required=False)
location_roomAptCondoFlat = forms.CharField(label ='Room/Condo', \
required=False)
location_city = forms.CharField(label ='City')
location_stateProvince = forms.CharField(label ='State/Province')
location_locality = forms.CharField(label ='Locality', \
required=False)
location_country = forms.CharField(label ='Country',
validators=[iso2])
location_postalCode = forms.CharField(label ='Postal Code')
location_latitude = forms.CharField(label ='Latitude')
location_longitude = forms.CharField(label ='Longitude')
Likewise, the form to capture contact information is very straightforward, as you can see here:
class ContactForm(forms.Form) :
config = Config()
commonSvc = CommonService(config)
socTypes = [(item, item) for item in \
commonSvc.fetchByKey('socMediaType')]
validEmail = EmailValidator(message="Must be a valid email address")
contact_email = forms.EmailField(label ='Email', \
validators=[validEmail])
contact_phone = forms.CharField(label='Phone')
contact_soc_type = forms.ChoiceField(label='Social Media', \
choices=socTypes,required=False)
contact_soc_addr = forms.CharField(label='Social Media Identity',
required=False)
The property form itself is an anti-climax. It only needs to capture the partner key and property name:
class PropertyForm(forms.Form) :
config = Config()
partSvc = PartnerService(config)
partKeys = partSvc.fetchPartnerKeys()
alnumPunc = RegexValidator(r'^[\w\.\-\,\"\' ]+$', \
'Only letters, numbers, spaces and punctuation are allowed.')
prop_partnerKey = forms.ChoiceField(label='Partner',choices=partKeys)
prop_propName = forms.CharField(label='Property Name', \
validators=[alnumPunc])
Let's now define templates to match sub-forms in the next section.
To foster re-usability, each sub-form is associated with its own template. As an example, the Name subclass is associated with NameForm, which in turn is rendered using the property_name.html template, shown here:
{% load static %}
<h3>Contact Name</h3>
<div class="row">
<div class="col-md-4">{{ form.name_title.label_tag }}</div>
<div class="col-md-4">{{ form.name_title }}</div>
<div class="col-md-4">{{ form.name_title.errors }}</div>
</div>
<div class="row">
<div class="col-md-4">{{ form.name_first.label_tag }}</div>
<div class="col-md-4">{{ form.name_first }}</div>
<div class="col-md-4">{{ form.name_first.errors }}</div>
</div>
<!-- other fields not shown -->
Each sub-form has its own template. When all sub-forms are rendered, the resulting HTML is then injected into a master form template, property.html, a portion of which is shown here:
<form action="/property/edit/" method="post">
{% csrf_token %}
<h1>Add/Edit Property</h1>
Name of Property: {{ prop_name }}
<div class="row extra" style="background-color: #E6E6FA;">
<div class="col-md-6">{{ name_form_html }}</div>
<div class="col-md-6" style="background-color: #D6D6F0;">
{{ contact_form_html }}
</div><!-- end col -->
</div><!-- end row -->
<div class="row extra" style="background-color: #E5E5E5;">
<div class="col-md-8">{{ info_form_html }}</div>
<div class="col-md-4"> </div>
</div><!-- end row -->
<input type="submit" value="Submit">
</form>
{{ message }}
We will define a FormService class in the next section.
As we need a way to manage all the sub-forms and subclasses, the most expedient approach is to develop a FormService class. We start by defining a property to contain form instances, and also a list of fields:
class FormService() :
forms = {}
list_fields = ['info_facilities','room_type_beds']
The constructor updates the internal forms property with instances of each sub-form:
def __init__(self) :
self.forms.update({ 'prop' : PropertyForm() })
self.forms.update({ 'name' : NameForm() })
self.forms.update({ 'info' : PropInfoForm() })
self.forms.update({ 'contact' : ContactForm() })
self.forms.update({ 'location' : LocationForm() })
It's also necessary to define a method that retrieves a specific sub-form:
def getForm(self, key) :
if key in self.forms :
return self.forms[key]
else :
return None
We also define a hydrateFormsFromPost() method that populates sub-forms with data coming from request.POST:
def hydrateFormsFromPost(self, data) :
self.forms['prop'] = PropertyForm(self.getDataByKey('prop', data))
self.forms['name'] = NameForm(self.getDataByKey('name', data))
self.forms['info'] = PropInfoForm(self.getDataByKey('info', data))
self.forms['contact'] = ContactForm( \
self.getDataByKey('contact', data))
self.forms['location'] = LocationForm( \
self.getDataByKey('location', data))
However, since the form posting data, captured in the Django request object, is returned as a single list, we need a way to filter out fields specific to each sub-form. This is performed by the getDataByKey() method, which simply scans the post data for fields with a prefix that matches the sub-form key:
def getDataByKey(self, key, data) :
prefix = key + '_'
formData = {}
for field, value in data.items() :
if field[0:len(prefix)] == prefix :
if field in self.list_fields :
value = data.getlist(field)
formData.update({field : value})
return formData
We also need a method to validate all sub-forms. This is accomplished by the is_valid() method:
def validateForms(self) :
count = 0
valid = 0
for key, sub_form in self.forms.items() :
count += 1
if sub_form.is_valid() : valid += 1
return count == valid
Post validation, we define a method that extracts clean data into a dictionary and ultimately populates the corresponding subclass:
def getCleanDataByKey(self, key) :
prefix = key + '_'
start = len(prefix)
cleanData = {}
for formField, value in self.forms[key].cleaned_data.items() :
docField = formField[start:]
cleanData.update({docField : value})
return cleanData
We also need a method to hydrate sub-forms from a Property instance. For that purpose, we define a method called hydrateFormsFromEntity():
def hydrateFormsFromEntity(self, prop) :
# hydrate Property form
formData = {}
formData.update({'prop_partnerKey' : prop.get('partnerKey')})
formData.update({'prop_propName' : prop.get('propName')})
self.forms['prop'] = PropertyForm(formData)
# hydrate NameForm
formData = {}
subclass = prop.get('contactName')
for field, value in subclass :
formData.update({'name_' + field : value})
self.forms['name'] = NameForm(formData)
# other sub-forms are hydrated in a manner identical to NameForm
Finally, the getPropertyFromForms() method creates a Property entity instance that can then be used by our application:
def getPropertyFromForms(self) :
prop_form = self.forms['prop']
data = {
'partnerKey' : prop_form.cleaned_data.get('prop_partnerKey'),
'propName' : prop_form.cleaned_data.get('prop_propName'),
'propInfo' : PropInfo(self.getCleanDataByKey('info')),
'address' : Location(self.getCleanDataByKey('location')),
'contactName' : Name(self.getCleanDataByKey('name')),
'contactInfo' : Contact(self.getCleanDataByKey('contact')),
}
return Property(data)
Next, let's define the view logic to handle form rendering and posting.
Within the Django property app, let's turn our attention to views.py. At the top of the module, the classes we need are imported:
from django.http import HttpResponse, HttpResponseRedirect
from django.template.loader import render_to_string
from .forms import FormService, PropertyForm, NameForm,
PropInfoForm, LocationForm, ContactForm, RoomTypeForm
import os,sys
from config.config import Config
from booksomeplace.entity.base import Base, Name, Location, Contact
from booksomeplace.entity.property import Property, PropInfo, RoomType
from booksomeplace.domain.property import PropertyService
The addEdit() method uses the same set of forms for either adding a new room or updating an existing one. The first thing we need to do in this method is to retrieve an instance of the property and form services:
def addEdit(request, prop_key = None) :
prop_name = 'NEW'
add_or_edit = 'ADD'
mainConfig = Config()
propService = PropertyService(mainConfig)
formSvc = FormService()
message = 'Please enter appropriate form data'
It's important to determine whether or not a property key was passed as a parameter. This parameter is defined in the URL, described in the next section. We make a call to the fetchByKey() method of the booksomeplace.domain.property.PropertyService class. If we get a hit, we record the property name, and assign add_or_edit a value of EDIT. In addition, we call the hydrateFormsFromEntity() form service method, which populates all sub-forms from a Property instance:
if prop_key :
prop = propService.fetchByKey(prop_key)
if prop :
prop_name = prop.get('propName')
add_or_edit = 'EDIT'
formSvc.hydrateFormsFromEntity(prop)
It is also necessary to check and see if the request method was POST. If so, we use the form service to populate all sub-forms with the appropriate request data. We then use the form service to validate all sub-forms. If validation is successful, and the operation is to add a property, we call the save() method of the PropertyService class. If the save operation was successful, we redirect to the page that lets us start adding rooms to the property:
if request.method == 'POST':
formSvc.hydrateFormsFromPost(request.POST)
if formSvc.validateForms() :
prop = formSvc.getPropertyFromForms()
if add_or_edit == 'ADD' :
if propService.save(prop) :
message = 'Property added successfully!'
return HttpResponseRedirect('/property/room/add/' + \
prop.getKey())
else :
message = 'Sorry! Unable to save form data.'
Otherwise, we call the edit() method (discussed in the next subsection) to perform an update. If any operation fails, an appropriate message is displayed:
else :
if propService.edit(prop) :
message = 'Property updated successfully!'
return HttpResponseRedirect('/property/list/' + \
prop.getKey())
else :
message = 'Sorry! Unable to save form data.'
else :
message = 'Sorry! Unable to validate this form.'
If the request method is not POST, we assume forms just need to be rendered as they stand. Because we are using two levels of form templates, we use the Django render_to_string() method. The results are then rendered into the master template, layout.html:
context = { 'message' : message, 'prop_name' : prop_name }
for key, sub_form in formSvc.forms.items() :
form_var = key + '_form_html'
form_file = 'property_' + key + '.html'
context.update({form_var : render_to_string(form_file, \
{'form': sub_form})})
prop_master = render_to_string('property.html', context)
page = render_to_string('layout.html', {'contents' : prop_master})
return HttpResponse(page)
Next, let's define a route to addEdit().
In order to process requests to add a property, we need to define a route to the appropriate view. This is accomplished in the urls.py file. The main website routes are defined in the project's urls.py file, found in the /path/to/repo/www/chapter_08/booksomeplace_dj directory. Also note the added entry for the property app:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('booking/', include('booking.urls')),
path('property/', include('property.urls')),
path('', include('home.urls')),
]
In the property/urls.py file, each entry also uses the special path() function, which takes three arguments:
In order to add a new property, from the client we would enter the URL /project/edit/. If, on the other hand, we had a specific property to edit, the URL would be /project/edit/<PROPKEY>/, as you can see from the file shown here:
from django.urls import path
from . import views
urlpatterns = [
path('edit/', views.addEdit, name='edit_property_new'),
path('edit/<slug:prop_key>/', views.addEdit, name='edit_property'),
]
Let's now generate the property key.
Before a new property can be saved, a property key needs to be generated from the property name. The best place to do this is in the entity class itself. Accordingly, we add a new method, generatePropertyKey(), to the booksomeplace.entity.property.Property class, as shown here. If for some reason the property name is less than four characters long, we append an underscore (_) character.
We also remove any spaces and make the first four letters uppercase. Finally, a random four-digit number is appended:
def generatePropertyKey(self) :
import random
first4 = self['propName']
first4 = first4.replace(' ', '')
first4 = first4[0:4]
first4 = first4.ljust(4, '_')
self['propertyKey'] = first4.upper() + \
str(random.randint(1000, 9999))
return self['propertyKey']
Next, we will update the property domain service.
We also need to add a save() method to the booksomeplace.domain.property.PropertyService class. This method needs to accept a Property instance as an argument, retrieved from cleaned form data using the getPropertyFromForms() form service method described earlier in this section. As our PropertyService class is already geared toward working with entity classes, the code is absurdly simple, as you can see here:
def save(self, prop) :
prop.generatePropertyKey()
return self.collection.insert_one(prop)
Next, we will learn to add a new property.
We can then enter the URL http://chap08.booksomeplace.local/property/edit/ into the browser. Here is the resulting screen, showing the sub-forms for each subclass:

If any of the fields fail validation, the form is re-displayed, with error messages next to the appropriate form fields. Once the form has been completed successfully, the user is redirected to a form where room types for this property can be added.
In this sub-section, we address adding rooms to a property. Room definitions take the form of an array (list) of RoomType objects. Thus, the task at hand is doubly complicated in that we need to work not only with embedded objects, but embedded lists as well.
The /path/to/repo/www/chapter_08/property/forms.py file represents a module containing a form to capture room type information that needs a set of multiple check boxes for room and bed types. The actual lists of types are drawn from common collection documents under the roomTypes and bedTypes keys. We use the CommonService domain service class to perform lookups for both keys. Each document in the common collection has two fields: key and value. The information contained in the value field is in the form of a list.
We also need a template to match the form. The Django template language is used, and provides safety measures in case of an error. The complete form template is located at /path/to/repo/www/chapter_08/templates/property_room_type.html.
We need a way to enter room types for a given property. This is seen in the /path/to/repo/www/chapter_08/property/views.py file. The addRoom() method looks for a URL parameter representing the property key. As you recall from the discussion in the earlier sub-section, after filling in the form for a new property, the Book Someplace admin is directed to this view.
The first logical block in this method verifies the incoming property key parameter. If it is present and correct, we use the PropertyService to perform a lookup, and retrieve a Property entity instance:
def addRoom(request, prop_key = None):
error_flag = True
prop_name = 'Unknown'
message = 'Please enter room information'
propService = PropertyService(Config())
if prop_key :
prop = propService.fetchByKey(prop_key)
if prop :
prop_name = prop.get('propName')
error_flag = False
else :
message = 'ERROR: property unknown: unable to add rooms'
else :
message = 'ERROR: property unknown: unable to add rooms'
The next several lines of code create an instance of the room form described earlier, and proceed if the request method is POST, and error_flag is False. error_flag is only set to False when a valid Property instance has been retrieved:
room_form = RoomTypeForm()
if request.method == 'POST' and not error_flag :
if 'done' in request.POST :
return HttpResponseRedirect('/property/list/' + prop.getKey())
room_form = RoomTypeForm(request.POST)
If the room form is valid, we retrieve clean data from the form:
if room_form.is_valid() :
start = len('room_type_')
cleanData = {}
for formField, value in room_form.cleaned_data.items() :
docField = formField[start:]
cleanData.update({docField : value})
We use the PropertyService to save the room, passing the RoomType instance and the property key as arguments:
if propService.saveRoom(RoomType(cleanData), prop_key) :
message = 'Added room successfully'
else :
message = 'Sorry! Unable to save form data.'
else :
message = 'Sorry! Unable to validate this form.'
If the request was not POST, or if the form fails validation, we render the room type form and produce a response:
room_form_html = render_to_string('property_room_type.html', {
'form' : room_form,
'message' : message,
'prop_key' : prop_key,
'prop_name' : prop_name,
'error_flag' : error_flag
})
page = render_to_string('layout.html', {'contents' : room_form_html})
return HttpResponse(page)
We also need a path to the new view method. This is defined in /path/to/repo/www/chapter_08/property/urls.py, as shown here. Please note that to conserve space we do not show other paths:
urlpatterns = [
# other paths not shown
path('room/add/<slug:prop_key>/', views.addRoom, name='property_add_room'),
]
Now we are ready to examine adding an embedded array in MongoDB.
The last piece of the puzzle is to define a method in the property domain service class that accepts form data and updates the database. Because we are adding room types to an embedded list within a document in the properties collection, the database operation we need to perform is not an insert, but rather an update.
MongoDB update operators (https://docs.mongodb.com/manual/reference/operator/update/#update-operators) are needed in order to effect changes when performing an update. Update operators are categorized according to what element they affect: fields, arrays, modifiers, and bitwise.
The following table summarizes the more important update operators:
| Affects ... | Operator | Description |
| Fields | $rename | Allows you to rename a field |
| $set | Overwrites the current value of the field with the new, updated, value | |
| $unset | Deletes the current value of a field | |
| Arrays | $ | Placeholder for first element that matches the filter query criteria |
| $[] | Placeholder for all elements that match the filter query criteria | |
| $addToSet | Adds an element to the array only if it doesn't already exist | |
| $push | Adds an item to the array | |
| Modifiers | $each | Works with $push to append multiple items to the array |
| $sort | Works with $push to re-order the array | |
| Bitwise | $bit | Used to perform bitwise operations AND, OR, and XOR on integer values |
Next, let's look at the domain service method to add rooms.
Before we can define the domain service to add a room, we need to ensure that individual room types can be independently selected. This is accomplished by generating a unique room type key. We thus add a new method, generateRoomTypeKey(), to the booksomeplace.entity.property.RoomType class, as shown here:
def generateRoomTypeKey(self) :
import random
first4 = self['type']
first4 = first4.replace(' ', '')
first4 = first4[0:4]
first4 = first4.ljust(4, '_')
self['roomTypeKey'] = first4.upper() + \
str(random.randint(1000, 9999))
return self['roomTypeKey']
We are now in a position to add a saveRoom() method to PropertyService in the booksomeplace.domain.property module. This method adds the new RoomType to the embedded list in the properties collection. Note the use of the $push array operator:
def saveRoom(self, roomType, prop_key) :
success = False
prop = self.fetchByKey(prop_key)
if prop :
updateDoc = {'$push' : { 'rooms' : roomType }}
query = {'propertyKey' : prop.getKey()}
success = self.collection.update_one(query, updateDoc)
return success
We will use the form in the next section.
In the browser on our local computer, we now test our new feature by entering this URL:
http://chap08.booksomeplace.local/property/room/add/MAJE2976/
We are then presented with the RoomType form, as shown in the following screenshot:

We can confirm the new addition from the mongo shell, as shown here:

Let's now learn how to update documents with embedded objects.
In this scenario, we imagine that an admin has received a request from a partner to update their contact details for a certain property. In order to enable the admin to update Property contact details, the web app needs to do the following:
The traditional approach would be to first look up a list of partners and then direct the admin to a screen where they would choose the partner to work on. The form posting would then present the admin with another form where they could choose from a list of properties for that partner. Finally, a third form would appear with the information to be updated. In total, this represents a cumbersome three-stage process, during which the application would need to carry along both the partner and property keys, either through hidden form properties or by using a session mechanism.
By incorporating JavaScript functions, we can accomplish the same thing using just a single form. The JavaScript functions make AJAX requests to backend views that deliver JSON responses. Using the jQuery change() method, we configure the form such that when the element containing the list of partners changes, an AJAX request is made to repopulate the list of properties such that the list represents only properties for the selected partner. In a similar manner, once the property has been selected, we populate all pertinent contact detail fields.
We start this discussion by presenting the form field that presents the list of partners.
In order to choose a partner to work with, we simply create a form with a single field, which we can populate with values obtained from the database. In order to accomplish this, we need the following:
For this purpose, in the property app within our Django website, we define the following form:
class ChoosePartnerForm(forms.Form) :
partSvc = PartnerService(Config())
partKeys = partSvc.fetchPartnerKeys()
alnumPunc = RegexValidator(r'^[\w\.\-\,\"\' ]+$',
'Only letters, numbers, spaces and punctuation are allowed.')
prop_partnerKey = forms.ChoiceField(label='Partner',choices=partKeys)
We also need a method in the partner domain service to retrieve partner names and keys for display, which we call fetchPartnerKeys(). We use the projection argument to the pymongo.collection.find() method to limit results to the partnerKey and businessName fields. We also define sort parameters so that partner business names are presented in alphabetical order. The method returns a dictionary that consists of key/value pairs, where the key is the partnerKey field and the value is the businessName:
def fetchPartnerKeys(self) :
query = {}
projection = {'partnerKey' : 1, 'businessName' : 1}
sortDef = [('businessName', pymongo.ASCENDING)]
skip = 0
limit = 0
cursor = self.collection.find(query, projection, skip, \
limit, False, CursorType.NON_TAILABLE, sortDef)
temp = {}
for doc in cursor :
temp.update({doc['partnerKey'] : doc['businessName']})
return temp.items()
Next, we present the methods that produce JSON results in response to AJAX queries.
In order to respond to an AJAX query, we define matching domain service methods that look up requested values. Also needed are the corresponding view methods to call the domain service methods and produce JSON responses. Accordingly, in booksomeplace.domain.partner.PartnerService we define a method called fetchByPartnerKey() that retrieves a list of property keys and names:
def fetchByPartnerKey(self, key) :
query = {'partnerKey' : key}
proj = {'propertyKey' : 1, 'propName' : 1, '_id' : 0}
result = self.collection.find(query, proj)
data = []
if result :
for doc in result :
data.append(doc)
return data
The other method needed is fetchContactInfoByKey(), called when a property has been selected. Again we use the projection argument to limit the information returned to the embedded object's Name, represented by contactName, and Contact, represented by contactInfo.
In this illustration we use the PyMongo find_one() collection method, as the property key is unique:
def fetchContactInfoByKey(self, key) :
query = {'propertyKey' : key}
proj = {'contactName' : 1, 'contactInfo' : 1, '_id' : 0}
result = self.collection.find_one(query, proj)
return result
In the Django booking app, found in the /path/to/repo/www/chapter_08/ directory, we define two view methods. The first method, autoPopPropByPartKey(), automatically populates the list of properties based upon the partner key. In this method, shown here, we return a JSON response:
def autoPopPropByPartKey(request, part_key = None) :
import json
propSvc = PropertyService(Config())
result = []
if part_key :
result = propSvc.fetchByPartnerKey(part_key)
return HttpResponse(json.dumps(result), content_type='application/json')
The second important method is autoPopContactUpdate(), which populates the appropriate contact information field once a property is chosen:
def autoPopContactUpdate(request, prop_key = None) :
import json
propSvc = PropertyService(Config())
result = []
if prop_key :
result = propSvc.fetchContactInfoByKey(prop_key)
return HttpResponse(json.dumps(result), content_type='application/json')
After creating these methods, we add a URL that maps to each method described just now. The first path requires a partner key to produce results. The other URL path takes a property key. These paths are placed in the urls.py file located in the /path/to/repo/www/chapter_08/booking directory, shown here:
urlpatterns = [
path('json/by_part/<slug:part_key>/', views.autoPopPropByPartKey,
name='property_json_props_by_part_key'),
path('json/update_contact/<slug:prop_key>/', \
views.autoPopContactUpdate, \
name='property_json_update_contact'),
# other paths not shown
]
The next step is to define the contact information form and view, which brings everything together.
In order to put all the pieces described in this sub-section together, we need the following:
A new view method, updateContact(), in /path/to/repo/www/chapter_08/property/views.py, creates instances of the form service described in the previous sub-section. In addition, it retrieves instances of both the partner and property domain services. The check variable represents the number of validation checks to be performed:
def updateContact(request) :
formSvc = FormService()
message = ''
config = Config()
partSvc = PartnerService(config)
propSvc = PropertyService(config)
check = 4
We then define a sequence of validation checks to be performed if the request method is POST. Before validation we use the form service to hydrate NameForm and ContactForm with post data:
if request.method == 'POST' :
formSvc.hydrateFormsFromPost(request.POST)
name_form = formSvc.getForm('name')
contact_form = formSvc.getForm('contact')
The first validation block confirms partner key validity by calling upon the partner domain service fetchByKey() method:
if 'prop_partnerKey' in request.POST :
partKey = request.POST['prop_partnerKey']
part = partSvc.fetchByKey(partKey)
if part :
check -= 1
else :
message += '<br>Unable to confirm this partner.'
else :
message += '<br>You need to choose a partner.'
The second validation block confirms that the property key is valid:
if 'prop_propertyKey' in request.POST :
propKey = request.POST['prop_propertyKey']
prop = propSvc.fetchByKey(propKey)
if prop :
check -= 1
else :
message += '<br>Unable to confirm this property.'
else :
message += '<br>You need to choose a property.'
The last two validation blocks use pre-defined Django form validators against the name and contact sub-forms:
if name_form.is_valid() : check -= 1
if contact_form.is_valid() : check -= 1
If all validations succeed, we create Name and Contact instances from the booksomeplace.entity.base module. We set these instances into the Property entity instance retrieved during validation and call upon the PropertyService to perform the update using a method called edit() (described later in this chapter). If the operation succeeds, we list the property:
if check == 0 :
prop.set('contactName', \
Name(formSvc.getCleanDataByKey('name')))
prop.set('contactInfo', \
Contact(formSvc.getCleanDataByKey('contact')))
if propSvc.edit(prop) :
message += '<br>Property contact information updated!'
return HttpResponseRedirect('/property/list/' + \
prop.getKey())
else :
message += '<br>Unable to update form data.'
else :
message += '<br>Unable to validate this form.'
If the request is not POST, we render the sub-forms and inject the resulting HTML into the primary website layout:
context = {
'message' : message,
'choose_part_form' : ChoosePartnerForm(),
'name_form_html' : render_to_string('property_name.html',
{'form': formSvc.getForm('name')}),
'contact_form_html' : render_to_string('property_contact.html',
{'form': formSvc.getForm('contact')})
}
prop_master = render_to_string('property_update_contact.html', context)
page = render_to_string('layout.html', {'contents' : prop_master})
return HttpResponse(page)
In order to have Django return the property contact update form, we need to add an entry to urls.py for the property app:
urlpatterns = [
path('contact/update/', views.updateContact, name='property_update_contact'),
# other paths not shown
]
Let's now define JavaScript functions to generate AJAX requests.
The property contact update form is located in /path/to/repos/www/chapter_08/templates/property_update_contact.html. It starts with a standard HTML <form> tag, along with an identifying header:
<form action="/property/contact/update/" method="post">
{% csrf_token %}
<h1>Update Property Contact</h1>
<div class="row extra" style="background-color: #C8C8EE;">
<div class="col-md-6">
{{ choose_part_form }}
</div><!-- end col -->
In the next several lines, you can see that the option to choose a property is populated from an AJAX request, and is thus not represented as a Python form:
<div class="col-md-6">
<label>Property:</label>
<select name="prop_propertyKey" id="ddlProperties" >
<option value="0">Choose Partner First</option>
</select>
</div><!-- end col -->
</div><!-- end row -->
The remaining HTML is very similar to the templates discussed earlier in this chapter:
<div class="row extra" style="background-color: #E6E6FA;">
<div class="col-md-6">
{{ name_form_html }}
</div><!-- end col -->
<div class="col-md-6" style="background-color: #D6D6F0;">
{{ contact_form_html }}
</div><!-- end col -->
</div><!-- end row -->
<input type="submit" value="Submit">
</form>
At the end of the contact form are two JavaScript functions using jQuery. The first monitors any changes to the HTML select element used to choose the partner. Should this change, an AJAX request is made that re-populates the HTML select element representing properties for that partner:
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script>
$(document).ready(function() {
$("#id_prop_partnerKey").change(function() {
var partKey = $(this).val();
console.log( "partner key: " + partKey );
$("#ddlProperties").empty();
$.ajax({
type: "GET",
url: "/property/json/by_part/" + partKey + "/",
dataType: 'json',
success: function(data){
$.each(data, function(i, d) {
$("#ddlProperties").append(
'<option value="' + d.propertyKey + '">'
+ d.propName
+ '</option>');
});
}
});
});
The second JavaScript function monitors any changes to the auto-generated properties of the HTML select element. An AJAX request is made to provide the property key:
$("#ddlProperties").change(function() {
var propKey = $(this).val();
console.log( "property key: " + propKey );
$.ajax({
type: "GET",
url: "/property/json/update_contact/" + propKey + "/",
dataType: 'json',
If the call was successful, the contact information fields are updated accordingly:
success: function(data){
$("#id_name_title").val(data['contactName']['title']);
$("#id_name_first").val(data['contactName']['first']);
$("#id_name_last").val(data['contactName']['last']);
$("#id_name_suffix").val(data['contactName']['suffix']);
$("#id_contact_email").val(data['contactInfo']['email']);
$("#id_contact_phone").val(data['contactInfo']['phone']);
var socMedia = data['contactInfo']['socMedia'];
for (var k in socMedia) {
$("#id_contact_soc_type").val(k);
$("#id_contact_soc_addr").val(socMedia[k]);
}
}
}); }); });
</script>
Next, let's use operators to update contact information.
In order to apply a contact information update, we use the $set update operator. As you recall, updates are performed using entity classes as an argument. Accordingly, it makes sense to use the entity class to create its own update document. Therefore, we add a method called getUpdateDoc() to the booksomeplace.entity.base.Base class, as shown here:
class Base(dict) :
def getUpdateDoc(self) :
doc = {}
for key, value in self.fields.items() :
doc.update({'$set' : {key : self[key]}})
return doc
# other methods not shown
We then add a method called edit() to booksomeplace.domain.property.PropertyService that accepts a property entity as an argument. In order to perform the update, it calls the inherited getUpdateDoc() method:
def edit(self, prop) :
query = {'propertyKey' : prop.getKey()}
return self.collection.update_one(query, prop.getUpdateDoc())
Next, we use the contact update form.
To use the contact update form, we enter the URL http://chap08.booksomeplace.local/property/contact/update. The initial form is as shown here:

When we select a partner, the list of properties changes, as shown here:

And finally, after selecting a property, contact information is updated, as shown here:

We can then make the needed changes and hit Submit to save changes.
Listing a property does not require a form, and only involves the pymongo.collection.findOne() method called from a property domain service lookup method. Things get a bit tricky, however, when dealing with room type listings. This involves parsing an embedded array of RoomType objects. What is even trickier is that within this array of objects is another property, beds, itself another array! So, here is our proposed approach:
The view method that returns JSON also breaks down the array of bed types embedded within each RoomType object.
In the Django property app, we add to /path/to/repo/www/chapter_08/property/views.py a view method, listProp(), that accepts a property key as an argument. If the property key is present, we use the property domain service to retrieve a Property entity instance:
def listProp(request, prop_key = None) :
propSvc = PropertyService(Config())
prop = Property()
rooms = []
message = ''
if prop_key :
prop = propSvc.fetchByKey(prop_key)
# build master form
We are now in a position to render the form:
info = prop.get('propInfo')
context = {
'message' : message,
'prop_name' : prop.get('propName'),
'prop_type' : info.get('type'),
'prop_des' : info.get('description'),
'prop_key' : prop_key,
}
prop_master = render_to_string('property_list.html', context)
page = render_to_string('layout.html', {'contents' : prop_master})
return HttpResponse(page)
Let's now look at the room types view method.
In the same file in the Django property app, we define a method that returns a JSON response to a jQuery AJAX request for room information. Again, as before, if the property key is present in the request, we use the property service to retrieve a Property instance:
def autoPopRoomsByPropKey(request, prop_key = None) :
import json
config = Config()
propSvc = PropertyService(config)
result = []
if prop_key :
temp = propSvc.fetchRoomsByPropertyKey(prop_key)
If we are able to get an instance, we flatten the RoomType.beds property list into a string:
if temp :
for rooms in temp :
beds = ''
for val in rooms['beds'] :
beds += val + ', '
beds = beds[0:-2]
We then formulate a return value as a JSON encoded list of dictionaries:
room_config = config.getConfig('rooms')
link = '<a target="_blank" href="'
link += room_config['details_url']
link += rooms['roomTypeKey'] + '/">Details</a>'
result.append({'type':rooms['type'], \
'view':rooms['view'], 'beds':beds, \
'available':rooms['numAvailable'],'link':link})
return HttpResponse(json.dumps(result), \
content_type='application/json')
You also can see that we modified the Config class found in /path/to/repo/chapters/08/src/config/config.py by adding extra configuration to represent the URL used in the link, giving more details on any given room:
# config.config
import os
class Config :
config = dict({
'rooms' : {
'details_url' : 'http://chap08.booksomeplace.local/property/room/details/',
},
})
# other configuration and methods not shown
}
Next, we add the appropriate URL routes.
In the Django property app, in the module file at /path/to/repo/www/chapter_08/property/urls.py, we add two routes, as shown here. The first route gives us access to the property listing. The second route is used by the jQuery AJAX request. They both define a property key parameter:
urlpatterns = [
path('list/<slug:prop_key>/', views.listProp, name='property_list'),
path('json/rooms/<slug:prop_key>/', \
views.autoPopRoomsByPropKey, \
name='property_json_rooms_by_prop_key'),
# other URL routes not shown
]
The property listing template, property_list.html, found in /path/to/repo/www/chapter_08/templates, is much like the other Django templates shown earlier in this chapter. We provide the values to the render process, which then produces the actual results:
<div class="container" style="margin-top: -100px;">
<div class="row">
<div class="col-md-6">
<h1>Property Listing</h1>
<div class="row extra">
<div class="col-md-2"><b>Name</b></div>
<div class="col-md-4">{{ prop_name }}</div>
</div><!-- end row -->
<div class="row extra">
<div class="col-md-2"><b>Type</b></div>
<div class="col-md-4">{{ prop_type }}</div>
</div><!-- end row -->
<div class="row extra">
<div class="col-md-2"><b>Description</b></div>
<div class="col-md-4">{{ prop_des }}</div>
</div><!-- end row -->
</div>
<div class="col-md-6">
<img src="{% static 'images/hotel.jpg' %}" style="width:100%;"/>
</div>
</div>
The table contains headers representing room type information, but the actual data is left blank. The reason is that this information is supplied by a jQuery AJAX request (described in the next sub-section):
<div class="row">
<div class="col-md-12">
<h3>Room Types</h3>
<table id="data_table" class="display" width="100%">
<thead>
<tr>
<th>Type</th>
<th>View</th>
<th>Beds</th>
<th>Available</th>
<th>Info</th>
</tr>
</thead>
</table>
{{ message }}
</div>
</div><!-- end row -->
</div><!-- end container -->
Next, we will cover the jQuery AJAX function requesting room types.
At the end of the same template file, property_list.html, we add a header block that ensures the desired version of jQuery is loaded:
{% block extra_head %}
<script type="text/javascript" charset="utf8" src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
{% endblock %}
After that, we add a jQuery $(document).ready() function that makes an AJAX request to the URL described in the preceding code block. The return value from the request is a list of dictionaries with the properties type, view, beds, available, and link:
<script type="text/javascript" charset="utf8">
$(document).ready( function () {
$.ajax({
'type' : 'GET',
'url' : '/property/json/rooms/{{ prop_key }}/',
'dataType' : 'json',
'success' : function(data){
$.each(data, function(i,row) {
item = '<tr><td>' + row.type + '</td>'
item += '<td>' + row.view + '</td>'
item += '<td>' + row.beds + '</td>'
item += '<td>' + row.available + '</td>'
item += '<td>' + row.link + '</td></tr>'
$('#data_table').append(item);
});
}
});
});
</script>
We can now list the results in the next section.
Putting it all together, in a browser, enter a URL that contains a valid property key. Here is an example of an appropriate URL:
http://chap08.booksomeplace.local/property/list/MAJE2976/
The result is shown here:

We now focus on how to remove a document.
Removing a property shares many similarities with the process of editing and listing a property. Where the operations diverge, of course, is when removing a property, we rely upon the pymongo.collection.delete_one() method. This method is called from the property domain service class.
The first step we take is to define a method in the PropertyService class, found in the /path/to/repo/chapters/08/src/booksomeplace/domain/property.py module:
def deleteByKey(self, key) :
query = {'propertyKey':key}
return self.collection.delete_one(query)
Next, we turn our attention to the process of choosing the partner and the property to be deleted.
As we mentioned, this process is similar to the process needed to update property contact information. Accordingly, we can simply copy the already existing view and template code from the update contact logic and merge it with the logic needed to list a property. In the Django property app, we add the delProperty() method to views.py. We start by initializing variables:
def delProperty(request) :
propSvc = PropertyService(Config())
prop = Property()
rooms = []
message = ''
confirm = False
prop_key = ''
Next, we check to see if a property has been chosen:
if request.method == 'POST' :
if 'prop_propertyKey' in request.POST :
prop_key = request.POST['prop_propertyKey']
prop = propSvc.fetchByKey(prop_key)
if prop :
confirm = True
message = 'Confirm property to delete'
If the prop_propertyKey field from the ChoosePartnerForm is not present, we next check to see if we are in phase 2, which is where a property has been chosen and we wish to confirm deletion:
if 'del_confirm_yes' in request.POST \
and 'prop_key' in request.POST :
prop_key = request.POST['prop_key']
prop = propSvc.fetchByKey(prop_key)
If the delete operation is confirmed, we call upon the property domain service to perform the delete and report whether it is a success or failure. We also set the appropriate return message:
if prop :
if propSvc.deleteByKey(prop_key) :
message = 'Property ' + prop.get('propName')
message += ' was successfully deleted'
else :
message = 'Unable to delete property ' + \
prop.get('propName')
else :
message = 'Unable to delete property ' + \
prop.get('propName')
The last part of the view method pushes forms and other information to the template, rendered as shown:
info = prop.get('propInfo')
context = {
'choose_part_form' : ChoosePartnerForm(),
'message' : message,
'prop_name' : prop.get('propName'),
'prop_type' : info.get('type'),
'prop_des' : info.get('description'),
'prop_key' : prop_key,
'confirm' : confirm,
}
prop_master = render_to_string('property_list_del.html', context)
page = render_to_string('layout.html', {'contents' : prop_master})
return HttpResponse(page)
We can now define and choose the confirm delete template.
The template for this operation needs to have two parts: one part where the admin chooses the partner and property. The second part displays chosen property information and asks for confirmation. This is accomplished using Django template language. We add an if statement that tests the value of a confirm property. If it is False, we display the sub-form to choose the partner and property. The second sub-form is only shown if we are in the confirmation phase. You can view the full form at /path/to/repo/www/chapter_08/templates/property_list_del.html.
In the Django property app, we add a route to urls.py to access the new view method:
urlpatterns = [
path('delete/', views.delProperty, name='property_delete'),
# other paths not shown
]
We can then enter this in the browser:
http://chap08.booksomeplace.local/property/delete/
We are then presented with the sub-form, which allows us to choose a partner and property to delete:

After making the desired choices, we are then shown property information and asked to confirm:

We are then returned to the first form, with a success message shown at the bottom.
The focus of this chapter was working with embedded objects and arrays (lists). Projects presented in this chapter, for the most part, were integrated into Django. In this chapter, you learned how to add a complex object containing embedded objects and arrays to the database. In order to accomplish this, you learned how to create entity classes from form data by defining a form service. The form service performed a process called hydration and moved data between form and entity, and back again. You were also shown how to associate embedded objects with their own sub-forms, and how to insert sub-forms into a primary form.
You then learned about adding rooms to a property, which involved creating embedded objects, that were in turn added to an embedded array. When learning how to use the pymongo.collection.update_one() method to update property contact information, you were also shown how to use AJAX requests to pre-populate fields when the value of other fields changed.
In the next chapter, you will learn to handle complex tasks using the MongoDB Aggregation Framework, as well as how to manage queries involving geo-spatial data and generating financial reports.
This chapter introduces you to the aggregation framework, a feature unique to MongoDB. This feature represents a departure point where MongoDB forges ahead of its legacy relational database management system (RDBMS) ancestors. In this chapter, you will also learn about the legacy Map-Reduce capability, which has been available in MongoDB since the beginning. Two other extremely useful techniques covered in this chapter are how to model complex queries using the MongoDB Compass tool and how to conduct geospatial queries.
The focus of this chapter is on financial reports, including revenue reports aggregated by user-selectable criteria, aging reports, and how to perform financial analysis on booking trends in order to create a financial sales projection report for Book Someplace. In this chapter, you will learn how all these revenue reports can assist in performing risk analysis. Finally, you will learn how to use geospatial data to generate a revenue report based on geographic area.
The topics covered in this chapter include the following:
The minimum recommended hardware for this chapter is as follows:
The software requirements are as follows:
Installation of the required software and how to restore the code repository for the book is explained in Chapter 2, Setting Up MongoDB 4.x. To run the code examples in this chapter, open a Terminal window (Command Prompt) and enter these commands:
cd /path/to/repo
docker-compose build
docker-compose up -d
docker exec -it learn-mongo-server-1 /bin/bash
To run the demo website described in this chapter, once the Docker container for Chapter 3, Essential MongoDB Administration Techniques, to Chapter 12, Developing in a Secured Environment, has been configured and started, there are two more things you need to do:
172.16.0.11 booksomeplace.local
172.16.0.11 chap09.booksomeplace.local
http://chap09.booksomeplace.local/
When you are finished working with the examples covered in this chapter, return to the Terminal window (Command Prompt) and stop Docker, as follows:
cd /path/to/repo
docker-compose down
The code used in this chapter can be found in this book's GitHub repository at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/chapters/09, as well as https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/www/chapter_09.
Queries can become quite complex when working with documents containing embedded objects and arrays. In this situation, it might be helpful to use an external tool to model potential queries before committing them to code. One such tool, included in default MongoDB installations, is the MongoDB Compass tool. As a first step, we will demonstrate how to conduct queries on fields within embedded objects. After that, we will move on to data contained in embedded arrays.
There are three versions of MongoDB Compass available, all open source and free to use:
The discussion in this section focuses on the full version, simply called Compass or MongoDB Compass.
To install MongoDB Compass on Windows, proceed as follows:
To install MongoDB Compass on a Mac, proceed as follows:
To install MongoDB Compass on RedHat/CentOS/Fedora, proceed as follows:
To install MongoDB Compass on Ubuntu/Debian, proceed as follows:
Now, let's have a look at establishing the initial connection.
The initial connection screen is very straightforward. You need to supply information pertaining to the host upon which your database resides, and any authentication information. In our example, ensure the Docker container you defined for Chapter 3, Essential MongoDB Administration Techniques, to Chapter 12, Developing in a Secured Environment, is up and running. Here is the initial screen after first starting MongoDB Compass on the host computer:

You can also select Fill in connection fields individually, in which case these are the two tab screens you can use:

You can see right away that the fields closely match the mongo Shell connection options. Here is a summary of the options available on the MongoDB Compass connection screens:
| Option | Notes |
| Hostname | Enter the DNS or IP address of the server to which you wish to connect. |
| Port | Enter the appropriate port number. The default is 27017. |
| SRV Record | Toggle this switch if the server to which you want to connect has a DNS SRV record. Note that when you toggle this switch, the Port option disappears as Compass queries the DNS for more information. |
| Authentication | Options include a standard username/password or SCRAM-SHA-256. The paid versions also include Kerberos, LDAP, and X.509. Authentication is discussed in more detail in Chapter 11, Administering MongoDB Security. |
| Replica Set Name | In this field, you must enter the name of the replica set if the server to which you plan to connect is a member of a replica set. The default is None. |
| Read Preference | This field allows you to influence how Compass performs reads. The default is Primary. If you choose Secondary, all reads are performed from a secondary server in the replica set. Choose Nearest if replica set servers are dispersed geographically and you wish to perform reads from the one that is fewer route hops away. Finally, there are options for Primary Preferred and Secondary Preferred. The Preferred option tells Compass to perform the read from the preferred choice, but allows reads from the other type if the preference is not available. |
You might also note a star icon to create a list of favorites. Next, we will discuss the SSL connection options, which are also part of the initial connection screen.
Use the SSL option if you need a secure connection. The default is None, which corresponds to the Unvalidated (insecure) option. If you are making a connection to MongoDB Atlas, use the System CA/Atlas Deployment option. If you select Server Validation, you will see a browse button appear that lets you choose the certificate authority file for the server running the mongod instance.
If you choose Server and Client Validation, you are presented with a set of drop-down boxes that let you select the server's certificate authority file, the client certificate file, and the client's private key file. You must also enter the password. Here are the additional fields that appear when you choose Server and Client Validation:

In order to connect via an SSH tunnel, click SSH Tunnel and select either Use Password or Use Identity File. If you choose the former, you must enter the SSH server's hostname, tunnel port, username, and password. Here is what you'll see:

If for SSH Tunnel you select Use Identity File, in addition to the information shown previously, you should browse to the file that identifies the client. On Linux and macOS, this file is typically located in the user's home directory in a hidden folder named .ssh. The name of this file typically starts with either id_rsa* or id_dsa*, although there are many variations. Here is the dialog that appears after selecting the Use Identity File option for SSH Tunnel:

Now that we have covered the SSL connection options, let's learn how to make the connection.
After selecting the appropriate options and entering the required information, click on the Connect button. You will see an initial screen that shows you the various databases present on your server. If you have been following the labs and examples in this book, you will see the presence of the sweetscomplete and booksomeplace databases. Here is the screen after connecting:

You can see that MongoDB Compass Community Edition can be used for basic database administration, which includes the following:
For the purpose of this section, go ahead and select booksomeplace. You will see documents in the collection in either tabular or list format. Here is the screen that shows details of the bookings collection:

You can actually perform a direct import of JSON documents from MongoDB Compass by selecting Add Data. However, the documents have to be pure JSON: not JavaScript variables, functions, or object code. For that reason, the mongo shell JavaScript files present in the /path/to/repo/sample_data directory do not work. Here is the import option dialog:

Now it's time to have a look at how we can model queries using MongoDB Compass.
Management has asked you to create a financial report that provides a summary of revenue generated by users with Facebook accounts. Looking at the structure of documents in the booksomeplace.bookings collection, you note that the Facebook page for customers is contained within a socialMedia array, which itself is contained in an embedded document, customerContact. You further note that information in the socialMedia array is keyed according to the social media type.
The MongoDB internal document representation closely follows JSON syntax. Accordingly, there are no object class types assigned when the document is stored in the database. You can access object properties by simply appending a dot (.) after the field that references the object. So, in our example, the customerContact field represents a Contact object. It, in turn, is nested in the field referenced by customer. In order to reference information in the socialMedia property of the embedded Contact object, the completed field reference would be as follows:
customer.customerContact.socMedia
In a similar way, array keys are viewed internally as properties of their owning field. In this case, we wish to locate documents where the array key in the socMedia field equals facebook. In order to use MongoDB Compass to model a query, connect to the appropriate database, select the target collection, and under the Documents tab, click on Options. We first enter the appropriate filter. Note that as you begin typing, a list of fields automatically appears, as shown here:

After choosing the field, we enter a colon (:). After typing in the dollar sign ($), a list of operators appears next, as shown:

You can then complete the query by filling in the projection (PROJECT) and any other appropriate parameters. The FIND button does not become active until you ensure that each option is itself a proper JSON document. Once completed, click FIND. Here are the results using the sample data:

To get a good overview of the completed query document, click on the three dots (...) at the top right, and select Toggle Query History. You can view your query history in MongoDB Compass, mark a query as favorite, and also copy the entire query document to the clipboard. In addition, if you click on the query itself, it places all the query parameters back into the main query options dialog. Here is the Past Queries window (on the right side of the following screenshot):

In order to move the query into your preferred programming language, from the Past Queries window, select the target query, and then select the icon to copy to the clipboard. We can now use this in a Python script, as in the following (found in /path/to/repo/chapters/09/compass_query_involving_embedded_objects.py).
Here is what was returned by copying the query from the MongoDB Compass Past Queries window:
{
filter: {
'customer.customerContact.socMedia.facebook': {
$exists: true
}
},
project: {
'customer.customerContact': 1,
'customer.customerName': 1,
totalPrice: 1
},
sort: {
'customer.custonerName.last': 1,
'customer.custonerName.first': 1
},
limit: 6
}
We can use this to create a script that produces the total revenue for customers who are on Facebook. As with the scripts you have reviewed in earlier chapters, we start with a set of import statements. We also create an instance of BookingService:
import os,sys
sys.path.append(os.path.realpath("src"))
import pymongo
from config.config import Config
from booksomeplace.domain.booking import BookingService
service = BookingService(Config())
We are then able to use the code captured by MongoDB Compass to formulate parameters for the query:
total = 0
query = {'customer.customerContact.socMedia.facebook': {'$exists': True}}
proj = {'customer.customerContact': 1, \
'customer.customerName': 1,'totalPrice': 1}
sort = [('customer.customerName.last',pymongo.ASCENDING), \
('customer.customerName.first',pymongo.ASCENDING)]
result = service.fetch(query, proj, sort)
After that, it's a simple matter of displaying the results and calculating the total revenue:
pattern = "{:12}\t{:40}\t{:8.2f}"
print('{:12}\t{:40}\t{:8}'.format('Name','Facebook Email','Amount'))
for doc in result :
name = doc['customer']['customerName']['first'][0] + ' '
name += doc['customer']['customerName']['last']
email = doc['customer']['customerContact']['socMedia']['facebook']
price = doc['totalPrice']
total += price
print(pattern.format(name, email, price))
print('{:12}\t{:40}\t{:8.2f}'.format('TOTAL',' ',total))
Here is the last part of the results produced:

In the next section, we will learn how to build queries involving embedded arrays.
Management is concerned about customer complaints regarding cleanliness. You have been instructed to produce a report on properties where more than 50% of the reviews for that property report a low cleanliness score (for example, 3 or less).
When conducting queries of any type, it's important to be aware of the database document structure. In this case, we need to examine the properties collection. Using MongoDB Compass, all you need to do is to connect to the database and select the booksomeplace database, as well as the properties collection. In the list view, you can click on the reviews field to get an idea of the structure, as shown here:

As you can see, reviews are represented as embedded arrays. Following the logic presented in the earlier sub-section in this chapter, your first thought might be to construct a query document such as this: {"reviews.cleanliness":{$lt:3}}:

This is not exactly the information we are looking for, of course, so we need to move the final query into a Python script that produces the final report. After selecting Toggle Query History, we copy the query document, as shown here:
{
filter: {'reviews.cleanliness': {$lt: 3}},
project: {propName: 1,address: 1,reviews: 1}
}
We can move the query into Python script. As before, we start with a series of import statements and create an instance of the PropertyService class from the booksomeplace.domain.service.property module:
import os,sys
sys.path.append(os.path.realpath("src"))
import pymongo
from config.config import Config
from booksomeplace.domain.property import PropertyService
service = PropertyService(Config())
Next, we formulate the query parameters and run the query off the fetch() method in the service. The cutoff variable represents the maximum cleanliness rating score specified by management:
cutoff = 3
cat = 'cleanliness'
key = 'reviews.' + cat
query = {key : {'$lt': 3}}
proj = {'propName': 1,'address':1,'reviews': 1}
sort = [('propName',pymongo.ASCENDING)]
result = service.fetch(query, proj, sort)
We can now loop through the result set and product a report with the property name, city, country, and average cleanliness score:
pattern = "{:20}\t{:40}\t{:5.2f}"
print('{:20}\t{:40}\t{:5}'.format('Property Name','Address','Score'))
print('{:20}\t{:40}\t{:5}'.format('-------------','-------','-----'))
for doc in result :
name = doc['propName']
addr = doc['address']['city'] + ' ' + doc['address']['country']
# total cleanliness scores
total = 0
count = 0
for revDoc in doc['reviews'] :
total += revDoc[cat]
count += 1
avg = 5 / (total/count)
if avg < cutoff :
print(pattern.format(name,addr[0:40], avg))
The result, in a Python /path/to/repo/chapters/09/compass_query_involving_arrays.py script, might appear as shown here. Note that we have only shown the last several lines to preserve space:

In the next section, we will learn how to use the aggregation framework.
The MongoDB aggregation framework allows you to group results into datasets upon which a series of operations can be performed. MongoDB aggregation uses a pipeline architecture whereby the results of one operation becomes the input to the next operation in the pipeline. The core of MongoDB aggregation is the collection-level aggregate() method.
The arguments to the db.<collection_name>.aggregate() method consist of a series of expressions, each beginning with a pipeline stage operator (see the next section). The output from one pipeline stage feeds into the next, in a chain, until the final results are returned. The following diagram shows the generic syntax:

Arguably, the most popular pipeline stage operators are $match, which performs a similar function to a query filter, and $group, which groups results according to the _id field specified. As a quick example, before getting into more details, note the following command, which produces a revenue report by country.
The $match stage allows documents where paymentStatus in the bookingInfo embedded object is confirmed. The $group stage performs grouping by country. In addition, the $group stage uses the $sum pipeline expression operator to produce a sum of the values in the matched totalPrice fields.
Finally, the $sort stage reorders the final results by the _id match field (that is, the country code). Here is the query:
db.bookings.aggregate([
{ $match : { "bookingInfo.paymentStatus" : "confirmed" } },
{ $group: { "_id" : "$customer.customerAddr.country",
"total" : { $sum : "$totalPrice" } } },
{ $sort : { "_id" : 1 } }
]);
Here is the output from the mongo shell:

Let's now look at the pipeline stage operators.
You can create multiple stages within the aggregation by simply adding a new document to the list of arguments presented to the aggregate() method. Each document in the list must start with a pipeline stage operator. Here is a summary of the more important pipeline stage operators:
| Operator | Notes |
| $bucket | Creates sub-groups based on boundaries, which allow further, independent processing. As an example, you could create a revenue report with buckets consisting of $0.00 to $1.000, $1,001 to $10,000, $10,001 to $100,000, and more. |
| $count | Returns the number of documents at this stage in the pipeline. |
| $geoNear | Used for geospatial aggregation. This single operator internally leverages $match, $sort, and $limit. |
| $group | Produces a single document that has an accumulation of all the documents with the same value for the field identified as _id. |
| $lookup | Incorporates documents from other collections in a similar manner to an SQL LEFT OUTER JOIN. |
| $match | Serves exactly like a query filter: eliminates documents that do not match the criteria. |
| $merge | Allows you to output to a collection in the same database or in another database. You can also merge results into the same collection that is being aggregated. This also gives you the ability to create on-demand materialized views. |
| $out | This operator is similar to $merge except that it's been available since MongoDB 2.6. Starting with MongoDB 4.4, this operator can now output to a different database, whereas with MongoDB versions 4.2 and below, it had to be the same database. Unlike $merge, this operator cannot output to a sharded collection. |
| $project | Much like the projection in a standard MongoDB query, this pipeline stage operator allows you to add or remove fields from the output stream at this stage in the pipeline. It allows the creation of new fields based on information from existing fields. |
| $sort | Reorders the documents in the output stream at this stage in the pipeline. Has the same syntax as the sort used in a standard MongoDB query. |
| $unionWith | With this operator, the pipeline stage combines the output of two collections into a single output stream. It allows you to create the equivalent of the following SQL statement: SELECT * FROM xxx WHERE UNION ALL SELECT * FROM yyy |
| $unwind | Deconstructs embedded arrays, sending a separate document for each array element to the output stream at this stage in the pipeline. |
Examples of these pipeline stages are used throughout the remainder of this chapter. Before we get to the fun stuff, it's also important to have a quick look at the pipeline expression operators, which can be used within the various pipeline stages.
Pipeline expression operators perform the various operations needed at any given pipeline stage you have defined. The two most widely used operators fall into the date and string categories. Let's start with the string expression operators.
Although just one set of operators among many, the string operators probably prove to be the ones most frequently used when expression manipulation needs to occur. Here is a summary of the more important of these operators:
| Operator | Notes |
| $concat | Concatenates two or more strings together. |
| $trim | Removes whitespace, or other specified characters (for example, CR\LF) from both the beginning and the end of a string. This family also includes $ltrim and $rtrim, which trims only to the left or right, respectively. |
| $regexMatch | New in MongoDB 4.2, returns TRUE or FALSE if the string matches the supplied regular expression. Also in this family are $regexFind, which returns the first string that matches a regular expression, and $regexFindAll, which returns all the strings that match the regex. |
| $split | Splits a string based on a delimiter into an array of substrings. |
| $strLenBytes | Returns the number of UTF-8-encoded bytes. Note that UTF-8-encoded characters may be encoded using up to 4 bytes per character. Thus, a word of three characters in Chinese might return a value of 9 or 12, whereas a word of three characters using a Latin-based alphabet might only return a value of 3. |
| $substrCP | Returns a substring that starts at the specified UTF-8 code point index (zero-based) for the number of UTF-8-encoded characters indicated by code point count. |
| $toUpper, $toLower | These operators produce a string of all-UPPER or all-lower case characters. |
As an example, using aggregation pipeline string operators, let's say that as part of a report, you need to produce an alphabetical listing of a customer's name in this format:
title + first name + middle initial + last name
The first consideration is that we need the $concat string operator in order to combine the four name attributes of the title and the first, middle, and last names. We also need $substrCP in order to extract the first letter of the middle name, as well as $toUpper. Finally, $cond (discussed later) is needed in case either the title or middle name is missing.
Here is how the query might be formulated. We start with the $project pipeline stage operator, and postulate two new fields. The last field contains the value of the last name, and is suppressed in the last state. The full_name field is our concatenation:
db.customers.aggregate([
{ $project: {
last : "$name.last",
full_name: { $concat: [
We then describe four expressions. The first and third include a condition, and only return a value if that field contains a value. To obtain the middle initial from the middle name field (that is, the first letter of a person's middle name, if there is one), we also apply $substrCP to extract the first letter, and make it uppercase. You might also note that in the case of the title and middle name, we do another concatenation to add a space:
{ $cond : {
if: "$name.title",
then: { $concat : ["$name.title", " "] },
else: "" } },
"$name.first", " ",
{ $cond : {
if: "$name.middle",
then: { $concat:[{ \
$toUpper : {$substrCP:[ \
"$name.middle",0,1]}},"."]},
else: "" } },
"$name.last" ]
} } },
In the next stage, we use the $sort pipeline operator to alphabetize the list. In the last stage, we use another $project operator to suppress the last name field, which is needed by the previous stage:
{ $sort : { "last" : 1 } },
{ $project : { cust_name : "$full_name" } } ]);
Here are the first 20 results of the operation:

You can view the entire query in the aggregation_examples.js file in /path/to/repo/chapters/09.
As you have seen in other examples in this chapter and earlier chapters, it is always possible to work with date strings. The problem with this approach, however, is that the data you have to work with might not be clean in that different date formats may have been used. This is especially true in situations where the data has accumulated over a period of time, perhaps from different versions of the web application.
Another problem when trying to perform operations on dates as strings is that performing date arithmetic becomes problematic. You will learn more about this later in this chapter when it comes time to present a solution for generating a 30-60-90 day aging report, which is a typical requirement for accounting purposes.
For the purposes of this sub-section, we will examine the more important date expression operators. There are two operators that create BSON date objects:
The format string follows ISO standards, and defaults to the following if not specified:
"%Y-%m-%dT%H:%M:%S.%LZ"
Thus, using this format, 10:02 the morning of January 1, 2020, would appear as follows, where Z indicates Zulu time (that is, UTC time):
"2020-01-01T10:02:00.000Z"
You can also reverse the process using either $dateToString or the $dateToParts operator. Once you have a BSON date object, use these operators to produce its component parts: $year, $month, $dayOfMonth, $hour, $minute, $second, and $millisecond.
Here is a brief summary of other important pipeline expression operators:
| Category | Operator | Notes |
| Arithmetic | $add, $subtract, $multiply, $divide, and $round |
Performs the associated arithmetic operation to returns sums of numbers, and so on. Note that $add and $subtract also work on dates. It should be noted that $round is only available as of MongoDB 4.2. |
| Array | $arrayElemAt, $first, and $last |
Returns the array element at a specified index or the first/last elements. $first and $last were introduced in MongoDB 4.4. |
| $arrayToObject and $objectToArray | Converts between an array and object. | |
| $in | Returns a Boolean value: TRUE if the item is in the array and FALSE otherwise. | |
| $map | Applies an expression to every array element. | |
| $filter | Returns a subset of array items matching the filter. | |
| Boolean | $and, $or, and $not | Lets you create complex expressions. |
| Comparison | $eq, $gt, $gte, $lt, $lte, $ne, and $cmp | Provides conditions for filtering results, corresponding to equals, greater than, greater than or equals, and so on. The last operator, $cmp, produces -1 if op1 < op2, 0 if equal, and +1 if op1 > op2. |
| Conditional | $cond | Provides ternary conditional expressions in the following form: { $cond : [ <expression>, <value if TRUE>, <value if FALSE> ] } |
| $ifNull | Provides the first non-null value for the specified field; otherwise, a default you provide. | |
| $switch | Implements a C language-style switch block. | |
| Literal | $literal | Allows you to return a value directly as is, with no modifications. |
| Object | $mergeObjects | Combines objects into a single document. |
| Set | $setDifference, $setUnion, and $setIntersection | These operators treat arrays as sets, allowing manipulations on the entire array. |
| Text | $meta | Gains access to text search metadata. |
| Trigonometry | $sin, $cos, and $tan | First introduced in MongoDB 4.2, this group of operators provides trigonometric functionality, such as sine, cosine, tangent, and so on. |
| Type | $convert, $toDate, $toInt, and $toString | First introduced in MongoDB 4.0, these operators perform data type conversions. |
Another group of operators, referred to as accumulators, are used to collect results such as producing totals. These operators are generally tied to specific aggregation pipeline stages, including $addFields, $group, and $project. Here is a brief summary of the more important accumulators associated with the $group, $addFields, and $project pipeline stages:
| Operator | Notes |
| $sum | Produces a sum of one of the fields in the group. The sum can also be produced from an expression, which might involve additional fields and/or operators. Any non-numeric values are ignored. |
| $function | Allows you to define a JavaScript function, the results of which are made available to this pipeline stage. Only available as of MongoDB 4.4, this expression operation completely eliminates the need for Map-Reduce. |
| $accumulator | Introduced in MongoDB 4.4, this operator lets you include a JavaScript function to produce results, making this an extremely valuable general-purpose accumulating operator. |
| $avg | Produces an average of a field or expression from within the group. Any non-numeric values are ignored. |
| $min | Returns the minimum value of a field or expression from within the group. |
| $max | Returns the maximum value of a field or expression from within the group. |
| $push | Returns the value of a field or expression of all the documents within the group in the form of an array. |
| $stdDevPop | Used in statistical operations, this operator returns the population standard deviation of a field or expression from within the group. |
| $stdDevSamp | Used in statistical operations, this operator returns the sample standard deviation of a field or expression from within the group. |
In addition, these operators are available only in the $group pipeline stage: $addToSet, $first, $last, and $mergeObjects.
In the next section, we will examine a more traditional way of handling aggregation; namely, using Map-Reduce functions.
Prior to the introduction of the aggregation framework, in order to perform grouping and other aggregation operations, database users used a less efficient method: db.collection.mapReduce(). This command has been a part of the MongoDB command set since the beginning, but is more difficult to implement and maintain and suffers from relatively poor performance compared with similar operations using the aggregation framework. Let's now have a look at the mapReduce() method.
The primary method that allows you to perform aggregation operations using a JavaScript function is mapReduce(). The generic syntax for the collection method is this:
db.<collection>.mapReduce(<map func>,<reduce func>, \
{query:{},out:"<new collection>"})
The first argument is a JavaScript function representing the map phase. In this phase, you define a JavaScript function that calls emit(), which defines the fields included in the operation. The second argument is a JavaScript function that represents the reduce phase. A typical use for this is to produce some sort of accumulation (for example, sum) on the mapped fields.
The third argument is a JSON object with two properties: query and out. query, as you might have guessed, is a filter that limits documents included in the operation. out is the name of a collection into which the results of the operation are written.
An even more flexible set of options is available when using db.runCommand(). Here is the alternative syntax:
db.runCommand( {
mapReduce: <collection>, map: <function>,
reduce: <function>, finalize: <function>,
out: <output>, query: <document>, sort: <document>, limit: <number>,
scope: <document>, jsMode: <boolean>, verbose: <boolean>,
bypassDocumentValidation: <boolean>, collation: <document>,
writeConcern: <document>
});
You can see that when using db.runCommand(), an additional function, finalize, is available, which is executed after the map and reduce functions.
A very simple example is enough to give you an idea of how this command works. Let's say that management wants a revenue report for 2019 with a sum for each country. First, we need to define the JavaScript map function. This function emits the country code and total price for each document in the bookings collection:
map_func = function () { \
emit(this.property.propertyAddr.country, this.totalPrice); }
Next, we define the reduce function. This function accepts the two values that are emitted, and produces a sum of the total prices paid aggregated by country:
reduce_func = function (key, prices) { return Array.sum(prices); }
Next, we define the document used as a query filter, as well as the target collection into which the resulting information is written:
query_doc = {"bookingInfo.paymentStatus":"confirmed", "bookingInfo.arrivalDate":/^2019/}
output_to = "country_totals"
We are now ready to execute the command using mapReduce() as a collection method:
db.bookings.mapReduce(map_func, reduce_func, {query:query_doc, out:output_to });
Alternatively, we can run this command using db.runCommand(), as follows:
sort_doc = { "property.propertyAddr.country" : 1 }
db.runCommand( {
mapReduce: "bookings",
map: map_func,
reduce: reduce_func,
out: output_to,
query: query_doc,
sort: sort_doc
});
As mentioned earlier, the main difference when using runCommand() is that other options, such as sort, are available. Once either command is executed, the result is written to a new collection, country_totals. Here are the first 20 results:

The complete Map-Reduce query is in the /path/to/repo/chapters/09/map_reduce_example.js file.
It's important to note that any query that breaks down revenue by differing criteria forms an important part of risk analysis. In this example, revenue totals by country can identify weak financial returns. Any investment in infrastructure in a geographic area of weak return would be considered a risk.
Let's now have a look at handling geospatial queries.
In order to conduct geospatial queries, you must specify one or more pairs of coordinates. For real-world data, this often translates into latitude and longitude. Before you go too much further, however, you need to stop and carefully consider what sorts of geospatial queries you plan to perform. Although all current MongoDB geospatial queries involve one or more sets of two-dimensional X,Y coordinates, there are two radically different models available: flat and spherical.
We begin our discussion by covering the flat 2D model, which performs queries on legacy coordinate pairs (for example, latitude and longitude).
In order to perform geospatial searches, we first need to prepare the database by creating the necessary geospatial fields from our existing latitude and longitude information. We then need to define an index on the newly added field.
Creating a new field from existing ones is another opportunity to use the aggregation framework. In earlier versions of MongoDB, it was necessary to create a JavaScript function that looped through the database collection and updated each document individually. As you can imagine, this sort of operation was enormously expensive in terms of time and resources needed. Essentially, two commands had to be performed for every document: a query followed by an update.
As the name implies, the flat 2D MongoDB geospatial model is used for situations where a street map is appropriate. This model is perfect for giving directions to properties, or for locating stores within a certain geographic area, and so forth.
Using aggregation we can consolidate the geospatial field creation into a single command. It is important to note that the current values for latitude and longitude are stored as a data type string, and hence are unusable for the purposes of geospatial queries. Accordingly, we employ the $toDouble pipeline expression operator.
Here is the command that adds a new field, geo_spatial_flat, consisting of legacy coordinate pairs, from existing latitude and longitude coordinates in the world_cities collection:
db.world_cities.aggregate(
{ "$match": {} },
{ "$addFields": {
"geo_spatial_flat" : [
{ "$toDouble" : "$longitude" },
{ "$toDouble" : "$latitude" } ]
}
},
{ "$out" : "world_cities" }
);
This command uses three stages: $match, $addFields, and $out:
As the collection identified by the $out stage is the same as the source of the data in our example, the effective result is the same collection with a new field added to each document.
Here is how the new field appears:

We will next have a look at indexing.
Before conducting a geospatial query, you need to create a 2D index on this field. Here is the command we use to create a flat 2D index on the newly created geospatial field:
db.world_cities.createIndex( { "geo_spatial_flat" : "2d" } );
Here is a screenshot of this operation:

Before we examine the spherical model, it's important to discuss GeoJSON objects.
GeoJSON objects are needed in order to conduct geospatial queries involving a spherical model. These object types are modeled after RFC 7946, The GeoJSON Format, published in August 2016. Here is the generic syntax for creating a MongoDB GeoJSON object:
<field>: { type: <GeoJSON type> , coordinates: <coordinates> }
Please note that, depending on the GeoJSON object type, there may be more than one pair of coordinates. MongoDB supports a number of these objects, summarized here:
<field>: { type: "Point", coordinates: [ x, y ] }
<field>: { type: "LineString", coordinates: [[x1,y1], [x2,y2]] }
<field>: { type: "Polygon", coordinates: [[x1,y1],[x2,y2],[x3, y3], etc., [x1,y1]]}
<field>: { type: "Polygon", coordinates: [
[[x1,y1],[x2,y2],[x3, y3], etc. ],
[[xx1,yy1],[xx2,yy2],[xx3, yy3], etc. ] ] }
The spherical 2D MongoDB geospatial model is used for situations where coordinates need to be located on a sphere. An obvious example would be creating queries that provide locations on planet Earth, the moon, or other planetary bodies. Here is the command that creates a GeoJSON object from existing coordinates in the properties collection. Again, please note the use of $toDouble to convert latitude and longitude values from string to double:
db.properties.aggregate(
{ "$match": {} },
{
"$addFields": {
"address.geo_spatial":{
"type":"Point",
"coordinates":
[
{ "$toDouble" : "$address.longitude" },
{ "$toDouble" : "$address.latitude" }
]
}
}
},
{ "$out" : "properties" }
);
This query is identical to the one described a few sections earlier, except for the addition of geospatial fields. We are then in a position to create an index to support geospatial queries based on the 2D spherical model:
db.properties.createIndex( { "address.geo_spatial" : "2dsphere" } );
After having created a GeoJSON field from the latitude and longitude fields for the world_cities collection, we also need to do the same for the properties collection.
MongoDB geospatial query operators are used to perform queries based on geographical or spatial sets of coordinates. As noted previously, only two operators support the flat model; however, all geospatial operators support the spherical model. Here is a brief summary of the MongoDB geospatial query operators:
| Operator | Spherical | Flat | Notes |
| $near | Y | Y | Returns documents in order of nearest to furthest from the point specified using the $geometry parameter. You can also supply two additional parameters, $minDistance and $maxDistance, in order to limit the number of documents returned. You can use either a 2D or a 2Dsphere index. |
| $nearSphere | Y | N | Operates exactly like $near, but uses a 2Dsphere index and only follows the spherical model. |
| $geoWithin | Y | Y | Accepts a $geometry argument with type and coordinates keys. The type can be either Polygon or MultiPolygon. |
| $geoIntersects | Y | N | This operator accepts the same arguments and keys as $geoWithin but operates on the spherical model. |
As an example, let's say that we produced a list of Book Someplace properties in India using this query:
db.properties.aggregate(
{ "$match" : { "address.country" : "IN" } },
{ "$project" : { "address.city" : 1, "address.geo_spatial" : 1, \
"_id" : 0 } },
{ "$sort" : { "address.city" : -1 } }
);
We now wish to obtain a list of world cities within 100 kilometers of this property. There is no need to add the country code as find criteria, as we are already dealing with geospatial coordinates.
Here is the query (note that $maxDistance is in meters):
db.world_cities.find(
{ "geo_spatial_sphere" :
{ "$near" : {
"$geometry" : {
"type" : "Point",
"coordinates" : [77.8956, 18.0806]
},
"$maxDistance" : 100000
}
}
},{ "name" : 1, "geo_spatial_sphere" : 1, "_id" : 0 });
Here is the resulting output:

Having seen the use of geospatial operators in a query, let's examine their use in an aggregation pipeline stage.
The $geoNear aggregation operator supports geospatial operations within the context of the aggregation framework. The $geoNear operator can be used to form a new pipeline stage, upon which geospatial data comes into play. In this case, documents are placed in the pipeline by the indicated GeoJSON field in order of nearest to furthest from a given point.
Here is a brief summary of the more important parameters available for this operator:
| Option | Notes |
| near | If the field used has a 2dsphere index, you can use either a GeoJSON object of the point type or a list of legacy coordinate pairs (for example, [ <longitude>,<latitude>]) as an argument. On the other hand, if the field only has a flat 2D index, you can only supply a legacy coordinate pair list as an argument. |
| spherical | When set to True, this parameter forces MongoDB to use a spherical model of calculation. Otherwise, if set to the default of False, the calculation model used depends on what type of index is available for the geospatial field. |
|
minDistance maxDistance |
Lets you set a minimum and maximum distance, which is extremely useful in limiting the number of documents in the resulting output. If you are using a geospatial field with a GeoJSON object, the distance is set in meters. Otherwise, when using legacy coordinate pairs, the distance can only be set in radians. |
|
key |
This parameter represents the name of the geospatial field you plan to use to calculate the distance. Although technically optional, you run into problems if you do not set a value for this parameter and the operation occurs on a collection with multiple geospatial indexes. |
|
distanceField |
The value you supply for this parameter is the name of a field that is added to the pipeline output and contains the calculated distance. |
Now that you have an understanding of working with geospatial data in MongoDB, it's time to have a look at generating financial reports.
Most financial reports are fairly simple in terms of the database queries and code needed to present the report. Management reports requested tend to fall into one of the following categories:
All three reports form tools that help you perform risk analysis. Let's start with revenue reports.
Revenue reports, detailing the money received, are most often requested based on certain periods of time. For example, you might be asked to produce a report that provides a summary of the revenue generated over the past year, with subtotals for each quarter.
Revenue reports are an important part of risk analysis. In the financial community, a risk can be defined as an investment that causes the company to lose money. Revenue reports, at their core, are just a total of money coming into the company broken down by time, location, or customer demographic information.
The criteria used in generating the report can highlight crucial areas of weakness, and thus risk. For example, if you generate a report by location, your company may discover that revenue is lower in certain regions than in others. If your company is investing heavily in a certain region but is generating little revenue in return, your report has identified a risk. Management can then use this information to reallocate advertising in an effort to improve revenue from the weak region. If a later revenue report reveals that the revenue has not increased given an increased advertising budget, management may decide to either seek a different sales and marketing strategy or perhaps withdraw from the region entirely.
In this example, we will generate a revenue report of customer reservations by room type. We will start by using the mongo shell to build a query.
We will start by modeling the query, either using MongoDB Compass or directly from the mongo shell. In order to run this query, we need information from the bookings collection's bookingInfo field (described in Chapter 7, Advanced MongoDB Database Design). You can see that there is often a twist. In this case, the twist is that we must be sure to only include bookings where the payment status is confirmed.
Query criteria are grouped together using the $and query operator. We use a regular expression to match the target year of 2018. In addition, we add a condition that specifies that only documents where the payment status is confirmed are matched. It is also worth mentioning at this point that in order to reference a property of an embedded object, you should use a dot (.) as a separator. Thus, in order to match arrivalDate, which is a property of the object represented by the bookingInfo field, we use this syntax:
bookingInfo.arrivalDate.
Here is a query that produces the data needed to generate the report for 2019:
db.bookings.find(
{ "$and":[
{ "bookingInfo.arrivalDate":{"$regex":"^2019"},
"bookingInfo.paymentStatus":"confirmed"
}
] },
{"totalPrice":1,"bookingInfo.arrivalDate":1}
);
This query, however, simply produces a block of data and places the burden of breaking down the information and producing totals on the Python script. With some thought, using the query shown previously as a base, it's easy enough to modify the query using aggregation.
The $match pipeline stage restricts the number of documents in the pipeline to only those where the date begins with the target year – in this case, 2019. Here is the revised query:
db.bookings.aggregate( [
{ $match :
{ "$and":[
{ "bookingInfo.arrivalDate":{"$regex":"^2019"},
"bookingInfo.paymentStatus":"confirmed"
}
] }
}
}]);
If you take a quick look at a couple of bookings, however, you'll find that bookings are by room type, and that room types are organized into an array of RoomType objects. Here is an example from the mongo shell:

In order to access information by room type, we'll first need to unwind the array. What this pipeline operator does is create duplicate documents, one for each array element, effectively flattening the array. As an example, consider this query:
db.bookings.aggregate( [
{$unwind : "$rooms"},
{$project: {"bookingKey":1,"rooms":1,"_id":0}}
]);
Now have a look at the output:

As you can see, the original booking documents are duplicated; however, only one room type is represented. We can then modify the model query by adding three new stages: $unwind, $project, and $sort, as shown here:
{ $unwind : "$rooms" },
{ $project : { "rooms" : 1 }},
{ $sort : { "rooms.roomType" : 1 }}
You can see that following the $unwind pipeline stage, we then limit the fields to rooms only using $project. After that, we sort by room type using $sort. The only thing left to do is to group by room type and add totals. Here are the additional stages:
{ $addFields :
{ "total_by_type" : { $multiply : [ \
"$rooms.price", "$rooms.qty" ] }}
},
{ $group : {
_id : { "type" : "$rooms.roomType" },
"count": { $sum: "$rooms.qty"},
"total_sales": { $sum: "$total_by_type"}}
}
Before implementing the Python script that generates the revenue report, we must make a simple addition to the BookingService class, which is in /path/to/repo/chapters/09/src/booksomeplace/domain/booking.py module. The new method, which we will call fetchAggregate(), accepts an aggregation pipeline document as an argument and leverages the pymongo collection-level aggregate() method. Here is the new method:
def fetchAggregate(self, pipeline) :
return self.collection.aggregate(pipeline)
We can now use the model query (just discussed) in a Python script, financial_query_sales_by_room_type.py.py, to produce the desired results. You can see the full script in the /path/to/repo/chapters/09 directory. As usual, we start with imports and create a BookingService instance:
import os,sys
sys.path.append(os.path.realpath("src"))
import pymongo
from config.config import Config
from booksomeplace.domain.booking import BookingService
service = BookingService(Config())
Next, pass the pipeline document as an argument to the newly defined domain service method, aggregate(). Here, in this code block, you can see exactly the same query as shown previously, but incorporated as a Python dictionary:
target_year = 2019
pipeline = [
{ '$match' :
{ '$and':[
{ 'bookingInfo.arrivalDate':{'$regex':'^' + str(target_year)},
'bookingInfo.paymentStatus':'confirmed'
}
] }
},
{ '$unwind' : '$rooms' },
{ '$project' : { 'rooms' : 1 }},
{ '$sort' : { 'rooms.roomType' : 1 }},
{ '$addFields' :
{ 'total_by_type':{'$multiply':['$rooms.price','$rooms.qty']}}
},
{ '$group' : {
'_id' : { 'type' : '$rooms.roomType' },
'count': { '$sum' : '$rooms.qty'},
'total_sales': { '$sum' : '$total_by_type'}}
}
]
result = service.fetchAggregate(pipeline)
As you can see, the pipeline dictionary is identical to the JSON structure used when modeling the query using the mongo shell.
We then loop through the query results and display the totals by room type, with a grand total at the end:
total = 0
count = 0
pattern_line = "{:10}\t{:6}\t{:10.2f}"
pattern_text = "{:10}\t{:6}\t{:10}"
print('Sales by Room Type For ' + str(target_year))
print(pattern_text.format('Type','Qty','Amount'))
print(pattern_text.format('----------','------','----------'))
for doc in result :
label = doc['_id']['type']
qty = doc['count']
amt = doc['total_sales']
total += amt
count += qty
print(pattern_line.format(label,qty,amt))
# print total
print(pattern_text.format('==========','======','=========='))
print(pattern_line.format('TOTAL',count,total))
Here is how the report might appear:

Let's now have a look at a class financial report: the 30-60-90 day aging report.
Aside from money received financial reports, one of the next biggest concerns for management is how much is still owed. Furthermore, management often requests aging reports. These reports detail the money owed broken down by time increments (for example, 30 days). One such report is referred to as a 30-60-90 day aging report.
Aging reports are an important part of risk analysis. If the percentage of the amount owed past 90 days is excessive compared with 30 or 60 days, a risk has been identified. It could be that a financial crisis is looming in one or more areas where you do business. Another reason might be that your company's customers are unable (or perhaps unwilling!) to pay promptly. If further investigation reveals that customers are unwilling to pay promptly, this could indicate a problem with quality control, or a failure to deliver products in time (depending on the nature of your business).
In the context of Book Someplace, this type of report for a hotel booking is not very important as most bookings are made within a 30-day window. However, for the purposes of illustration, we have included in the sample data booking entries where the payment is still pending, furnishing us with enough data to generate a decent 30-60-90 day aging report.
We can now apply this query to a Python script that uses the pymongo.collection.find() method. The full script is named financial_30_60_90_day_aging_report_using_aggregation.py and can be found at /path/to/repo/chapters/09.
As before, we first define the import statements and create an instance of the BookingService class from the booksomeplace.domain.booking module:
import os,sys
sys.path.append(os.path.realpath("src"))
import pymongo
from datetime import date,timedelta
from config.config import Config
from booksomeplace.domain.booking import BookingService
service = BookingService(Config())
Next, we stipulate the target date from which we work backward 30, 60, and 90 days:
year = '2020'
target = year + '-12-01'
now = date.fromisoformat(target)
aging = {
'30' : (now - timedelta(days=30)).strftime('%Y-%m-%d'),
'60' : (now - timedelta(days=60)).strftime('%Y-%m-%d'),
'90' : (now - timedelta(days=90)).strftime('%Y-%m-%d'),
}
We now formulate a Python dictionary that forms the basis of the $project stage in the aggregation operation. In this stage, we allow three fields to be pushed into the pipeline: bookingInfo.arrivalDate, bookingInfo.paymentStatus, and totalPrice. Note that this formulation allows us to effectively create an alias for the first two fields, which makes the remaining stages easier to read:
project = {
"$project" : {
"arrivalDate" : "$bookingInfo.arrivalDate",
"paymentStatus" : "$bookingInfo.paymentStatus",
"totalPrice" : 1,
}
}
We then create another dictionary to represent the $match stage. In this example, we wish to include booking documents where the payment status is pending, the date is in the year 2019, and the date is less than our target date of 2019-12-01:
match = {
"$match" : {
"$and":[
{ "paymentStatus" : "pending" },
{ "arrivalDate" : { "$regex" : "^" + year }},
{ "arrivalDate" : { "$lt" : target }}
]
}
}
Although not technically needed, we include a $sort stage so that if we do need to examine the data in detail, the dates are sorted, making it easier to confirm the report totals:
sort = { "$sort" : { "arrivalDate" : 1 }}
Finally, the most complicated stage is $bucket. In this stage, we group the dates by arrivalDate (an alias for bookingInfo.arrivalDate) and set boundaries that represent 30, 60, and 90 days. The default parameter represents the bucket that contains data outside of the boundaries:
bucket = {
"$bucket" : {
"groupBy" : "$arrivalDate",
"boundaries" : [ aging['90'], aging['60'], aging['30'], target ],
"default" : target,
"output" : {
"totals" : { "$sum" : "$totalPrice" },
"amounts" : { "$push" : "$totalPrice" },
"dates" : { "$push" : "$arrivalDate" }
}
}
}
The actual execution of the aggregation is absurdly simple: just two lines of code!
pipeline = [ project, match, sort, bucket ]
result = service.fetchAggregate(pipeline)
Before the main loop, we initialize some variables:
days = 90;
total = 0.00
dbTot = 0.00
pattern_line = "{:20}\t{:10.2f}\t{:10.2f}"
We then loop through the results, consisting of one document per bucket:
for doc in result :
if doc['_id'] == target :
amt_plus = doc['totals']
dbl_plus = sum(doc['amounts'])
else :
label = str(days)
amt = doc['totals']
dbl = sum(doc['amounts'])
total += amt
dbTot += dbl
days -= 30
print(pattern_line.format(label,amt, dbl))
print(pattern_line.format('TOTAL',total,dbTot))
This screenshot gives you an idea of the output, with labels added for convenience:

Next, we will have a look at generating a trend report that can assist in risk analysis.
The last category we will cover here is that of revenue trends. These reports are used by management to make decisions on where to invest money in marketing, as well as where to cut funds from non-productive initiatives. Revenue trend reports can be based on actual historic data, projections of future data, or a combination of the two. When generating revenue trends for the future, it's a good idea to first plot a graph of the actual historic data. You can then apply the graph to predict the future revenue.
When analyzing historic data in order to predict future data, be careful to spot and avoid anomalies. These are factors that are one-time events that have a significant impact on a particular trend, but that cannot be counted on in the future. An example might be a worldwide pandemic. This would obviously cause a significant drop in bookings for properties in the area affected by the outbreak. However, even though these properties might be in a hot zone, an event such as this could (hopefully!) be considered an anomaly, and the historic data that encapsulates the timespan between the outbreak occurrence and the recovery period should be weighted when predicting potential future revenue.
Any anomalies play an important part in overall risk analysis. Downward revenue trends identify a risk. If your projection is accurate, you can help your company avoid investing in a potentially disastrous risky region or demographic.
Since the output of this report is a line chart, we start by defining a Django app for that purpose. The app is located in the /path/to/repo/www/chapter_09/trends folder. We then add a reference to /path/to/repo/www/chapter_09/booksomeplace_dj/urls.py, such that the /trends/future URL points to the new Django app. In the same file, urls.py, we add another route for a view method that responds to an AJAX request, /trends/json:
from django.urls import path
from . import views
urlpatterns = [
path('future/', views.future, name='trends.future'),
path('future/json/', views.futureJson, name='trends.future_json'),
]
Let's now define a form to capture query parameters.
Next, in /path/to/repo/www/chapter_09/trends/forms.py, we define a form class, TrendForm, which captures the query parameters used to formulate the revenue trends report. The fields that are defined here are ultimately used to form a query filter. The trend_region field could be used to include states, provinces, or even countries, such as England in the UK. The last field, trend_factor, is used for the purposes of future revenue projection. The initial value is .10, which represents revenue growth of 10%. The step attribute is .01, and represents the increment value for this number field.
We are now in a position to define view methods, entered into /path/to/repo/www/chapter_09/trends/views.py. The first method defined is future(). It defines initial form values, creates a TrendForm instance, renders it using the trend.html template, and injects the resulting HTML into the layout. The only other method is futureJson(), which responds to an AJAX request. Its main job is to make a call to the BookingService class instance, which is in the booksomeplace.domain.booking module:
def futureJson(request) :
import json
bookSvc = BookingService(Config())
params = {
'trend_city' : None,
'trend_region' : 'England',
'trend_locality' : None,
'trend_country' : 'GB',
'trend_factor' : 0.10
}
if request.method == 'POST':
params = request.POST
data = bookSvc.getTrendData(params)
return HttpResponse(json.dumps(data), content_type='application/json')
The response from the getTrendData() method is a list of revenue totals, both past and estimated future, which are mapped into a line chart over a span of months.
After the logic used to calculate trend data, the next most difficult task is defining JavaScript code used to make AJAX requests for trend data. This is accomplished in the /path/to/repo/www/chapter_09/templates directory. The template filename is trend.html. Inside this template file, we add a <div> tag, where the JavaScript-generated chart resides. For the purpose of this example, we will use Chartist.js, an extremely simple-to-use JavaScript responsive chart generator that has no dependencies.
First, we need to define an HTML element into which the chart can be written:
<div class="col-md-6">
<div class="ct-chart ct-perfect-fourth"></div>
</div>
Next, we link in the stylesheet, and an override to make the X-Y axis labels easier to read:
{% block extra_head %}
<link rel="stylesheet" href="//cdn.jsdelivr.net/chartist.js/latest/chartist.min.css">
<style>
.ct-label {
color: black;
font-weight: bold;
font-size: 9pt;
}
</style>
{% endblock %}
We now import the JavaScript libraries for both jQuery and Chartist:
<script src="//cdn.jsdelivr.net/chartist.js/latest/chartist.min.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
Next, we define the JavaScript doChart() function, which actually draws the chart:
<script type="text/javascript" charset="utf8">
function doChart(data)
{
var plot = { labels:data.labels, series:data.series };
new Chartist.Line('.ct-chart', plot);
}
Another JavaScript function we define is ajaxCall(), which makes the jQuery AJAX request. In this method, we define the request as an HTTP POST request, identify the target URL, and specify the data type as JSON. We then use jQuery to retrieve the current value of the form fields discussed earlier. Upon success, we call the doChart() function, which draws the chart. The ajaxCall() function is shown here:
function ajaxCall() {
$.ajax({
type: "POST",
url: "/trends/future/json/",
dataType: 'json',
data: {
'trend_city' : $('#id_trend_city').val() ,
'trend_region' : $('#id_trend_region').val() ,
'trend_locality' : $('#id_trend_locality').val() ,
'trend_country' : $('#id_trend_country').val() ,
'trend_factor' : $('#id_trend_factor').val() ,
},
success: function (data) { doChart(data); }
});
}
Finally, we tie the ajaxCall() function into a document ready() function call, which runs ajaxCall() when the document loads:
$(document).ready(function() {
ajaxCall();
$("#id_trend_city").change(function() { ajaxCall(); });
$("#id_trend_region").change(function() { ajaxCall(); });
$("#id_trend_locality").change(function() { ajaxCall(); });
$("#id_trend_country").change(function() { ajaxCall(); });
$("#id_trend_factor",).change(function() { ajaxCall(); });
});
</script>
Subsequently, if any of the parameter field values change, ajaxCall() is called again to redraw the chart.
In the BookingService class, located in /path/to/repo/chapters/09/src/booksomeplace/domain/booking.py, we define a method, getTrendData(), which returns historic and future data based on query parameters. Next, we formulate the base query filter, as well as the desired projection and sort criteria:
query = {'bookingInfo.paymentStatus':'confirmed'}
projection = {'bookingInfo.arrivalDate':1,'totalPrice':1}
sort = [('bookingInfo.arrivalDate',1)]
We now build onto the query dictionary by checking to see whether parameters are set:
if 'trend_city' in params and params['trend_city'] :
query.update({'property.propertyAddr.city':
{'$regex' : '*' + params['trend_city'] + '*' }})
if 'trend_region' in params and params['trend_region'] :
query.update({'property.propertyAddr.stateProvince': \
params['trend_region']})
if 'trend_locality' in params and params['trend_locality'] :
query.update({'property.propertyAddr.locality': \
params['trend_locality']})
if 'trend_country' in params and params['trend_country'] :
query.update({'property.propertyAddr.country': \
params['trend_country']})
We can then glue the query fragments together using the MongoDB $and query operator, and then run the query:
final_query = {'$and':[query]}
hist_data = self.fetch(query, projection, sort)
Next, we initialize the variables used in the trend data-generation process. Note that the report marks a total span of 5 years – 2 years before and 2 years after the current year:
today = datetime.now()
date_format = '%Y-%m-%d %H:%M:%S'
start_year = today.year - 2
end_year = today.year + 2
year_span = end_year - start_year + 1
years = range(start_year, end_year+1)
months = range(1,12+1)
year1_mark = (today.year - start_year - 1) * 12
year2_mark = (today.year - start_year - 2) * 12
We then prepopulate a flat list to represent a span of 60 months:
total = [0.00 for key in range(1,year_span*12)]
We now iterate through the result of the MongoDB query and build a Python datetime instance from the booking arrival date. We then calculate the index within the flat total list by multiplying year_key by 12 and adding the key for the month:
for doc in hist_data :
arrDate = datetime.strptime(\
doc['bookingInfo']['arrivalDate'], date_format)
price = doc['totalPrice']
year_key = arrDate.year - start_year
month_key = arrDate.month - 1
total[(year_key*12) + month_key] += price
Next, we grab the trend factor from the query or assign a default value of 0.10:
if 'trend_factor' in params and params['trend_factor'] :
factor = float(params['trend_factor'])
else :
factor = 0.10
Following this, we initialize lists used to represent last year's data, the difference between the last 2 years, and next year:
avg_diff = []
last_year = []
next_year = []
We then calculate the monthly difference between the past 2 years by simply subtracting the current year from the year before, month by month. We then append this figure, multiplied by factor, to produce the average difference. We also append last year's data to the last_year list:
for month in range(1,13) :
diff = total[year1_mark + month - 1] - \
total[year2_mark + month - 1]
avg_diff.append(diff * factor)
last_year.append(total[year1_mark + month - 1])
The next bit of logic is the key to generating future trend data. We take the data from last year and apply the average difference, which has been weighted by factor:
for key in range(0,12) :
next_year.append(last_year[key] + avg_diff[key])
Finally, we build lists that represent labels, and the series, which consists of 2 years of data: last year's totals and the estimated future revenue. These are returned as a single dictionary, data:
months = ['J','F','M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D']
data = {'labels' : months + months, 'series':[last_year + next_year]}
return data
The full code file can be viewed at /path/to/repo/chapters/09/src/booksomeplace/domain/booking.py.
Turning to our website, if we access http://chap09.booksomeplace.local/trends/future/, we can see the initial data that represents property bookings in England with a factor of 10%. Here is a screenshot of our work:

If we make a change to any of the fields, the chart changes. Other things that could be done at this point are the following:
As you have now learned, the three types of financial reports – revenue reports, aging reports, and revenue trend reports – are all important tools in the overall process of risk analysis. Trend reports especially benefit from a graphical representation.
In this chapter, you first learned how to use MongoDB Compass to connect to the database and to model complex queries. You then learned how to copy the resulting JSON into a Python script to run the query. Next, you learned about the MongoDB aggregation framework, consisting of a series of pipeline stage operators, which allow you to perform various aggregation operations such as grouping, filtering, and so forth. You also learned about pipeline operators, which can perform operations on the data in the pipe, including conversions, summation, and even adding new fields. You were then shown a brief section on Map-Reduce functions, which allow you to perform operations very similar to that of the aggregation framework but using JavaScript functions instead.
The next section showed you how to work with MongoDB geospatial data. You learned about GeoJSON objects, and the two types of indexes and models supported: flat or spherical. You then stepped through a sample query that listed the major cities that are within a specified kilometer radius to one of the Book Someplace properties.
Finally, in the last section, you learned about key financial reports, including revenue reports and 30-60-90 day aging reports, and generating future revenue trend charts. In order to create an interactive chart, you were shown how to integrate jQuery by making AJAX requests, which used a domain service class to return JSON data used to build the chart. It's extremely important to note that all of the report types that we have covered form an important part of the overall process of risk analysis.
In the next chapter, you will learn how to work with complex documents across multiple collections.
Section 4 introduces BigLittle Micro Finance Ltd., a financial start-up. The core business involves matching lenders with borrowers. This leads to complex relationships across documents that need to be addressed. Being a financial organization, they need to implement security. Another area of concern is to implement replication to ensure their data is continuously available. Finally, as they grow in size, the company expands to Asia, setting up an office in Mumbai, leading them to implement a sharded cluster.
This section contains the following chapters:
This chapter starts with a brief overview of the scenario used for the remaining chapters of the book. After a brief discussion on how to handle monetary data, the focus of this chapter switches to working with multiple collections. Developers who are used to formulating SQL JOIN statements that connect multiple tables are shown how to do the equivalent in MongoDB. You'll also learn how to process programming operations that require multiple updates across collections. The technique can be tricky, and this chapter outlines some of the potential pitfalls novices to MongoDB might experience. Lastly, GridFS technology is introduced as a way of handling large files and storing documents directly in the database.
The topics covered in this chapter include the following:
Minimum recommended hardware:
Software requirements:
Installation of the required software and how to restore the code repository for the book is explained in Chapter 2, Setting Up MongoDB 4.x. To run the code examples in this chapter, open a Terminal window (Command Prompt) and enter these commands:
cd /path/to/repo
docker-compose build
docker-compose up -d
docker exec -it learn-mongo-server-1 /bin/bash
To run the demo website described in this chapter, once the Docker container from Chapter 3, Essential MongoDB Administration Techniques to Chapter 12, Developing in a Secured Environment has been configured and has been started, there are two more things you need to do:
172.16.0.11 chap10.booksomeplace.local
http://chap09.booksomeplace.local/
When you are finished working with the examples covered in this chapter, return to the Terminal window (Command Prompt) and stop Docker as follows:
cd /path/to/repo
docker-compose down
The code used in this chapter can be found in the book's GitHub repository at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/chapters/10 as well as https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/www/chapter_10.
For our last use case scenario we introduce BigLittle Micro Finance Ltd. This is a fictitious online company that links borrowers with lenders. Although technically a bank, BigLittle Micro Finance Ltd. specializes in micro loans: loan amounts that do not exceed 20,000 USD. Accordingly, in order to make money, the company needs to have a much larger customer base than an ordinary commercial bank would. Borrowers could be either individuals or small businesses. Lenders, likewise, can be individuals or small businesses (or even small banks).
For the purposes of this scenario, assume the website is based on Django for reasons stated in Chapter 7, Advanced MongoDB Database Design, in the section entitled Using MongoDB with Django. Unlike the previous scenario (for example, Book Someplace Inc.), however, in the remaining chapters of this book, we do not show full Django code. Rather we provide the pertinent code examples and leave it to you to fit the code into a full Django implementation. Also, please bear in mind that the full code can be seen in the repository associated with this book.
First, let's have a look at how monetary data should be handled.
Normal floating-point data representations work well for most situations. In the case of financial operations, however, extreme precision is required. As an example, take the subject of currency conversion. If a customer is transferring money between United States Dollars(USD) and Indian Rupees (INR), for example, adding precision produces radically different results as the sums of money exchanged increase.
Here is an example. For the sake of argument, let's say that the actual exchange rate between USD and INR is the following:
ACTUAL: 1 USD = 70.64954999 INR
However, if the program application only extends precision to 4 decimal places, the rounded rate used might be this:
ROUNDED: 1 USD = 70.6495
The difference when transferring 1,000,000 USD starts to become significant:
Actual: 70649549.99
Rounded: 70649500.00
Even in the case of small amounts being exchanged, over time the differences could start to add up to significant amounts. Also, imagine the effect precision might have when calculating loan payments where the annual and effective percentage rates are stated without high precision! For the purposes of precision, MongoDB includes the NumberDecimal data type, which is based upon the BSON Decimal128 data type. This is preferred when dealing with financial data as extreme precision is required.
Let's now have a brief look at the data structures needed for BigLittle Micro Finance Ltd.
As seen with Book Someplace, Inc., we break down each collection document structure into a series of smaller classes. Accordingly, the following data structures are exactly as shown in Chapter 7, Advanced MongoDB Data Design:
The BigLittle user collection document structure includes a userType field that identifies whether the user is a borrower or a lender. Further, these two identifiers are present in the common collection under the userType key to maintain uniformity. Here is the formal JSON data structure for the user collection:
User = {
"userKey" : <string>,
"userType" : Common::userType,
"amountDue" : <NumberDecimal>,
"amountPaid" : <NumberDecimal>,
"name" : <Name>,
"address" : <Location>,
"contact" : <Contact>,
"otherContact" : <OtherContact>,
"otherInfo" : <OtherInfo>,
"login" : <LoginInfo>
}
In our scenario, in order to provide convenience to loan administrators, lenders, and borrowers, we have added two additional fields to the user collection: amountPaid and amountDue. In the case of a borrower, amountPaid represents how much money has been paid to the lender, including overpayments. The amountDue field represents a sum of the amountDue fields in the loan payments list (shown next).
In the case of a lender, amountPaid represents a sum of the amounts received from users who borrowed from this lender. The amountDue field represents the sum of all amounts due from borrowers for the lender.
The heart of this business is the loan collection. Obviously, each loan document records not only the currency, amount, and interest, but the payment schedule as well. Accordingly, we need to first define a Payment class, as shown here. Each payment records the payment amount due, the amount paid, the due date, the payment received date, and the status. The status consists of a set of key words recorded in the common collection payStatus key:
Payment = {
"amountDue" : <NumberDecimal>,
"amountPaid" : <NumberDecimal>,
"dueDate" : <Date>,
"recvDate" : <Date>,
"status" : Common::payStatus
}
The LoanInfo class describes the amount borrowed (the principal), annual interest rate, effective interest rate, currency, and monthly payment amount:
LoanInfo = {
"principal" : <NumberDecimal>,
"numPayments" : <int>,
"annualRate" : <NumberDecimal>,
"effectiveRate" : <NumberDecimal>,
"currency" : Common::currency,
"monthlyPymt" : <NumberDecimal>
}
For the purposes of this book, we make the following assumptions:
The formula for calculating monthly payments is as follows:
monthlyPymt = principal *
( effectiveRate / (1 - (1 + effectiveRate) ** -numPayments ))
Finally, the loan collection document structure simply links the borrower, lender, and payment schedule, as well as loan information. In addition, the loan document contains a payment schedule in the form of a list of payment documents, as well as a field representing any overpayments:
Loan = {
"loanKey" : <string>,
"borrowerKey" : <string>
"lenderKey" : <string>,
"loanInfo" : <LoanInfo>,
"overpayment" : <NumberDecimal>
"payments" : [<Payment>,<Payment>,etc.]
}
Next, we'll tackle common values used by BigLittle Micro Finance Ltd.
Finally, as with our other scenarios, we introduce a common collection that serves to house choices used in drop-down menus. We also use this to enforce uniformity when collecting data. Here are the currently suggested definitions for BigLittle:
Common = [
{ "title" : ['Mr','Ms','Dr',etc.] },
{ "userType" : ['borrower','lender'] },
{ "phoneType" : ['home', 'work', 'mobile', 'fax'] },
{ "socMediaType" : ['google','twitter','facebook','skype',etc.] },
{ "genderType" : [{'M' : 'Male'},{'F' : 'Female'},{'X' : 'Other'}] },
{ "currency" : ['AUD','CAD','EUR','GBP','INR','NZD','SGD','USD'] },
{ "payStatus" : ['scheduled','received','overdue'] },
]
Now that you have an idea of the BigLittle data structures, as well as a general understanding of the NumberDecimal class, let's look at how financial data might be stored in the case of BigLittle Micro Finance Ltd.
As with Book Someplace, Inc., our previous scenario, we use Django to build a web infrastructure. The source code can be seen at /path/to/repo/chapters/10. The web infrastructure is at /path/to/repo/www/chapter_10. In this chapter, we do not dive into the details of Django, but rather draw out the pertinent bits of code needed to illustrate the point.
In order to generate a loan proposal, we need to develop a domain service class that gives us access to the database loans collection. For this purpose, we created the biglittle.domain.loan.LoanService class. In this class, the key method generates a loan proposal, as shown here:
def generateProposal(self, principal, numPayments, annualRate, \
currency, borrowerKey, lenderKey, \
lenderName, lenderBusiness) :
effective_rate = annualRate / 100 / 12;
monthly_payment = principal * \
( effective_rate / (1 - (1 + effective_rate) ** -numPayments ))
loanInfo = {
'principal' : principal,
'numPayments' : numPayments,
'annualRate' : annualRate,
'effectiveRate' : effective_rate * 1000,
'currency' : currency,
'monthlyPymt' : monthly_payment
}
loan = {
'borrowerKey' : borrowerKey,
'lenderKey' : lenderKey,
'lenderName' : lenderName,
'lenderBusiness' : lenderBusiness,
'overpayment' : 0.00,
'loanInfo' : loanInfo,
'payments' : []
}
return loan
The save() method does the actual database insertion, shown here. Note that we first convert the data format of the financial data fields to BSON Decimal128 before storage:
def save(self, loan) :
loan = self.generateLoanKey(loan)
loan.convertDecimalToBson()
return self.collection.insert(loan)
As mentioned earlier, we need to use the Python class decimal.Decimal for processing, but wish to store our information in MongoDB using the BSON Decimal128 (MongoDB NumberDecimal) data format. The conversion process could be used any time data needs to be stored and retrieved, however, this could lead to redundant code. In this case, the best solution is to have the entity class perform the data conversion. Accordingly, from the top of the loan entity class, found in the module /path/to/repo/chapters/10/src/biglittle/entity/loan.py, we import the two data format classes:
from decimal import Decimal
from bson.decimal128 import Decimal128
We then add two methods to the biglittle.entity.loan.Loan class: convertDecimalToBson() and convertBsonToDecimal(). The convertDecimalToBson() method is shown first. As you can see, the conversion process simply takes the current value of the field and supplies a string representation to the constructor of bson.decimal.Decimal128:
def convertDecimalToBson(self) :
self['overpayment'] = Decimal128(str(self['overpayment']))
if 'payments' in self :
for doc in self['payments'] :
doc['amountPaid'] = Decimal128(str(doc['amountPaid']))
doc['amountDue'] = Decimal128(str(doc['amountDue']))
loanInfo = self['loanInfo']
loanInfo['principal'] = Decimal128(str(loanInfo['principal']))
loanInfo['annualRate'] = Decimal128(str(loanInfo['annualRate']))
loanInfo['effectiveRate'] = \
Decimal128(str(loanInfo['effectiveRate']))
loanInfo['monthlyPymt'] = Decimal128(str(loanInfo['monthlyPymt']))
return True
The reverse process, converting from Decimal128 to Decimal, is in one sense easier, and in another sense more difficult. The process is easier in that there is a method, Decimal128.to_decimal(), that produces a decimal instance. It is more difficult in that we need to first make sure that all financial data fields are of type Decimal128. This is accomplished by first calling convertDecimalToBson(), ensuring all financial data is in BSON Decimal128 format. The code for convertBsonToDecimal() is shown here:
def convertBsonToDecimal(self) :
self.convertDecimalToBson()
self['overpayment'] = self['overpayment'].to_decimal()
if 'payments' in self :
for doc in self['payments'] :
doc['amountPaid'] = doc['amountPaid'].to_decimal()
doc['amountDue'] = doc['amountDue'].to_decimal()
loanInfo = self['loanInfo']
loanInfo['principal'] = loanInfo['principal'].to_decimal()
loanInfo['annualRate'] = loanInfo['annualRate'].to_decimal()
loanInfo['effectiveRate'] = loanInfo['effectiveRate'].to_decimal()
loanInfo['monthlyPymt'] = loanInfo['monthlyPymt'].to_decimal()
return True
Now that you have an idea how to handle monetary data, it's time to examine how to connect documents across multiple collections.
As we have discussed throughout this book, one major objective in your initial database design should be to design a database document structure such that there is no need to cross-reference data between collections. The temptation, especially for those of us steeped in traditional RDBMS database techniques, is to normalize the database structure and then define a series of relationships that are contingent upon a series of interlocking foreign keys. This is the beginning of many problems ... for legacy RDBMS developers! For those of us fortunate enough to be operating in the MongoDB NoSQL world, such nightmares are a thing of the past, given a proper design. Even with a proper database design, however, you might still find occasions requiring a reference to documents across collections in MongoDB.
Let's first have a look at a number of techniques commonly used to reference documents across collections.
There are several techniques available to MongoDB developers. The more popular ones include the following:
A less common cross-reference technique would be to use database references (DBRefs). For on-the-fly cross-references, another extremely useful tool is to use the $lookup pipeline aggregation operator. Although the building block approach has actually already been used in the scenarios describing Book Someplace, we present a short discussion of this approach next.
In this approach, the developer defines a set of small classes (that is, building blocks) that represent objects needed in your application that must ultimately be stored. You then define a collection of document structures as a super-set of these small classes. We have already seen an example of this approach in the design for Book Someplace. For example, we designed an object location, which includes fields such as streetAddress, city, postalCode, and more. This class was then used as part of the document structure for both the customers and the properties collections.
The advantage of this approach is that the size of each document is smaller and more manageable. The disadvantage is that you might need to perform a secondary lookup if additional information from the other collection is needed.
As an example, recalling the Book Someplace scenario, when we create an entry in the bookings collection, we include the following small objects containing customer information: Name, Location, and Contact. But what if we need to produce a booking demographic report that also includes the customer's gender? Using the building block approach, we would have two choices: expand what is included in each booking document to also include the OtherInfo class (includes gender), or perform a secondary lookup and get the full document from the customers collection.
This approach is very similar to the one we've just described, except, in this case, the entire document from one collection is embedded directly inside the document from another collection. When using the building block approach, only a sub-class from one collection is inserted into the other collection.
The advantage of the embedded document approach is that there is absolutely no need for a further lookup should additional information from the other collection be needed. The disadvantage of this approach is that we now have duplicate data, which in turn also means potentially wasted space as we probably do not need all of this information all of the time.
As an example, in the case of Book Someplace, for each booking, instead of using the building block approach and storing just fragments of the customer, we could instead embed the entire document for each customer making a booking.
The third approach, most commonly used by MongoDB developers, is to use document references. In this approach, instead of storing a partial document (that is, a building block), or instead of storing an entire document (embedded document), you only store the equivalent of a foreign key, which then allows you to perform a secondary lookup to retrieve the needed information.
Although you could conceivably store the _id (ObjectID instance) field as the reference, it is considered a best practice in the MongoDB world to have a developer-generated unique key of some sort, which can be used to perform the lookup.
As an example, when storing loan information, instead of storing partial or complete borrower and lender information, just store the userKey fields. This can then be used in conjunction with find() to retrieve lender or borrower information. You will note that this is the approach taken with BigLittle Micro Finance Ltd. When creating a loan document, the userKey field for the lender is stored in lenderKey, and the userKey value for the borrower is stored in borrowerKey.
Yet another approach, used less frequently, is to use a DBRef. Technically speaking, there is actually no difference between creating a DBRef as compared to simply adding a document reference (see the previous sub-topic). In both cases, an additional lookup is required. The main difference between using a DBRef compared to using a document reference is that the DBRef is a uniform, formalized way of stating the reference.
DBRef is actually not a data type in and of itself. Rather, it's a formal JSON structure that completely identifies the document in the target collection. Here are the fields required to insert a properly recognized DBRef:
The generic syntax for creating a DBRef would appear as follows:
var = { "$ref" : <collection name>, "$id" : ObjectId(<value of _id field>) }
Alternatively, you can directly reference the DBref and create the instance by supplying these two arguments to the constructor:
var = DBRef(<collection name>, ObjectId(<value of _id field>))
Now let's have a quick look at how you can use the aggregation framework to perform operations across collections.
One last document cross-reference approach we present is to use the aggregation pipeline $lookup stage operator (https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#lookup-aggregation). This operator performs the equivalent of an SQL LEFT OUTER JOIN. The prerequisite for this operation is to have a field that is common between the two collections referenced. The generic syntax is as follows:
db.<this_collection>.aggregate([
{ $lookup: {
from: <target_collection_name>,
localField: <common_field_in_this_collection>,
foreignField: <common_field_in_target_collection>,
as: <output_array_field> } }
]);
Two other options are available for more complex processing. let allows you to assign a value to a variable for temporary use in the pipeline. pipeline lets you run pipeline operations on the incoming data stream from the target collection. A practical example follows in the next several sections.
As we have already covered usage examples for building-block and embedded documents in earlier chapters, let's now focus on how to use and create a document that relates to other documents using a common reference. As an example, let's look at the process of creating a Loan document. As you recall from the Loan collection data structure section in this chapter, the Loan document includes two references to the users collection: one for the borrower and another for the lender.
The users collection includes a unique field named userKey. When we create a loan document, all we need to do is to add the value of the userKey property for the lender, and the userKey value for the borrower.
The final loan document might appear as follows:
{
"_id" : ObjectId("5da950636f165e472c37a98d"),
"loanKey" : "LAMOHOLM1595_CHRIMARQ4459_20200303",
"borrowerKey" : "LAMOHOLM1595",
"lenderKey" : "CHRIMARQ4459",
"overpayment" : NumberDecimal("0.00),
"payments" : [ <not shown> ],
"loanInfo" : {
"principal" : NumberDecimal("19700.0000000000"),
"numPayments" : 24,
"annualRate" : NumberDecimal("1.26000000000000"),
"effectiveRate" : NumberDecimal("0.00105000000000000"),
"monthlyPymt" : NumberDecimal("831.650110710560")
}}
Let's first look at the process of collecting loan information from a potential borrower and generating loan proposals.
In our scenario, we build the loan document after collecting information from the potential borrower using a Django form. Here is the code, located in /path/to/repo/www/chapter_10/loan/views.py. At the beginning of the code file are the import statements:
from django.template.loader import render_to_string
from django.http import HttpResponse
from config.config import Config
from db.mongodb.connection import Connection
from biglittle.domain.common import CommonService
from biglittle.domain.loan import LoanService
from biglittle.domain.user import UserService
from biglittle.entity.loan import Loan
from biglittle.entity.user import User
from utils.utils import Utils
The method that presents the form to the fictitious borrower is index(). We first initialize the important variables:
def index(request) :
defCurr = 'USD'
proposals = {}
have_any = False
principal = 0.00
numPayments = 0
currency = defCurr
config = Config()
comSvc = CommonService(config)
userSvc = UserService(config, 'User')
maxProps = 3
utils = Utils()
borrowerKey = 'default'
We then use the biglittle.domain.common.CommonService class to retrieve a list of payment lengths and currencies that are offered by our fictitious lenders:
payments = comSvc.fetchByKey('paymentLen')
currencies = comSvc.fetchByKey('currency')
Next, we check to see if the request is an HTTP POST. If so, we gather the parameters needed to calculate loan payments:
if request.method == 'POST':
if 'borrower' in request.POST :
borrowerKey = request.POST['borrower']
if 'principal' in request.POST :
principal = float(request.POST['principal'])
if 'num_payments' in request.POST :
numPayments = int(request.POST['num_payments'])
if 'currency' in request.POST :
currency = request.POST['currency']
If the amount entered for principal is greater than zero, we proceed with the loan calculation. Please note that the pickRandomLenders() method shown here is a simulation. In actual practice, the web app would send a notification to potential lenders. The web app would then present proposals to the borrower for final selection. Although we use a custom cache class to store proposals for the next cycle, we could just as easily have used the Django session mechanism:
if principal > 0 :
utils = Utils()
have_any = True
loanSvc = LoanService(config, Loan())
lenders = userSvc.pickRandomLenders(maxProps)
have_any = True
proposals = loanSvc.generateMany(float(principal), \
int(numPayments), currency, borrowerKey, lenders)
utils.write_cache(borrowerKey, proposals)
Finally, as with the web apps shown for our previous scenario, Book Someplace, Inc., we render nested templates – first loan.html, and then layout.html, and return an HttpResponse:
params = {
'payments' : payments,
'currencies' : currencies,
'proposals' : proposals,
'have_any' : have_any,
'borrowers' : borrowers,
'principal' : principal,
'numPayments': numPayments,
'currency' : currency,
'borrower' : str(borrowerKey)
}
main = render_to_string('loan.html', params)
home = render_to_string('layout.html', {'contents' : main})
return HttpResponse(home)
The loan generation method is located in the /path/to/repo/chapters/10/src/biglittle/domain/loan.py module. Again, as mentioned above, this is a simulation where we pick annual rates at random. In actual practice, the web app would solicit loan proposals from lenders who would offer an appropriate annual rate:
def generateMany(self, principal, numPayments, currency, \
borrowerKey, lenders) :
proposals = {}
for item in lenders :
import random
annualRate = random.randint(1000,20000) / 1000
doc = self.generateProposal(principal, numPayments, \
annualRate, currency, borrowerKey, item['key'], \
item['name'], item['business'])
proposals.update({ item['key'] : doc })
return proposals
Now, it's time to have a look at the code in which a borrower accepts a loan proposal.
Once a loan proposal has been accepted by the borrower, we are able to put together a Loan document. This is where we put document references to use. The code that captures the borrower's selection is also located in views.py, in the accept() method. We start by checking to see if the request is an HTTP POST. We also check to make sure the accept button was pressed, and that the key for lender was included in the post, shown here:
def accept(request) :
message = 'Sorry! Unable to process your loan request'
utils = Utils()
if request.method == 'POST':
if 'accept' in request.POST :
if 'lender' in request.POST :
lenderKey = request.POST['lender']
borrowerKey = request.POST['borrower']
We retrieve the proposals from our simple cache class and loop through them until we match the lender key, after which we use the loan domain service to save the document:
proposals = utils.read_cache(borrowerKey)
if lenderKey in proposals :
loan = proposals[lenderKey]
config = Config()
loanSvc = LoanService(config, Loan())
loanSvc.save(loan)
All that is left is to return to the previous web page – simply a matter of calling the index() method:
return index(request)
Let's now turn our attention to a practical example using the $lookup aggregation pipeline operator.
For the sake of illustration, let's assume that management wishes to be able to generate a report of lender names and addresses sorted by the lender's country, and then by the total amounts loaned, from largest to smallest. Lender information is found in the users collection, however, loan amount information is in loans; accordingly, the aggregation framework is needed.
The first aggregation pipeline stage uses the $lookup pipeline operator to join the users and loans collections. As with other complex examples discussed in this book, we first model the query using the mongo shell. Here is how the query might appear:
db.users.aggregate([
{ $match : { "userType" : "lender" }},
{ $project : { "userKey" : 1, "businessName" : 1, "address" : 1 }},
{ $lookup : { "from" : "loans", "localField" : "userKey",
"foreignField" : "lenderKey", "as" : "loans" }},
{ $addFields : { "total" : { "$sum" : "$loans.loanInfo.principal" }}},
{ $sort : { "address.country" : 1, "total" : -1 }}
]);
Now let's break this down into stages:
| Stage | Notes |
| $match | Adds documents to the pipeline from the users collection whose userType is lender. |
| $project | Strips out all fields in the pipeline other than userKey, businessName, and address. |
| $lookup | Adds documents from the loans collection into a new array field called loans. |
| $addFields | Adds a new field, total, which represents a sum of the principal amounts in the loans array. |
| $sort | Sorts all documents in the pipeline first by country and then by total in descending order. |
We do not have space to show the entire result set, however, here is how a single document might appear after launching the query just shown:
{
"_id" : ObjectId("5d9d3d008b1c565ae0d2813e"),
"userKey" : "CASSWHEE6724",
"businessName" : "Illuminati Trust",
"address" : {
"streetAddress" : "5787 Red Mountain Boulevard",
"geoSpatial" : [ "103.7", "17.2167"]
# not all fields shown
},
"loans" : [ {
"_id" : ObjectId("5da5981b72d8437f7436c419"),
"borrowerKey" : "SUNNCHAM4815",
"lenderKey" : "CASSWHEE6724",
"loanInfo" : {
"principal" : NumberDecimal("12800.0000000000"),
# not all fields shown
} } ],
"total" : NumberDecimal("12800.0000000000")
}
Having successfully modeled the query using the mongo shell, it's time to formulate it into Python code.
For the purposes of this illustration, we create a new Django app called maintenance located in /path/to/repo/www/chapter_10/maintenance. The logic in views.py, shown here, is extremely simple. The main work is done by the user domain service. The resulting iteration is then rendered in the totals.html template and injected into the main layout.html template.
As usual, the module starts with a set of import statements:
from django.template.loader import render_to_string
from django.http import HttpResponse
from config.config import Config
from db.mongodb.connection import Connection
from biglittle.domain.user import UserService
We then define a method, totals(), which runs the report and sends the results to the view template:
def totals(request) :
config = Config()
userSvc = UserService(config, 'User')
totals = userSvc.fetchTotalsByLender()
params = { 'totals' : totals }
main = render_to_string('totals.html', params)
home = render_to_string('layout.html', {'contents' : main})
return HttpResponse(home)
Likewise, because most of the work is passed on to the MongoDB aggregation framework, the new method, fetchTotalsByLender(), in the biglittle.domain.user.UserService class is also quite simple. Here is the code:
def fetchTotalsByLender(self) :
pipe = [
{ "$match" : { "userType" : "lender" }},
{ "$project" : { "userKey":1, "businessName":1, "address":1 }},
{ "$lookup" : { "from" : "loans", "localField" : "userKey",
"foreignField" : "lenderKey", "as" : "loans" }},
{ "$addFields" : {"total":{"$sum":"$loans.loanInfo.principal"}}},
{ "$sort" : { "address.country" : 1, "total" : -1 }}
]
return self.collection.aggregate(pipe)
Finally, the view template logic is shown next. Notice that we use the Django template language to loop through the list of totals. We use loop.counter0 to create a double-width table to conserve screen space:
<h2>Total Amount Lent By Lender</h2>
<hr>
<table border=1>
<tr><th>Business</th><th>Country</th><th>Total Lent</th>
<th>Business</th><th>Country</th><th>Total Lent</th></tr>
{% for doc in totals %}
{% if doc.total %}
{% if forloop.counter0|divisibleby:2 %}<tr>{% endif %}
<td class="td-left">{{ doc.businessName }}</td>
<td class="td-left">{{ doc.address.country }}</td>
<td class="td-right">{{ doc.total }}</td>
{% if not forloop.counter0|divisibleby:2 %}</tr>{% endif %}
{% endif %}
{% endfor %}
</table>
The output appears similar to the following screenshot:

To test the working code using the Docker-based test environment, enter this URL in the browser of your host computer: http://chap10.biglittle.local/maintenance/totals/
In the next section, we take a look at situations where you need to update documents across collections.
In the case of BigLittle Micro Finance, when a borrower makes a loan payment, the status field for that payment needs to be updated to reflect payments being received. However, given our example document structure, we also need to update the amountDue and amountPaid fields in the user document. This is referred to as a secondary update.
This is a step easily overlooked when developing financial applications. What can often happen is that the total amount paid as recorded in the users collection could fall out of sync with the sum of the amount field values in the loans collection. Accordingly, it's not a bad idea when performing secondary updates to either provide an immediate double-check and note any discrepancies in a log, or alternatively, provide management with a discrepancies report allowing administrators to perform the double-check themselves. Let's first look at the process of accepting a loan payment in our sample scenario.
For the purposes of this illustration, we make the following assumptions:
In order to accept a loan payment, logic needs to be devised in the Django views.py module of the newly created maintenance app. In addition, we need to add the appropriate methods to the loan domain service for BigLittle. Finally, a form template needs to be created that lets a loan administrator choose the borrower and process a loan payment. Also, in the urls.py file found in /path/to/repo/www/chapter_10/maintenance, we define two routes: one that lets a loan admin choose the borrower, and another to accept the payment. The contents of this file are shown here:
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='maintenance.index'),
path('totals/', views.totals, name='maintenance.totals'),
path('payments/', views.payments, name='maintenance.payments'),
]
Now we can examine the view logic.
The view logic for this illustration is found in /path/to/repo/www/chapter_10/maintenance/views.py. As with other classes, we first import the necessary Python and Django classes. In addition, we import the necessary BigLittle entity and domain service classes:
from django.template.loader import render_to_string
from django.http import HttpResponse
from config.config import Config
from db.mongodb.connection import Connection
from biglittle.domain.user import UserService
from biglittle.domain.loan import LoanService
from biglittle.entity.loan import Loan,LoanInfo,Payment
from biglittle.entity.user import User
In the index() method, the loan admin chooses the borrower whose payment is processed. We first initialize key variables:
def index(request) :
loan = Loan()
amtPaid = 0.00
amtDue = 0.00
message = ''
borrower = None
borrowerKey = None
borrowerName = ''
payments = []
overpayment = 0.00
After that, we initialize domain services and fetch a list of borrower keys and names:
config = Config()
userSvc = UserService(config, 'User')
borrowers = userSvc.fetchBorrowerKeysAndNames()
Finally, we render the results using the inner template, maintenance.html, subsequently injected into the layout:
params = {
'loan' : loan,
'payments' : payments,
'borrowers' : borrowers,
'borrowerName' : borrowerName,
'borrowerKey' : borrowerKey,
'amountDue' : amtDue,
'overpayment' : overpayment,
'message' : message
}
main = render_to_string('maintenance.html', params)
home = render_to_string('layout.html', {'contents' : main})
return HttpResponse(home)
Next, we look at the view logic for processing a payment.
As with the index() method, we first initialize the same variables as, ultimately, we'll be sending results to the same template for rendering:
def payments(request) :
loan = Loan()
amtPaid = 0.00
amtDue = 0.00
message = ''
borrower = None
borrowerKey = None
borrowerName = ''
overpayment = 0.00
We then initialize payments as a list with a single blank Payment instance:
payment = Payment()
payments = [payment.populate()]
Next, in a similar manner to the index() method, we get instances of domain service classes representing users and loans. With the user domain service, we retrieve a list of borrower keys and names:
config = Config()
userSvc = UserService(config, 'User')
loanSvc = LoanService(config, 'Loan')
borrowers = userSvc.fetchBorrowerKeysAndNames()
We then check to see if the HTTP request method was POST. If so, we check for the existence of borrowerKey and amount_paid values:
if request.method == 'POST':
if 'borrowerKey' in request.POST :
borrowerKey = request.POST['borrowerKey']
if 'amount_paid' in request.POST :
amtPaid = float(request.POST['amount_paid'])
If we have a borrowerKey, it is used to retrieve borrower information. If valid, we proceed to retrieve loan information using the validated borrowerKey:
if borrowerKey :
borrower = userSvc.fetchUserByBorrowerKey(borrowerKey)
if borrower :
borrowerName = borrower.getFullName()
loan = loanSvc.fetchLoanByBorrowerKey(borrowerKey)
A valid loan instance is returned. If the amount paid by the borrower in this processing pass is greater than zero, we process the loan and retrieve the modified loan document:
if loan :
if amtPaid > 0 :
if loanSvc.processPayment(borrowerKey, amtPaid, loan) :
message = 'Payment processed'
loan = loanSvc.fetchLoanByBorrowerKey(borrowerKey)
else :
message = 'Problem processing payment'
Inside the block created by the if loan statement, we add logic to check to see if we have a valid loan document. We also extract a list of payments and the overpayment amount. Because Python and Django do not directly support the BSON Decimal128 format, we first convert all financial information to Decimal before rendering:
loan.convertBsonToDecimal()
payments = loan.getPayments()
loanInfo = loan.getLoanInfo()
amtDue = loanInfo.getMonthlyPayment()
overpayment = loan.get('overpayment')
We then present all this information to the view rendering process. The parameters are initialized, and a the sub document maintenance.html is rendered first. It is then injected into layout.html to produce the final view:
params = {
'loan' : loan,
'payments' : payments,
'borrowers' : borrowers,
'borrowerName' : borrowerName,
'borrowerKey' : borrowerKey,
'amountDue' : amtDue,
'overpayment' : overpayment,
'message' : message
}
main = render_to_string('maintenance.html', params)
home = render_to_string('layout.html', {'contents' : main})
return HttpResponse(home)
Let's now have a look at the domain service methods referenced in the view logic.
fetchBorrowerKeysAndNames() is the first method referenced in the view logic. This method is found in the biglittle.domain.user.UserService class. Its purpose is to return a list of documents containing the values of the _id, userKey, and name fields. This list is subsequently used in the view template to provide a drop-down list from which the loan administrator can choose. Here is that method:
def fetchBorrowerKeysAndNames(self) :
keysAndNames = []
lookup = self.collection.find({'userType':'borrower'})
for borrower in lookup :
doc = {
'id' : borrower.getId(),
'key' : borrower.getKey(),
'name' : borrower.getFullName()
}
keysAndNames.append(doc)
return keysAndNames
Another method used during payment acceptance is fetchUserByBorrowerKey(), found in the same class. Its purpose is to return a biglittle.entity.user.User instance from which the user's name is extracted. It is a simple lookup method, shown here:
def fetchUserByBorrowerKey(self, borrowerKey) :
return self.collection.find_one({ \
"userKey" : borrowerKey, "userType" : "borrower" })
In a similar manner, once the borrower has been identified, and the loan payment amount has been recorded, the fetchLoanByBorrowerKey() method from the LoanService class, found in the biglittle.domain.loan.py module is called. Again, it's a simple lookup, this time by the borrower key. The outstanding feature of this method is that we first convert all financial information from MongoDB NumberDecimal (BSON Decimal128) to Python decimal.Decimal, for ease of processing. Here is that method:
def fetchLoanByBorrowerKey(self, borrowerKey) :
loan = self.collection.find_one({"borrowerKey":borrowerKey})
loan.convertBsonToDecimal()
return loan
As you may recall from our list of assumptions discussed at the start of this subsection, we assume any given borrower can have at most one outstanding loan. Thus, if we do a lookup by borrower key, we are assured of retrieving a unique loan document.
Finally, we have the processPayment() method, located in the LoanService class, which applies the payment, updates the status, and records any overpayment information. As with most complex methods, we start by initializing key variables. We also split out loanInfo from the loan document in order to retrieve the expected payment amount due:
def processPayment(self, borrowerKey, amtPaid, loan) :
from decimal import Decimal
from bson.decimal128 import Decimal128
config = Config()
result = False
overpayment = 0.00
loanInfo = loan.getLoanInfo()
amtDue = loanInfo.getMonthlyPayment()
Next, we check the data type of amtPaid and convert it to decimal.Decimal if needed:
if not isinstance(amtPaid, Decimal) :
amtPaid = Decimal(amtPaid)
The heart of this method loops through the payments list, looking for the first document in the list for which the amountPaid field is zero. If found, we check to see if the amount paid is less than the amount due. If so, we apply that amount to overpayment, but do not otherwise touch the loan payment list:
for doc in loan['payments'] :
if doc['amountPaid'] == 0 :
if amtPaid < amtDue :
overpayment = amtPaid
Still in the loop, if the amount paid is equal to or greater than the amount due, we set the amountPaid field in the payments list equal to the amount due. Any excess amount is added to the overpayment field. status is updated to received, and the date the payment was accepted is stored in recvDate. We also break out of the loop as there is no need for further processing:
else :
overpayment = amtPaid - amtDue
doc['amountPaid'] = doc['amountDue']
doc['status'] = 'received'
from time import gmtime, strftime
now = strftime('%Y-%m-%d', gmtime())
doc['recvdate'] = now
break
Now out of the payments loop, we update the overpayment field, adding any excess payments:
currentOver = loan.get('overpayment')
loan.set('overpayment', currentOver + overpayment)
Lastly, we convert all decimal values to NumberDecimal, set the filter to borrowerKey, and perform an update:
loan.convertDecimalToBson()
filt = { "borrowerKey" : borrowerKey }
result = self.collection.replace_one(filt,loan)
return result
Now, let's have a quick look at the loan maintenance template.
The loan maintenance view template is located at /path/to/repo/www/chapter_10/templates/maintenance.html. The first item of interest is how to present the drop-down list of borrowers. The view calls a method, fetchBorrowerKeysAndNames(), from the UserService class, subsequently sent to the template. We are then able to build the HTML SELECT element using the Django template language, as shown here:
<select name="borrowerKey">
{% if borrowerKey and borrowerName %}
<option value="{{ borrowerKey }}">{{ borrowerName }}</option>
{% endif %}
{% for user in borrowers %}
<option value="{{ user.key }}">{{ user.name }}</option>
{% endfor %}
</select>
We also provide the complete payment schedule for the borrower in the same template. This is so that the loan administrator can see which payments have been received. The view extracts the payments list from the loan document and sends it to the template, where we again use the Django template language to provide the list:
<h3>Payment Schedule</h3>
{% if payments %}
<table>
<tr><th>Amount Paid</th><th>Due Date</th><th>Status</th></tr>
{% for doc in payments %}
<tr><td>{{ doc.amountPaid|floatformat:2 }}</td>
<td>{{ doc.dueDate }}</td><td>{{ doc.status }}</td></tr>
{% endfor %}
</table>
{% endif %}
Now that you have an idea of how payment processing works, it's time to look at the secondary updates that need to occur.
In order to avoid the overhead involved in creating normalized data structures with lots of foreign keys and relationships, MongoDB DevOps may choose to store redundant information across collections. The main danger here is that if a change occurs in one collection, you, the developer, may need to ensure that a secondary update occurs in the collection with related information.
Accordingly, when a loan payment is accepted, the following secondary updates need to occur in the users collection:
Before we get started on the implementation, we need to first digress and discuss using listeners and events.
What is now known as the Publish-Subscribe (or affectionately PubSub) software design pattern was originally proposed by Birman and Joseph in a paper entitled Exploiting Virtual Synchrony in Distributed Systems, presented to the 11th Association for Computing Machinery (ACM) symposium on operating system principles. To boil down a rather complex algorithm into simple terms, a publisher is a software class that sends a message before or after an event of significance takes place. A common example of such an event would be a database modification. A subscriber is a software class that listens to messages produced by its associated publisher. The subscriber examines the message, and may or may not take action, at its own determination. In our example scenario, we create a publisher class that sends a message to subscribers when a loan payment is received. The subscriber performs the secondary update on the relevant borrower document.
At this point, you might ask yourself: why not just update the borrower document right away? The answer is that such an operation would violate an important software principal known as separation of concerns. Each software class and each method within that class should have a single logical focus. In our example, it's the job of the LoanService class to service the loans collection, and no other.
If we start to have one domain service interfere with the responsibilities of another domain service, soon we end up with what eventually came to be known as spaghetti code, made famous by a Dilbert cartoon published on June 10, 1994.
Important terms for you to be aware of include the following:
Rather than having to invent a custom Publish-Subscribe class, in this illustration, we take advantage of a very simple PubSub implementation released in 2015, called Python Simple PubSub. It is a simple matter to define a Publisher class that provides a wrapper for PubSub. The code can be found at /path/to/repo/chapters/10/src/biglittle/events/publisher.py. The main thing to note here is that the pubsub.pub.sendMessage() method takes its second argument in **kwargs format. Accordingly, you need to be careful to always use whatever key you define. In this example, the name of the key is arg:
import pubsub
class Publisher :
def attach(self, topic, listener) :
pubsub.subscribe(listener, topic)
def trigger(self, topic, obj) :
pubsub.sendMessage(topic, arg=obj)
We'll look at creating listeners for loan events next.
Since the secondary update needs to occur on the users collection, it's appropriate to define the listener in the UserService class already defined. We add a new method, updateBorrowerListener(), which is later subscribed as a listener. Note that the argument to the listener is arg, in order to match that defined in our Publisher wrapper class described in the section just before.
We first import the Python classes to convert between Decimal and Decimal128:
def updateBorrowerListener(self, arg) :
from decimal import Decimal
from bson.decimal128 import Decimal128
We then initialize two sets of two values representing amounts due, and amounts paid, as defined by information from the users collection and also the loans collection:
loan = arg['loan']
amtPaid = arg['amtPaid']
amtDueFromLoan = Decimal(0.00)
amtPaidFromLoan = Decimal(0.00)
amtDueFromUser = Decimal(0.00)
amtPaidFromUser = Decimal(0.00)
Next, we retrieve a User entity class instance based on the borrower key stored in the loan entity. If not found, an entry is made in the discrepancy log:
config = Config()
log_fh = open(config.getConfig('discrepancy_log'), 'a')
borrowerKey = loan.get('borrowerKey')
borrower = self.fetchUserByBorrowerKey(borrowerKey)
if not borrower :
message = now + ' : User ' + borrowerKey + ' not found'
log_fh.write(message + "\n")
We can now retrieve and update the information on the amount due and paid as recorded in the User document:
else :
amtDueFromUser = borrower.get('amountDue').to_decimal()
amtPaidFromUser = borrower.get('amountPaid').to_decimal()
amtDueFromUser -= amtPaid
amtPaidFromUser += amtPaid
The secondary update is then performed using the pymongo update_one() method:
filt = {'userKey' : borrower.getKey()}
updateDoc = { '$set' : {
'amountDue' : Decimal128(str(amtDueFromUser)),
'amountPaid' : Decimal128(str(amtPaidFromUser))
}}
self.collection.update_one(filt, updateDoc)
Let's now look at how the publisher notifies the subscriber.
Setting up the publisher to notify the subscriber involves two steps:
The first step is achieved in the views.py module of the Django maintenance app. In the accept() method, the following is added immediately after variable initialization:
from biglittle.events.publisher import Publisher
publisher = Publisher()
config = Config()
userSvc = UserService(config, 'User', publisher)
loanSvc = LoanService(config, 'Loan', publisher)
publisher.attach(publisher.EVENT_LOAN_UPDATE_BORROWER, \
userSvc.updateBorrowerListener)
In the biglittle.domain.base.Base class, we modify the __init__() method to accept the publisher as an argument:
class Base :
def __init__(self, config, result_class = None, publisher = None) :
self.publisher = publisher
# other logic in this method now shown
In the processPayment() method of the biglittle.domain.loan.LoanService class (described earlier), we add a call to the publisher to notify (that is, send a message to) the subscriber. Note that we convert the loan entity back to Decimal for processing:
filt = { 'borrowerKey' : borrowerKey }
result = self.collection.replace_one(filt,loan)
if result :
loan.convertBsonToDecimal()
arg = { 'loan' : loan, 'amtPaid' : amtPaid }
self.publisher.trigger( \
self.publisher.EVENT_LOAN_UPDATE_BORROWER, arg)
return result
Next, we look at common problems encountered when working with documents across collections.
Many of the show stoppers that are prevalent in the legacy relational database management system (RDBMS) arena are simply not an issue when using MongoDB due to differences in architecture. There are a few major issues in the area of cross-collection processing of which you need to be aware:
Let's start this discussion by examining ways to ensure uniqueness when using MongoDB.
The first question that naturally comes to mind is: what is uniqueness? In a traditional RDBMS database design, you must provide a mechanism whereby each database table row is different from every other row. If you had two RDMBS table rows, X and Y, which were identical, it would be impossible to guarantee access to X or to Y. Further, it would be a pointless waste of space to maintain duplicate data in such a manner. Accordingly, in the RDBMS world, when designing your database, you need to identify one or more database columns as part of the primary key.
In the case of MongoDB, the equivalent of an RDBMS primary key would be the autogenerated ObjectId. You can reference this value via the alias _id. Unlike RDBMS sequences or auto-increment fields, however, the MongoDB _id field includes both sequential as well as random elements. It consists of 12 bytes broken down as follows:
The _id field could potentially be used when conducting cross-collection operations (see the discussion on DBRefs earlier in this chapter). There is a method, ObjectId.valueOf(), that returns a hexadecimal string of 24 characters. However, within your application code, or when performing an operation using the aggregation framework, as this field is actually an object (for example, ObjectId), you would need to perform further conversions before being able to use it directly. Accordingly, it is highly recommended that within your application you create a custom unique and meaningful key that can be used to cross-reference documents between collections.
There are no specific rules, regulations, nor protocols that apply to define a unique custom key, however, there are two criteria that stand out. The key you choose should be the following:
Usually, in order to achieve uniqueness, some sort of random strategy needs to be employed. Using a value derived from the year, month, day, hours, minutes, and seconds might be of use. You can also use bits from other fields within the document to build a unique key that is meaningful.
As an example, in the BigLittle Micro Finance Ltd. scenario, each borrower or lender has information stored in the users collection. Each document, in addition to the automatically generated ObjectId, has a unique userKey field. As you can see from the following screenshot, the key consists of the first 4 letters of first and last names with a random 4-digit number:

Now that you have an idea of how uniqueness might be defined, let's have a look at how it can be enforced.
Once you have settled on an algorithm for choosing a unique key, is there a way to guarantee uniqueness? One very useful and effective solution is to create a unique index on the key field. This has two benefits:
Using the mongo shell, you can create an index on a field as follows:
db.<name_of_collection>.createIndex(keys, options);
Let's jump right into an example involving the users collection and the userKey field mentioned earlier in this subsection. Here is the query that creates a unique index on the userKey field in ascending order:
db.users.createIndex( { "userKey" : 1 }, { "unique" : true } );
If we then attempt to insert a document with a duplicate userKey, MongoDB refuses to perform the operation, thus enforcing uniqueness. Here is a screenshot of this attempt:

Next, we'll look at the subject of orphans.
The first question to be addressed is: what is an orphan? In the context of RDBMS, a database table row becomes an orphan when its parent relation is deleted. As an example, suppose you have one table for customers and another table for phone numbers. Each customer can have multiple phone numbers. In the phone number table, you would have a foreign key that refers back to the customer table. If you then delete a given customer, all the related rows in the phone numbers table become orphans.
As we mentioned before, MongoDB is not relational, so such a situation would not arise. A lot depends on good MongoDB database design. Let's look at two approaches that avoid creating orphans: embedded arrays and documents.
Using the example in the previous subsection, instead of creating a separate table for phone numbers, in MongoDB, simply include the phone numbers as an embedded array. Here is an example from the users collection. Note how we store multiple email addresses and phone numbers as embedded arrays within the otherContact field:
db.users.findOne({},{"userKey":1,"otherContact":1,"_id":0});
{
"userKey" : "CHARROSS3456",
"otherContact" : {
"emails" : [
"cross100@swisscom.com",
"cross100@megafon.com"
],
"phoneNumbers" : [
"100-688-6884",
"100-431-1074"
]
}
Thus, when a user document is deleted, all of the related information is deleted at the same time, completely avoiding the issue of orphaned information.
In a similar manner, as discussed earlier in this chapter, instead of using DBRefs, or unique keys to reference documents across collections, you can simply embed the entire document, or a subset of that document, where the information is needed. Going back to the Book Someplace, Inc. scenario as an example, a Booking document includes fragments of both Customer and Property. To refresh your memory, have a look at the Booking, CustBooking, and PropBooking document structures:
Booking = {
"bookingKey" : <string>,
"customer" : <CustBooking>,
"property" : <PropBooking>,
"bookingInfo" : <BookingInfo>,
"rooms" : [<RoomBooking>,<RoomBooking>,etc.],
"totalPrice" : <float>
}
CustBooking = {
"customerKey" : <string>,
"customerName" : <Name>,
"customerAddr" : <Location>,
"customerContact" : <Contact>,
"custParty" : [<CustParty>,<CustParty>,etc.]
}
PropBooking = {
"propertyKey" : <string>,
"propertyName" : <string>,
"propertyAddr" : <Location>
"propertyContact" : <Contact>
}
This approach has its own disadvantages. If a customer document representing a customer who has made a booking is deleted, no orphaned documents are created. However, we now have a strange situation where the booking document contains a value for customerKey, but the corresponding customer document no longer exists! So, in a certain sense, although there are no orphaned documents, we now have an orphaned field. One way this situation can be avoided is to move deleted documents into an archive collection. You could then perform a secondary update on all booking documents associated with the removed customer, adding a new field: archived.
We'll now look at considerations for keeping values across collections properly synchronized.
Continuing the discussion from the previous subsection, we once again look at the database structure for Book Someplace, Inc. Another issue with the embedded document approach is what happens when the original customer document is updated? Now you have two places where customer information is stored: once in the customers collection, another in the bookings collection. The solution to this problem depends on the company's philosophy with regard to bookings. On the one hand, you could argue that at the time of the booking the customer information was correct, and must be maintained as is for historic reasons. On the other hand, you could argue that the latest information should always be present in bookings regardless of the situation that existed at the time of the booking. In the latter case, following a customer update, you would need to perform a series of secondary updates on all relevant bookings.
Returning to the BigLittle Micro Finance Ltd. scenario, the situation gets slightly more complicated because financial amounts are involved. Accordingly, it's highly important that your programming code includes a double-check for accuracy. In this case, we need to add programming logic that produces a sum of the amountDue and amountPaid fields in the loan.payments list and compare it with the same fields in the associated user document. If the information does not match, a discrepancies log file entry must be made. What would normally follow would be a tedious manual process of going through all loan payments, beyond the scope of this book.
In order to implement this secondary accuracy check, we add additional logic to the updateBorrowerListener() method in the UserService class already defined. We draw upon our config.Config() class to provide the name and location of the discrepancies log file. In addition, we pull today's date for logging purposes. This logic would then appear immediately after variables are initialized:
def updateBorrowerListener(self, arg) :
# imports (not shown)
# init vars (not shown)
config = Config()
log_fh = open(config.getConfig('discrepancy_log'), 'a')
from time import gmtime, strftime
now = strftime('%Y-%m-%d', gmtime())
# other code already shown earlier in this chapter
Subsequently, just after we perform the update, we add logic to double-check the accuracy of the updated information. First, we calculate the amount due and paid from the list of payments:
# earlier code not shown
self.collection.update_one(filt, updateDoc)
for doc in loan.getPayments() :
amtDueFromLoan += doc['amountDue']
amtPaidFromLoan += doc['amountPaid']
If the amount due or paid does not match, discrepancy log entries are made. Also, of course, it's good practice to close the log file, which ends this method:
if amtDueFromUser != amtDueFromLoan :
log_fh.write(now + ':Amount due discrepancy ' + \
borrowerKey+"\n")
log_fh.write('--"users" collection: ' + \
str(amtDueFromUser)+"\n")
log_fh.write('--"loans" collection: ' + \
str(amtDueFromLoan)+"\n")
if amtPaidFromUser != amtPaidFromLoan :
log_fh.write(now + ' :Amount paid discrepancy ' + \
borroweKey+"\n")
log_fh.write('--"users" collection: ' + \
str(amtPaidFromUser)+"\n")
log_fh.write('--"loans" collection: ' + \
str(amtPaidFromLoan)+"\n")
log_fh.close()
In the next section, we introduce a feature related to storing images in the database called GridFS.
GridFS (https://docs.mongodb.com/manual/core/gridfs/#gridfs) is a technology included with MongoDB that allows you to store files directly in the database. GridFS is a virtual filesystem mainly designed to handle files larger than 16 MB in size. Each document is broken down into chunks of 255 KB except for the last chunk, which could be less than 255 KB. By default, two collections assigned to an arbitrary bucket are used. The chunks collection stores the actual contents of the file. The files collection stores file metadata. Within your database, you can designate as many buckets as needed.
Let's start by discussing why you might want to incorporate GridFS into your applications.
One big advantage is that once stored, MongoDB offers the same advantages to large files as it does to any other type of data: replication to ensure high availability, sharding to facilitate scalability, and the ability to access a file regardless of the host operating system. Accordingly, if the server's operating system places a limit on the number of files in a single directory, you can use GridFS to store the files and get around OS limitations.
Another big advantage is that due to the way GridFS breaks files into 255 KB chunks, reading extremely large files is not as memory intensive as when you try to directly access the same file stored on the server's filesystem. Finally, using GridFS is a great way to keep files synchronized across servers.
It is recommended that you do not use GridFS if all the files you need to store are less than 16 MB in size. If this is the case, and you still want to take advantage of using MongoDB, simply store the file directly in a normal MongoDB document. The maximum size of a BSON document is 16 MB, so if all your files are smaller, using GridFS is a waste of resources.
Also, it is not recommended to use GridFS if the entire document needs to be updated atomically. As an example, if you are maintaining legal documents, each revision needs to be stored as a separate document with changes clearly marked. In this case, either store the document using the server's filesystem or clone the document and store clearly marked revisions.
GridFS is available from the command line via the mongofiles utility. The generic syntax is as follows:
mongofiles <options> <commands> <filename>
Most of the options are identical to those of the mongo shell and include command-line flags to identify a URI style connection string, host, port, and authentication information. You can also specify the IP version (for example, --ipv6) and SSL/TLS secure connectivity options.
Some options are not relevant to the database connection. These additional options, peculiar to GridFS, are summarized here:
| Option | Notes |
| --local=<filename> | Use this option if you want the filename recorded in GridFS to be different from the local filename on the server's OS. In place of <filename>, enter the actual name of the file on the local server filesystem. At the end of the entire command string, enter the filename as you wish it to appear in GridFS. |
| --replace | When you use the put command (described in the next subsection) to store a file, existing files of the same name are not overwritten. Instead, a new entry is created. If you want the currently stored file to be completely replaced, use this option. |
| --prefix=<string> | Use this option to specify a different bucket. The default bucket is fs. Think of a bucket as being like a subdirectory. |
| --db=<database> | This option specifies which database to use for GridFS storage. |
| --uri="<string>" | Just as with the mongo shell, you can consolidate command-line options into a single URI-style string. |
In addition to options, the mongofiles utility has commands, discussed next.
In the following table is a summary of the more important commands used with the mongofiles utility:
| Command | Notes |
| list <filter> | This command operates much like the Linux ls or the Windows dir commands. If you specify one or more characters in the filter, you get a list of filenames starting with those characters. Using Linux as an example, this command is similar to ls xyz* (returns a list of files starting with xyz). |
| search <filter> | This command is the same as list except that a list of filenames matching any part of filter are displayed. Using Linux as an example, this command is similar to ls *xyz* (returns a list of filenames containing xyz). |
| put <filename> | Copies a file from the local filesystem to GridFS. By default, GridFS uses the local filename. If you need to have the GridFS filename be different, use the --local option. Use the --replace option to overwrite an existing GridFS of the same name. |
| get <filename> | The opposite of put. Copies a file from GridFS into the local filesystem. |
| get_id "<_id>" | The same as get except that the file to be copied is identified by its _id field. |
| delete <filename> | Erases a file from GridFS storage. |
| delete_id "<_id>" | The same as delete except that the file to be deleted from GridFS is identified by its _id field. |
Now let's look at a usage example.
In this example, we copy a number of large files drawn from the open source GeoNames project representing world city data and postal codes. The data provided by the project, authored by Marc Wick, is licensed under a Creative Commons Attributions v4.0 license. In the book repository, there is a script, /path/to/repo/sample_data/geonames_downloads.sh, that imports and unzips two large GeoNames project sample data files. Here is that download script:
#!/bin/bash
export STATS_URL="https://download.geonames.org/export/dump/allCountries.zip"
export ZIP_URL="https://download.geonames.org/export/zip/allCountries.zip"
wget -O /tmp/stats.zip $STATS_URL
wget -O /tmp/postcodes.zip $ZIP_URL
unzip /tmp/stats.zip -d /tmp
mv /tmp/allCountries.txt /tmp/allCountriesStats.txt
unzip /tmp/postcodes.zip -d /tmp
mv /tmp/allCountries.txt /tmp/allCountriesPostcodes.txt
rm /tmp/*.zip
ls -l /tmp/all*
Here is a list of the files to be copied into GridFS:

As you can see, most files are well over 16 MB in size. We use the put command to store the files in GridFS in the biglittle database on localhost. Here is an example using the --local option. In this example, we want the filename in GridFS to appear as /postalCodes/world_postal_codes.txt. We can then use list to view results (search could also be used):

From the mongo shell, you can see the new fs collection. fs.chunks contains the actual contents of the files. fs.files contains file metadata, as shown here:

Next, let's look at how GridFS is supported by the PyMongo driver.
The PyMongo driver provides a package, gridfs, that provides support for GridFS from a programming context. The core class is gridfs.GridFS. There are a number of supporting packages, however, in order to retain our focus on the essentials, we concentrate here only on the core class. GridFS class methods mirror the commands discussed in the previous subsection. The following table provides a summary of the more important methods:
| gridfs.GridFS.method | Notes |
| find(*args, **kwargs) | This method operates in a similar manner to pymongo.collection.find(). *args is a JSON document with key/value pairs. The key can be anything in the fs.files collection. The most common key is filename. **kwargs is a series of key-value keypairs including the keys filter, skip, limit, no_cursor_timeout and sort. find_one() takes the same arguments except only a single file is returned. |
| list() | Operates much like the mongofiles list command. It returns a list of of files in GridFS. |
| put(data, **kwargs) | Operates like the mongofiles put command. It writes a file to GridFS. Data can take the form of a bytes data type or an object (for example, a file handle) that provides a read() method. **kwargs can be any of the options mentioned in the documentation for gridfs.grid_file.GridIn. |
| get(file_id) | Much like the mongofiles get command, returns the file based on its _id field value. The return value is the gridfs.grid_file.GridOut instance (much like a file handle). The GridOut object provides methods that allow you to get metadata on the file. Of special interest is its read (NNN) method, giving access to the file contents, NNN number of bytes at a time. There is also a readLine() method, which reads a single line, and readChunk(), which reads out the file in chunks (preset to 255KB). |
| exists(file_id) | Returns a Boolean set to True if the given file _id field value exists; False otherwise. |
| delete(file_id) | Deletes a file with a given value of the _id field. |
Now it's time to have a look at a practical example using this class in our sample scenario.
We need a way to upload loan documents, so we add a file upload HTML element to the maintenance.html template. We also add a loop that displays the currently uploaded documents for this borrower:
<tr><th class="th-right">Loan Documents Stored</th>
<td class="td-left">
{% for name in loan_docs %} <br>{{ name }} {% endfor %}
</td>
</tr>
<tr>
<th class="th-right">Upload Loan Document</th>
<td class="td-left"><input type="file" name="loan_doc" /></td>
</tr>
In the view logic (/path/to/repo/www/chapter_10/maintenance/views.py), we add the following to the payments() method. If there is a borrowerKey value available, we import GridFS and os, and retrieve borrower information (discussed earlier):
if borrowerKey :
from gridfs import GridFS
import os
borrower = userSvc.fetchUserByBorrowerKey(borrowerKey)
We then check to see whether any files have been uploaded. If so, we prepend the value of the borrowerKey property and store it in GridFS:
grid = GridFS(loanSvc.getDatabase())
if request.FILES and 'loan_doc' in request.FILES :
fn = request.FILES['loan_doc']
newFn = borrowerKey + '/' + os.path.basename(fn.name)
grid.put(fn, filename=newFn)
message = 'File uploaded'
We then get a list of only filenames, subsequently sent to the view template for rendering:
loan_docs = []
temp = grid.list()
for name in temp :
if name.find(borrowerKey) >= 0 :
loan_docs.append(os.path.basename(name))
Now that you have an idea of how to use GridFS from both the command line and from a program script, it's time to jump to the chapter summary.
In this chapter, you learned about the MongoDB NumberDecimal class, based upon the BSON Decimal128 class, used to store financial numbers with high precision. You also learned that not all programming languages provide direct support for this class, which means a conversion might be necessary in order to perform processing.
Next, you learned how to handle database operations across collections. Several approaches were covered, including building block, embedded document, database reference, and DBRef approaches. You also learned how to produce a single iteration of documents across collections using the $lookup aggregation pipeline operator. After this, you learned about secondary updates and where they might be performed, as well as common pitfalls when working with documents across collections. These include problems associated with ensuring uniqueness, orphaned documents or fields, and keeping values synchronized.
Finally, at the end of this chapter, you learned about GridFS, a technology provided by MongoDB that lets you store large documents directly in the database. You learned that GridFS files can be accessed using the mongofiles command from the command line, or via a programming driver. You then learned how to use the gridfs.GridFS class provided by the pymongo driver to store and then access files.
In the next chapter, continuing with the BigLittle Micro Finance Ltd. scenario, you'll learn how to secure connections to the database at both the user level as well as at the SSL/TLS level.
Security is an important consideration for any organization, especially companies conducting financial transactions over the internet, and also companies dealing with sensitive user data such as health service providers, counselling services, law firms, and so forth. This chapter focuses on the administration needed to secure a MongoDB database. First, you are shown how to secure the database by creating database users and how to implement role-based access control. After that, you learn how to secure the MongoDB database communications by implementing transport layer (SSL/TLS) security based upon X.509 certificates.
The following topics are covered in this chapter:
The minimum recommended hardware is as follows:
The software requirements are as follows:
Installation of the required software and how to restore the code repository for the book is explained in Chapter 2, Setting Up MongoDB 4.x. To run the code examples in this chapter, open a terminal window (Command Prompt) and enter these commands:
cd /path/to/repo
docker-compose build
docker-compose up -d
docker exec -it learn-mongo-server-1 /bin/bash
When you are finished working with the examples covered in this chapter, return to the terminal window (Command Prompt) and stop Docker as follows:
cd /path/to/repo
docker-compose down
The code used in this chapter can be found in the book's GitHub repository at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/chapters/11.
The first step in securing a MongoDB database is to enable security. By default, a MongoDB database is without transport layer security mechanisms and without any role-based access controls. This is by design, otherwise how would you be able to access the database in order to make it secure? Also, not having to deal with authentication is convenient for initial application development and database document design. If you implement an unsecured database in a production environment, you might end up being unemployed!
Before moving into a discussion on how to enable security, it is important to gain an understanding of the MongoDB authentication process, and what forms of authentication are available.
MongoDB authentication involves a number of different factors: the authentication database, the authentication mechanism, and the database user. The authentication database is the place where authentication information is stored. The authentication mechanism concerns how the authentication is to take place. Finally, the database user brings together one or more roles, the authentication mechanism, and the password. Before getting into the details, however, it's important to understand the difference between authentication and authorization.
An important distinction we need to make right away is to describe the difference between authentication and authorization:
In this section, we discuss authentication. In the next major section in this chapter, we discuss authorization, which, in MongoDB, takes the form of role-based access control. Now let's turn our attention to the authentication database.
The authentication database (https://docs.mongodb.com/manual/core/security-users/#user-authentication-database) can be any MongoDB database in which you have defined users and roles. The authentication database chosen for a given user depends on what level of access that user is to be assigned. If the user has privileges in many different databases, it doesn't make sense to define the same user multiple times in each database. Instead, it makes more sense to define the user in a single central database, and to specify the central database as the authentication database when attempting access.
On the other hand, it's quite possible that you wish to confine the activities of a particular database user to one single database. In this case, you could create the user in that one database, and assign the appropriate privileges. We now move our attention to the different authentication mechanisms available in MongoDB.
There are four primary authentication mechanisms (https://docs.mongodb.com/manual/core/authentication-mechanisms/#authentication-mechanisms) currently supported by MongoDB. Salted Challenge Response Authentication Mechanism (SCRAM) and X.509 Certificate Authentication are supported in all versions of MongoDB. Two additional mechanisms, Keberos and Lightweight Directory Access Protocol (LDAP), are available only in the Enterprise edition. Let's look at them individually:
As the focus in this book is on the MongoDB Community Edition, in this section we cover implementation only of the first two mechanisms. In order to enable database security, you need to know how to create a database user, discussed next.
When you create a database user, you bring together both authentication and authorization. The key command used for this purpose is a database command, createUser(). Here is the generic syntax:
db.createUser( { user_document }, { writeConcern_document } )
Both the user and writeConcern documents are JSON documents with a number of parameters. The following table summarizes user_document parameters.
| Parameter | Required | Notes |
| user | Yes | Username in the form of a text string. |
| pwd | Yes | Password in the form of a text string. When the password is actually stored in the database, it is first converted into a BCRYPT hash. MongoDB also allows you to insert a passwordPrompt() function in place of a text string. In this case, you are first prompted for the password before the insertion operation proceeds. |
| roles | Yes | A list of one or more roles to be assigned to this user. |
| customData | No | A JSON document with any additional information to store for this user (such as department, title, phone number, and so on). |
| authenticationRestrictions | No | A list consisting of a list of IP addresses and/or Classless Inter-Domain Routing (CIDR) ranges from which the user is allowed to connect. The restriction can apply directly to the client, in which case you need the clientSource sub-parameter, Otherwise, by using the serverAddress sub-parameter, the restriction applies to the server this database user uses as a connection. |
| mechanisms | No | Use this parameter to specify which SCRAM authentication can be used. Choices can be either SCRAM-SHA-1 or SCRAM-SHA-256. Note that these choices are in effect regardless of the authenticationMechanisms setting in the mongod config file. |
| passwordDigestor | No | This setting controls where the password hash is created when a plain text password is received. The default is server. If using SCRAM-SHA-1 you can specify either client or server. |
The second document is optional and can be used to specify the behavior of write concerns. Here is the prototype for the writeConcern document:
{ w: <int | string>, j: <boolean>, wtimeout: <number of milliseconds> }
Here is a brief summary of these three arguments visible in the writeConcern document:
If you are passing a write request to a replica set, any value above 1 signals that many servers must acknowledge. So, for example, a value of 3 indicates that the primary and two secondaries must acknowledge the write operation.
If the value for w is not a string, it must be either majority or a custom write concern name. In this case, the value assigned to w would be the number of servers in the custom write concern that is named, or the number of servers considered to be a majority within the given replica set.
Let's now have a look at how to enable security in a MongoDB database.
In order to enable database security, you need to start the mongod instance without security, create at least one user in the authentication database with the userAdminAnyDatabase role and restart mongod with security enabled. You can then authenticate as the new admin user and create additional users and make assignments as needed.
Here are the detailed steps you should follow to enable database security:
Here is an example on an Ubuntu 18.04 server:

superMan = {
user : "superMan",
pwd : "password",
roles : [ { role : "userAdminAnyDatabase", db : "admin" },
"readWriteAnyDatabase" ]
}
db.createUser(superMan);
The following screenshot shows what you might see at this point:




Looking at the screenshot, note that of the last four entries, WiredTiger.turtle has reverted to root. All files in the directory where mongod expects to find its database files must be set to the mongodb user and group.

You now have a good idea on what needs to be done to enable database security. Next, you will learn how to configure access control based on roles.
MongoDB role-based access control (https://docs.mongodb.com/manual/core/authorization/#role-based-access-control) involves three main factors: role, resource, and rights. In the documentation on security, you see rights referred to as actions, privileges, and also privilege actions. To form a mental picture of a role, picture managing a server. The person who creates users and assigns filesystem rights assumes the administrator role. However, in a small company, this person could also manage the accounting department, and thus also assumes the role of accounting manager.
A resource, in the context of MongoDB security, is most often a database or collection. Resources could also be a server cluster or a built-in resource such as anyResource. The most complicated to understand are rights, thus we begin our discussion with privilege actions.
MongoDB privilege actions fall into two main categories: operational and maintenance. Rights in the operational category are documented under Query and Write Actions. Maintenance rights are documented starting with the Database Management Actions section. See the documentation on privilege actions (https://docs.mongodb.com/manual/reference/privilege-actions/) for more information.
We first look at operational rights.
Operational rights are the set of rights most often assigned by DevOps, and are the ones most often used when writing application code. The four primary rights in this category are summarized in the following table. As you scan this table, please bear in mind that if a given database command is affected, it has a ripple effect on any mongo shell methods or programming language driver leveraging the given database command.
| Right | Database Command(s) Affected |
| find | Allows the user assigned this right the ability to query the database. Database commands enabled by this assignment include find, geoSearch, and aggregate. Other less obvious commands also affected include count, distinct, getLastError, mapReduce, listCollections, and listIndexes, to name a few. |
| insert | When this right is assigned, the database user is able to add documents to a collection using the insert or create database commands. It is interesting to note that the aggregation database command is also affected if the $out pipeline stage operator is used. |
| remove | The primary database commands affected by this right remove and findAndModify. However, mongo collection-level shell methods such as mapReduce() and aggregate() are also affected if directed to write results to a collection if the collection already exists and replacements are made. |
| update | This affects the update and findAndModify database commands. The mongo shell db.collection.mapReduce() method is also affected when outputting results to a collection. |
We now turn our attention to maintenance rights.
The list of maintenance rights is much more extensive than the operational rights listed in the previous section. In order to conserve space, a summary of the more important rights is presented here:
| Right | User With This Assignment Can ... |
| changePassword | Change passwords of users. |
| createCollection, createIndex, createRole, createUser |
Create collections, indexes, roles, or users. The user with createIndex can create indexes, the user with createUser can create users, and so on. |
| dropCollection, dropRole, dropUser | Drop a collection, role, or user. |
| grantRole, revokeRole | Assign roles (grantRole) or take a role away (revokeRole). The grantRole right is a very dangerous ability to assign. Please exercise caution and do not assign this right indiscriminately! |
| replsetConfigure | Make changes to the configuration of a replica set. |
| addShard, removeShard | Add or remove shards from a sharded cluster. |
| dropConnection, dropDatabase, dropIndex | Drop a connection, database, or index. |
| shutdown | Shut down a database. |
| listSession | List sessions of a cluster resource. |
| setFreeMonitoring | Enable or disable free monitoring on a cluster resource. |
| collStats, indexStats, dbStats | Gather statistics on collections, indexes, or databases. |
| listDatabases, listCollections, listIndexes | Get a list of databases, collections, or indexes. |
Please note that this list is by no means comprehensive. We have covered barely a third of all possible actions. It would be well worth your while to consult the documentation on privilege actions (https://docs.mongodb.com/manual/reference/privilege-actions/#privilege-actions) for more information.
The next subsection describes a series of built-in roles consisting of pre-defined sets of the rights described in the previous tables.
MongoDB provides built-in roles (https://docs.mongodb.com/manual/reference/built-in-roles/#built-in-roles) that represent preset combinations of rights. Some of the built-in roles are available on a specific database, others apply to any database. Management roles are available to enable individual users to perform backup and restore as well as replica sets and/or sharded clusters. Let's now examine built-in roles that apply to a specific database.
The read and readWrite built-in roles can be applied to the database in use or to the database designated specifically when creating a user. Here is a summary of these two roles:
Let's look at an example using database-specific rights.
For our first scenario, let's say that the management appoints an admin to run financial reports on the biglittle database. However, the database server also hosts another database to which this admin should not have rights. Here is an example of how the rights might be assigned:
bgReader = {
user : "bgReader",
pwd : "password",
roles : [ { role:"read", db:"biglittle" } ],
mechanisms: [ "SCRAM-SHA-256" ]
}
db.createUser(bgReader);
This user can now authenticate to the server using the mongo shell, specifying the -u and -p parameters. In addition the user needs to identify the authentication source using the --authenticationDatabase parameter. As you can see from the screenshot shown here, the new user, bgReader, can access the biglittle database and issue the findOne() command:

However, if the same user attempts to insert something into the database, the operation fails and an authentication error appears, similar to that shown in the next screenshot:

Likewise, if the bgReader user attempts any operation on another database, unless assigned rights to that database specifically, the operation fails. It might become too cumbersome to have to visit every single database, and create the same user over and over again. Accordingly, MongoDB also provides roles that apply to all databases, which we examine next.
The read and readWrite built-in roles can be applied to the database in use or to the database designated specifically when creating a user. Here is a summary of these two roles:
The next subsection looks at how to create a user with all database rights.
For our next scenario, let's say that management wants a senior admin to have the ability to not only run financial reports on any hosted databases, but have the ability to perform create, update, and delete operations as well. Here is an example of how the rights might be assigned:
use admin;
rwAll = {
user : "rwAll",
pwd : "password",
roles : [ "readWriteAnyDatabase" ],
mechanisms: [ "SCRAM-SHA-256" ]
}
db.createUser(rwAll);
This user can now authenticate to the server using the mongo shell, specifying the -u and -p parameters. As you can see from the following screenshot, the rwAll user can perform read and write operations on both the sweetscomplete and biglittle databases:

Administrative built-in roles are examined in the next subsection.
The remaining set of built-in roles falls into the general category of administration. Here is a summary of the built-in most commonly assigned administrative roles:
Next, we look at what's involved in enabling SCRAM authentication.
As mentioned in the previous subsection, SCRAM is the default authentication mechanism. The only thing you need to do is to identify the type of hash when creating a database user. When defining a new database user, add a mechanisms key, and set the value to either SCRAM-SHA-1 or SCRAM-SHA-256. The SHA-1 hashing algorithm is no longer considered secure; however, you may need this option for backwards compatibility with older hashed passwords. The SHA-256 algorithm is recommended.
If using SCRAM-SHA-1, you can also set a value of scramIterationCount, which has a minimum value of 5000 and a default value of 10000. The higher the value, the more secure is the hash. The higher the value, the longer it takes to calculate, so you'll need to arrive at a good balance between security and performance. The default is recommended.
Likewise, if using SCRAM-SHA-256, you can set a value of scramSHA256IterationCount, which has a minimum value of 5000 and a default value of 20000. Here is an example creating a user using SCRAM-SHA-256 with an iteration count of 10000:
db.adminCommand( { setParameter: 1, scramSHA256IterationCount: 10000 } );
scramUser = {
user : "scramUser",
pwd : "password",
roles : [ "readAnyDatabase" ],
mechanisms: [ "SCRAM-SHA-256" ]
}
db.createUser(scramUser);
// set iteration back to default
db.adminCommand( { setParameter: 1, scramSHA256IterationCount: 15000 } );
At this point, you have an idea of how MongoDB role-based access control works. The next topic is setting up secure communications using SSL/TLS and X.509 certificates.
In this section, you'll learn how to configure a mongod (MongoDB server daemon) instance to communicate with clients and peers in a secure manner. In this section, we discuss how to secure communications to and from a mongod instance. We also discuss securing communications between the mongo shell and a mongod instance. The sections that follow cover who can connect to the MongoDB database, and what that person (or role) is allowed to do.
Before we get going on the details, however, we need to first examine what is meant by transport layer.
The transport layer in TCP/IP networks sits below the application layer (that is, TCP sits just below HTTP) and above the internet layer (that is, TCP sits above Internet Protocol (IP)). The two possibilities are either Transmission Control Protocol (TCP), defined by RFC 793, or User Datagram Protocol (UDP), defined by RFC 768. The main difference between these two is that TCP is slower, but provides reliable transport, in that there is a packet sequencing control aspect not present in UDP. UDP, on the other hand, although not considered reliable, is faster.
Secure Sockets Layer (SSL) was first introduced in the early 1990s by Netscape as a way to secure communications between browser and server (see RFC 8446 for more details). Its goal was to provide privacy and data integrity to secure transport (such as TCP). Version 1.0 was never published, but version 2.0 was first made available in 1995, followed a year later by version 3.0, defined by RFC 6101.
Security vulnerabilities soon cropped up, however, and work began on a replacement for SSL, which became known simply as Transport Layer Security (TLS), starting with version 1.0 in 1999. TLS versions 1.0 and 1.1 are now considered compromised, however, and are expected to be deprecated in 2020. It is recommended that you use at a minimum version 1.2, and later versions if supported.
Starting with MongoDB version 4.0, support for TLS 1.0 is disabled. Also, as of MongoDB 4.0, native operating system libraries are used for TLS support. Thus, if your OS supports TLS 1.3, MongoDB automatically supports it as well. The libraries used are as follows:
MongoDB provides automatic support for a number of extremely secure TLS algorithms, including Ephemeral Elliptic Curve Diffie-Hellman (ECDHE) and Ephemeral Diffie-Hellman (DHE). If you need to provide specific settings in order to influence these algorithms, a command-line parameter, opensslCipherConfig, is available. A complete discussion of all possible settings is well beyond the scope of this book.
Now let's take a quick look at what is needed in terms of certificates.
In order to secure communications between mongod or mongos instances, you need a public key certificate (also called an X.509 certificate). This is also often misleadingly referred to as an SSL certificate. It's best to use a certificate that is signed by a trusted certificate authority (CA). However, if you are setting up a MongoDB database just for testing purposes, or if you are only developing inside a company network, a self-signed certificate might be sufficient.
To generate a self-signed certificate, use the server's operating system SSL library. Here is an example script, found at /path/to/repo/chapters/11/install_ssl_cert.sh, that installs test certificates for both client and server:
#!/bin/bash
echo "Generating SSL certificates ..."
export RAND_DIGITS=`date |cut -c 18-20`
export EMAIL_ADDR="doug@unlikelysource.com"
mkdir /etc/.certs
cd /etc/.certs
echo $RAND_DIGITS >file.srl
touch /root/.rnd
openssl req -out ca.pem -new -x509 -days 3650 -subj \
"/C=TH/ST=Surin/O=BigLittle/CN=root/emailAddress=$EMAIL_ADDR" \
-passout pass:password
openssl genrsa -out server.key 2048
openssl req -key server.key -new -out server.req -subj \
"/C=TH/ST=Surin/O=BigLittle/CN=$HOSTNAME/emailAddress=$EMAIL_ADDR"
openssl x509 -req -in server.req -CA ca.pem -CAkey privkey.pem \
-CAserial file.srl -out server.crt -days 3650 -passin pass:password
cat server.key server.crt > server.pem
openssl verify -CAfile ca.pem server.pem
openssl genrsa -out client.key 2048
openssl req -key client.key -new -out client.req -subj \
"/C=TH/ST=Surin/O=BigLittle/CN=client1/emailAddress=$EMAIL_ADDR"
openssl x509 -req -in client.req -CA ca.pem -CAkey privkey.pem \
-CAserial file.srl -out client.crt -days 3650 -passin pass:password
cat client.key client.crt > client.pem
openssl verify -CAfile ca.pem client.pem
chmod -R -v 444 /etc/.certs
Although self-signed certificates are fine for the development environment, it's vital that production servers have a certificate signed by a recognized and trusted certificate authority. There is an excellent, free, open source certificate authority known as Let's Encrypt (https://letsencrypt.org/) that is extremely widely used. The main downside to Let's Encrypt is that the certificates they generate are generally only good for 90 days, meaning you must put into place some mechanism to renew the certificate. The Let's Encrypt website offers more information on how this can be done.
Other alternatives would be to conduct a search for companies that offer SSL certificates for sale. Preferably your own web hosting provider might offer commercial SSL certificates that can be installed on your website. Let's now have a look at how to configure a mongod server instance to communicate using TLS.
Once you have a valid SSL certificate installed on your server, whether self-signed or generated by a recognized signing authority, you are in a position to secure the mongod server instance. If you are using self-signed certificates, please be aware that although communications to and from the server are encrypted, no identity validation takes place. This means the communications chain can potentially be subject to man-in-the-middle attacks.
No matter what operating system is used, you can use Privacy-enhanced Electronic Mail (PEM) key files to establish secure communications. For Windows or Mac servers, you have the option to configure secured communications using the operating system certificate store. In addition, by adding an additional configuration parameter, the server can force the client to present a certificate for identity verification.
In order to configure a mongod or mongos instance to use TLS, add the appropriate net.tls settings to the MongoDB config file (such as /etc/mongod.conf). For an overview, see the documentation page on Procedures Using net.tls Settings.
Please note that as an alternative to setting parameters in the MongoDB config file, you can also use the equivalent command-line switches. The following table summarizes useful net.tls settings and their equivalent command-line switches:
| net.tls.<setting> | Command Line | Notes |
| mode | --tlsMode | Possible values include disabled, requireTLS, allowTLS, and preferTLS. requireTLS refuses any connection that is not using TLS. Use requireTLS if connections default to insecure, but where there might be some TLS connections involved. Use preferTLS if connections are secure by default, but where there might be some non-TLS connections involved. If your setup does not use TLS at all, use disabled (default). |
| certificateKeyFile | --tlsCertificateKeyFile | The value is a string representing the full path to the PEM file containing both the certificate and key. Cannot be used with the certificateSelector option. |
| certificateSelector | --tlsCertificateSelector | This option is only available on a Windows or Mac server and allows MongoDB to select the certificate from the local certificate store. Values under this option include subject, a string representing the name associated with the certificate in the store, and thumbprint, a hex string representing SHA-1 hash of the public key. See the next subsection for more details on this option. You cannot use this with the certificateKeyFile option. |
| certificateKeyFilePassword | --tlsCertificateKeyFilePassword | The value associated with this option is a string representing the password if the certificate is encrypted. If the certificate is encrypted and this option is not present, you are prompted for the password when the mongod (or mongos) instance first starts on Linux installations. If the certificate is encrypted and MongoDB is running on a Windows or Mac server, this option must be set. It's important to note that this password is removed from any MongoDB log files. |
| CAFile | --tlsCAFile | Use this option if you need to specify the root certificate chain from the Certificate Authority being used. |
| allowInvalidCertificates | --tlsAllowInvalidCertificates | If an invalid certificate is presented by the client, and this setting is true, a TLS connection is established, but client X.509-based authentication fails. |
| allowInvalidHostnames | --tlsAllowInvalidHostnames | If this setting is true, MongoDB disables verifying that the client hostname matches the one specified in the certificate. |
| disabledProtocols | --tlsDisabledProtocols | Options accepted by this parameter include none, TLS1_0, TLS1_1, TLS1_2, and TLS1_3. You can specify multiple protocols to disable by separating them with commas. MongoDB 4 automatically disables TLS 1.0 if TLS 1.1 or greater is available. You can only specify protocols that your OS and your version of MongoDB support. If you specify none, all protocols are allowed, including TLS 1.0 (now considered insecure). |
Examples of these settings follow in the next subsections. Now let's examine how to use PEM key files.
Once you have access to valid server certificate and key files, they are generally combined into a single file referred to as a PEM file. Unfortunately, there are a number of variants of the PEM format, so you'll have to consult the documentation for the server's operating system. As an example, assuming you ran the install_ssl_cert.sh script described earlier, let's add the following to the existing MongoDB config file:
net:
port: 27017
bindIp: 0.0.0.0
tls:
mode: requireTLS
certificateKeyFile: /etc/.certs/server.pem
CAFile: /etc/.certs/ca.pem
Before you restart the mongod instance, be sure to perform a proper shutdown as follows:
mongo admin
> db.shutdownServer();
> exit
Notice that after we restart the mongod instance, when we try to connect using a client that does not connect using TLS, the connection is rejected, as seen in the screenshot:

If we subsequently modify the MongoDB config file, and change the setting for mode to allowTLS, a connection is allowed.
Let's now look at configuring the client to enforce an end-to-end TLS connection.
Now that you know how to configure the database server to communicate using a secure TLS connection, it's time to have a look at how the client can connect over a TLS connection. For the purposes of this discussion, we focus on connecting using the mongo shell. The next chapter, Chapter 12, Developing in a Secured Environment, covers how to connect over a TLS connection using the PyMongo client and an X.509 certificate.
For command line mongo shell connections over a secure TLS connection, command-line switches are summarized in the following table:
| Switch | Arguments | Notes |
| --tls | -- | Causes the mongo shell to request a TLS handshake from the target mongod or mongos instance. |
| --tlsCertificateKeyFile | string | The value represents the full path to the PEM file containing the client certificate and key. |
| --tlsCertificateKeyFilePassword | string | The value associated with this option is a string representing the password if the client certificate is encrypted. If the certificate is encrypted and this option is not present, you are prompted for the password from the command line. |
| --tlsCAFile | string | Use this option if you need to specify the full path and filename for the Certificate Authority file on the server used to verify the client certificate. |
| --tlsCertificateSelector | string | This option is only available on a Windows or Mac server and allows the mongo shell to select the certificate from the local certificate store. |
Here is an example connection using the test certificates described in Appendix C of the MongoDB security documentation in the article entitled OpenSSL Client Certificates for Testing. This example assumes the hostname is server.biglittle.local, the CA file is in the /etc/.certs directory, and the client certificate is in the home directory:
mongo --tls --tlsCertificateKeyFile /etc/.certs/client.pem \
--tlsCAFile /etc/.certs/ca.pem --host server1 --quiet
Here is a screenshot of the result:

In this chapter, you learned how to secure a MongoDB database. This involves enabling security and providing a mechanism for both authentication and authorization. You learned how to create a database user and what database rights (or action privileges) are available, as well as built-in roles. You learned about built-in roles that affect a single database, or all databases, and also roles involved in maintenance. In the last section, you learned how to enable secure communications to and from a MongoDB database using TLS and X.509 certificates.
In the next chapter, you'll put this knowledge to use by writing program applications that require security.
In this chapter, you will learn how to write applications that access a database in which role-based access control has been implemented. In addition, this chapter shows you how to develop an application where the client-to-database communications are secured at the transport layer using x.509 certificates.
Building on the work started in the previous chapter, Chapter 11, Administering MongoDB Security, in this chapter, you will learn how to configure a Python application to use different database users with different sets of permissions. In addition, you will learn how to configure a Python application to communicate using an encrypted TLS connection using x.509 certificates.
The topics covered in this chapter include the following:
The minimum recommended hardware is as follows:
The software requirements are as follows:
The installation of the required software and how to restore the code repository for the book is explained in Chapter 2, Setting Up MongoDB 4.x. To run the code examples in this chapter, open a Terminal window (Command Prompt) and enter these commands:
cd /path/to/repo
docker-compose build
docker-compose up -d
docker exec -it learn-mongo-server-1 /bin/bash
When you are finished working with the examples covered in this chapter, return to the Terminal window (Command Prompt) and stop Docker, as follows:
cd /path/to/repo
docker-compose down
The code used in this chapter can be found in the book's GitHub repository at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/chapters/12.
One of the golden security rules of web application development is the principle of least privilege. According to the United States Department of Homeland Security, Cyber Infrastructure Division, this principle is defined as follows:
In the context of a MongoDB database application, this means assigning a role to a database user with the least amount of rights needed to perform the task. In this section, we will discuss how to set up the MongoDB database and configure an application to perform queries with the minimum amount of rights.
When an application conducts queries only, there is no need to write anything to the database. Accordingly, we need to find a privilege action (that is, right) allowing a user to read from the database. The most likely right would be find. This right allows us to run the pymongo.collection.find() and pymongo.collection.aggregate() methods. This is perfect for an application that only needs to return the results of a query.
We do not assign this right directly to a user, however; instead, we select a role that includes this right, and assign the role to a database user. With this in mind, we will now consider the read built-in role. This role includes the find action privilege (or right), and also allows related operations, such as the ability to list collections and manage cursors (for example, the results of a query).
In order to create this user using the mongo shell, proceed as follows:

doc = {
user: "biglittle_reader",
pwd: passwordPrompt(),
roles: [ { role: "read", db: "biglittle" } ],
authenticationRestrictions: [ {
clientSource: ["127.0.0.1"],
serverAddress: ["127.0.0.1"]
}],
mechanisms: [ "SCRAM-SHA-256" ],
passwordDigestor: "server"
}


Next, we will look at configuring the application to use the newly created database user.
To configure an application to use the newly created database user, proceed as follows:
import pprint
from pymongo import MongoClient
client = MongoClient('localhost');
find_result = client.biglittle.users.find_one({},{"name":1,"address":1})
pprint.pprint(find_result)

import pprint
from pymongo import MongoClient
client = MongoClient(
'localhost',
username='biglittle_reader',
password='password',
authSource='biglittle',
authMechanism='SCRAM-SHA-256');
find_result = client.biglittle.users.find_one({},{"name":1,"address":1})
pprint.pprint(find_result)

Let's now look at configuring an application to use a user with privileges restricted to a single database.
In the following example, the database application is configured to use a user, bgReadWrite. This database user has limited access to the database as it is assigned the readWrite built-in role. Further, this user is only able to use the biglittle database. In the example you see in the next line, we create a MongoClient instance assigned the bgReadWrite user:
import pprint
from pymongo import MongoClient
client = MongoClient(
'localhost',
username='bgReadWrite',
password='password',
authSource='biglittle',
authMechanism='SCRAM-SHA-256');
All the data is first removed from test_collection in the biglittle database and a single document is inserted. This demonstrates the ability of the application to write:
client.biglittle.test_collection.delete_many({})
insert_result = client.biglittle.test_collection.insert_one( {
'first_name' : 'Fred',
'last_name' : 'Flintstone'
})
This is followed by a call to pymongo.collection.find_one(), demonstrating the ability to read, as seen here:
find_result = client.biglittle.test_collection.find_one()
pprint.pprint(insert_result)
pprint.pprint(find_result)
The results of this successful operation are shown in the following screenshot:

However, if in the same application we modify the code to access the sweetscomplete database, with the same user, the results show a failure, as in the following screenshot:

Next, we will examine how to configure an application with expanded rights.
When discussing expanded rights, there are three aspects we must consider, expanding the rights to include the following:
All of these involve MongoDB role-based access control. In all of these cases, the primary question you need to ask is what is the application expected to do? Let's first examine expanding the rights to incorporate multiple databases.
Strictly following the principle of least privilege means not assigning a role that encompasses more than one database. For practical reasons, however, the administrative cost involved in creating and maintaining a large number of least-privilege database users in every single database might be excessive. Another factor to consider is that the more database users you need to create, the greater the chance of making a mistake is. Mistakes could include assigning an incorrect mix of users, roles, and applications. This could not only result in applications not working but could also cause an application to have far greater rights than are merited. The greater security problem posed in such a situation might outweigh the benefit of strict adherence to the principle of least privilege.
There are two primary built-in roles that allow a database user to read and/or write to all databases: readAnyDatabase and readWriteAnyDatabase, described in the previous chapter. Here is an example of a mongo shell command that creates a database user, read_all, assigned the readAnyDatabase role:
doc = {
user: "read_all",
pwd: "password",
roles: [ "readAnyDatabase" ],
mechanisms: [ "SCRAM-SHA-256" ]
}
db.createUser(doc);
The following code example is configured to use the read_all user. In this example, we retrieve the first document from the users collection in the biglittle database. Immediately after, you see a find_one() method call that retrieves the first document from the customers collection in the sweetscomplete database:
import pprint
from pymongo import MongoClient
client = MongoClient(
'localhost',
username='read_all',
password='password',
authSource='admin',
authMechanism='SCRAM-SHA-256');
result_biglittle = client.biglittle.users.find_one({},{"name":1,"address":1})
result_sweets = client.sweetscomplete.customers.find_one({},
{"firstName":1,"lastName":1,"city":1,"locality":1,"country":1})
pprint.pprint(result_biglittle)
pprint.pprint(result_sweets)
Here is a screenshot of the results of the test code proving that this application can read from multiple databases:

Next, we will look at applications that perform database and collection administration.
There are three primary roles, discussed in Chapter 11, Administering MongoDB Security, that allow the assigned database user to perform database administration:
Their abilities include operations such as gathering statistics, running queries, and managing indexes, all the way up to dropping the database.
In this mongo shell script example, a user, biglittle_owner, is created and assigned the dbOwner role to the biglittle database:
doc = {
user: "biglittle_owner",
pwd: "password",
roles: [ { role : "dbOwner", db : "biglittle" } ],
mechanisms: [ "SCRAM-SHA-256" ]
}
db.createUser(doc);
We next create a demo program associated with this user, as follows:
import pprint
import pymongo
from pymongo import MongoClient
client = MongoClient(
'localhost',
username='biglittle_owner',
password='password',
authSource='biglittle',
authMechanism='SCRAM-SHA-256');
client.biglittle.loans.create_index([
('borrowerKey', pymongo.ASCENDING),('lenderKey', pymongo.ASCENDING)])
count_result = client.biglittle.loans.count_documents({});
stats = client.biglittle.command('collstats', 'loans')
print("\nNumber of Documents: " + str(count_result))
pprint.pprint(stats)

Finally, we will look at applications that involve the administration of database users.
As you can imagine, allowing an application to actually create database users and assign rights presents a potentially massive security risk. However, there may be situations in which you need to create a GUI frontend that allows customers to easily manage these aspects of their database. Accordingly, we will now turn our attention to the following built-in roles: userAdmin and userAdminAnyDatabase.
It is much better to assign the application userAdmin rights to a specific database, rather than create an application assigned userAdminAnyDatabase. The only exception might be a situation where you need an application that can perform as a superuser. In such cases, it's extremely important to severely limit access to this application.
As in the previous sub-section, we will define a simple application to demonstrate the concept. From the mongo shell, we first define a user, biglittle_admin, assigned the dbOwner role, to the biglittle database. In this case, we do not wish to use userAdmin as the application also needs the ability to read the database as well. A user assigned the dbOwner role works well in this case because this role encompasses both read and userAdmin for a specific database:
doc = {
user: "biglittle_owner",
pwd: "password",
roles: [ { role : "dbOwner", db : "biglittle" } ],
mechanisms: [ "SCRAM-SHA-256" ]
}
db.createUser(doc);
We then create a demonstration command-line application that allows the administrator running the application to define a user assigned either the read or readWrite roles:
import sys
import pprint
from pymongo import MongoClient
client = MongoClient(
'localhost',
username = 'biglittle_owner',
password = 'password',
authSource = 'biglittle',
authMechanism = 'SCRAM-SHA-256');
if len(sys.argv) != 4 :
print("Usage: test_db_admin.py <username> <password> <r|rw>\n")
sys.exit()
if sys.argv[3] == 'rw' :
newRole = 'readWrite'
else :
newRole = 'read'
roleDoc = [ { "role" : newRole, "db" : "biglittle" } ]
mechDoc = [ "SCRAM-SHA-256" ]
result = client.biglittle.command("createUser", createUser=sys.argv[1], pwd=sys.argv[2], roles=roleDoc, mechanisms=mechDoc)
print ("\nResult:\n")
pprint.pprint(result);


In the next section, we will look at configuring applications to communicate using TLS.
In order to have your applications communicate with the MongoDB database using a secured TLS connection, you first need to either generate or otherwise obtain the appropriate x.509 certificates. You then need to configure your MongoDB server to communicate using TLS, as described in Chapter 11, Administering MongoDB Security. In this section, we will examine how to configure the PyMongo client for TLS communications. First, let's look at the configuration options.
The TLS configuration options for pymongo.mongo_client.MongoClient() are in the form of keyword arguments (for example, **kwargs). Here is a summary of the possible key/value pairs:
| Key | Value |
| host | Typically, the DNS address of the server running the mongod instance to which you want to connect. It's extremely important that the hostname specified here matches one of the DNS settings in the [ alt_names ] section of the configuration file used to generate the server certificate. |
| tls | Set this key to True if you wish to have the client connect via TLS. The default is False. |
| tlsCAFile | Specifies the certificate authority file, typically in PEM format. |
| tlsCertificateKeyFile | Specifies the file containing the client certificate and private key, signed by the certificate authority (CA) specified previously. |
| tlsAllowInvalidCertificates | If this option is set to True, it instructs MongoDB to allow the connection to be made regardless of certificate validity. This is useful for test and development, but not recommended for production. |
| tlsAllowInvalidHostnames | If this option is set to True, certificate validation is disabled. This is useful for test and development, but not recommended for production. |
| tlsInsecure | Implies tlsAllowInvalidCertificates = True and tlsAllowInvalidHostnames = True. |
| tlsCertificateKeyFilePassword | If your certificate is encrypted with a password, the password can be placed here. |
| tlsCRLFile | Use this option if you using certificate revocation lists. |
Now that you have an idea of the possible TLS settings, let's look at configuring an application to communicate using TLS.
Here is an example of a client application connecting using TLS:
import pprint
from pymongo import MongoClient
client = MongoClient(
username='read_all',
password='password',
authSource='admin',
authMechanism='SCRAM-SHA-256',
host='server.biglittle.local',
tls=True,
tlsCAFile='/etc/.certs/test-ca.pem',
tlsCertificateKeyFile='/home/ked/test-client.pem');
result_biglittle = client.biglittle.users.find_one({},{"name":1,"address":1})
pprint.pprint(result_biglittle)
Here is a screenshot showing the results of the test run:

For this example, we modified /path/to/repo/chapters/12/test_read_all.py (described earlier in this chapter) to now incorporate TLS options. As you can see from the modified version of the file shown here, we configure the application to use the read_all user, assigned the readAnyDatabase role. TLS settings include enabling TLS, identifying the server, the location of the CA, and the client key.
That concludes our discussion of configuring an application to communicate securely using x.509 certificates.
In this chapter, you learned how to configure an application to connect as a database user. As you learned, the security of your application is contingent upon which database user is selected and what roles are assigned to these users. You were shown examples with minimal rights, followed by another section demonstrating applications with expanded rights. In the last section of this chapter, you learned how to configure an application to connect over a TLS connection using x.509 certificates.
In the next chapter, you will learn how to safeguard your database by deploying a replica set.
In this chapter, you will learn how to deploy a replica set. Initially, the focus of this chapter is the bigger picture: what a replica set is, how it works, and why use it. In the first section, you are given information to assist in developing a business rationale for deploying a replica set. Following that, you are shown how to use Docker to model a replica set for testing and development purposes. The remainder of the chapter focuses on configuration and deployment.
In this chapter, we will cover the following topics:
The minimum recommended hardware is as follows:
The software requirements are as follows:
The installation of the required software and how to restore the code repository for this book is explained in Chapter 2, Setting Up MongoDB 4.x. To run the code examples in this chapter, open a Terminal window (Command Prompt) and enter these commands:
cd /path/to/repo/chapters/13
docker-compose build
docker-compose up -d
This brings up three Docker containers that are used as part of the demonstration replica set configuration. Further instructions on the replica set setup are given in this chapter. When you are finished working with the examples covered in this chapter, return to the Terminal window (Command Prompt) and stop Docker, as follows:
cd /path/to/repo/chapters/13
docker-compose down
The code used in this chapter can be found in the book's GitHub repository at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/chapters/13.
As you will learn in this section, a replica set refers to three or more mongod instances hosting copies of the same databases. Although there are significant advantages to this arrangement, implementing a replica set incurs higher costs. Therefore, before you deploy a replica set, it's important to establish a solid business rationale. To start this discussion, let's have a look at what exactly a replica set is and what its potential benefits are.
A replica set is the concrete implementation of the larger topic of replication. Replication is the process of creating copies of a dataset. Each replica set consists of a primary and one or more secondaries. The recommended minimum is three mongod instances: one primary and two secondaries.
As you can see from the following diagram, the application client (for example, a Python application using the PyMongo client) conducts queries (reads) and saves data (writes) to the MongoDB database without being aware that a replica set is involved:

The read and write requests are by default accepted by the primary. Confirmation of availability between members of the replica set is maintained by the equivalent of a TCP/IP ping, referred to in the MongoDB documentation as a heartbeat. The primary maintains a log of performed operations called the operations log (oplog), which is shared between replica set members. The process of synchronization involves secondaries performing operations based on the shared oplog.
Let's now have a look at the benefits of implementing a replica set.
There are four primary benefits gained by deploying a replica set:
Now, we can have a look at how a replica set responds when one of its member servers goes down.
If a replica set member fails to respond to the regularly scheduled heartbeat within a predefined interval, the other members of the replica set assume it has failed and hold an election (https://docs.mongodb.com/manual/core/replica-set-elections/#replica-set-elections). A new primary is elected, which immediately starts accepting read and write requests and replicating its database. The following diagram illustrates the situation before and after an election:

Each member server has one vote. On the left side of the preceding diagram, the primary has gone offline. The remaining secondaries in the replica set hold an election. After the election, as shown on the right side of the diagram, the topmost secondary is elected as the new primary.
In order to avoid a voting deadlock (for example, where an even number of remaining servers are split over who becomes the new primary), it is possible to establish arbiters. An arbiter is a server that is part of the replica set, has a vote, but does not retain a copy of the data. Through replica set configuration (discussed later in this chapter), it is possible to add non-voting replica set members. You can also assign members a priority to influence the election process and to make it proceed more quickly.
Now, let's have a closer look at how data is synchronized between replica set members.
When the replica set is first initialized, all the data from all member servers is copied to all the replica set members. The only exception is the local database, which contains, among other things, the oplog. After the initial synchronization, when read and write requests are received by the primary, it first performs the operation requested by a mongo shell instance or application code, and then records the MongoDB commands actually executed in a special capped collection, local.oplog.rs, referred to as the oplog. The actual synchronization process itself does not involve copying actual data between the primary and its secondaries. Rather, synchronization consists of secondaries receiving a copy of local.oplog.rs. Once the copy is received, the secondary applies the operations in an asynchronous manner.
Let's now have a look at replica set design.
As mentioned earlier, the ideal replica set would consist of a minimum of three computers. To give an example of a bad design, consider three old computers, each with an old, outdated OS, sitting on a shelf next to each other in the same room. As you can deduce from this simple example, the number of computers in a replica set is not the only consideration. What we need to look at now are positive suggestions on what would comprise a smoothly operating replica set.
The elements of a proper replica set design are listed here:
If the majority of replica set members are not visible to a stranded member, this member switches to read-only mode. It could conceivably be converted to a standalone replica set, but this would have to be done manually, and could cause synchronization issues when connections are restored.
It would be impossible to always have an odd number of servers in the replica set, however, because you cannot predict exactly when, or how many, servers might go offline. Accordingly, other factors can be built into your replica set configuration that influence an election in order to prevent a deadlock.
One such factor that can be set manually in the configuration is priority. Other information that comes into play is performance metrics, to which the replica set members have access. For more information on the election process, see the Replica Set Elections article in the MongoDB documentation.
As you can imagine, hosting your database on a replica set can become potentially quite expensive. The next sub-section shows you how to establish a solid business rationale.
By this point, you are most likely convinced that implementing your database using a replica set makes good sense. In order to justify the purchase of the extra resources required to support a replica set, however, you need to determine the following:
In most cases, the amount of money associated with these four factors outweighs the costs involved in implementing a replica set. For a start-up company, however, where cash and resources are tight, it might not make business sense to invest in the extra resources needed for a replica set. Also, for companies that offer a public service, or for non-profit organizations, the considerations listed previously might not matter as customers are not paying money for the service, which means that any downtime, although not appreciated, is considered acceptable (or even expected!) to website visitors.
Next, we will have a look at how a developer can model a replica set using Docker.
A classic problem facing MongoDB developers is the lack of enough hardware to properly set up and test a replica set. Furthermore, even though some developers either have the requisite hardware or have access to a properly equipped lab, the computers available are often old and dissimilar, leading to inconsistent results during development and testing. One possible solution to this problem is to use virtualization technology to model a replica set.
The way to start this setup is to create a Dockerfile for each member of the replica set, examined next. You can find these files in /path/to/repo/chapters/13.
In order to build a Docker container, you generally need two things: an image and a file that describes a series of commands that populates the image with the software needed to perform the functions required of the new container. In this case, we're using mongo, the official Docker image published by MongoDB (https://hub.docker.com/_/mongo).
In the Dockerfile used for replica members, the FROM directive tells Docker what image to use as a basis for the new container. Next, we use the RUN directive to install the custom software packages needed for our replica set member. We first update apt-get and perform any upgrades needed. After that, a common set of network tools is installed, including ping:
FROM mongo
RUN \
apt-get update && \
apt-get -y upgrade && \
apt-get -y install vim && \
apt-get -y install inetutils-ping && \
apt-get -y install net-tools
Here is the generic mongod.conf file that is linked to the /etc folder of the new container (MongoDB configuration is discussed in Chapter 2, Setting Up MongoDB 4.x):
systemLog:
destination: file
path: "/var/log/mongodb/mongodb.log"
logAppend: true
storage:
dbPath: "/data/db"
journal:
enabled: true
net:
bindIp: 0.0.0.0
port: 27017
replication:
replSetName: learn_mongodb
Here is the hosts file, whose entries are later linked to /etc/hosts inside the Docker container:
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.16.0.11 member1.biglittle.local
172.16.0.22 member2.biglittle.local
172.16.0.33 member3.biglittle.local
Next, we will examine how to bring up multiple containers using Docker Compose.
Docker Compose is a scripting language designed to facilitate creating, running, and maintaining Docker containers. The main configuration file is docker-compose.yml. It is in YAML (YAML Ain't Markup Language) format, meaning key directives are followed by a colon (:). Sub-keys are indented directly underneath parent keys.
Here is the docker-compose.yml file used to bring up three mongod instances that simulate a replica set. This file is broken down into five main blocks: three to define each of the replica set members, a fourth to describe the virtual network, and a fifth block to describe volumes. At the top of the file, we place the version directive. This informs Docker Compose that this file follows directives compatible with version 3:
version: "3"
We then start with the definition for member1. In this definition, you will see the Docker container name, the hostname, the source image, port mappings, the location of the Dockerfile, and network information.
The networks directive is used to assign an IP address to the container. Under volumes, you can see the permanent location of the data, as well as mappings to a common /etc/hosts file and /etc/mongod.conf file:
services:
learn-mongodb-replication-member-1:
container_name: learn-mongo-member-1
hostname: member1
image: mongo/bionic:latest
volumes:
- db_data_member_1:/data/db
- ./common_data:/data/common
- ./common_data/hosts:/etc/hosts
- ./common_data/mongod.conf:/etc/mongod.conf.
ports:
- 27111:27017
build: ./member_1
restart: always
command: -f /etc/mongod.conf
networks:
app_net:
ipv4_address: 172.16.0.11
The definition blocks for member2 and member3 (not shown) are identical except that references to 1 are changed to 2 or 3. Finally, the last two definition blocks describe the virtual network and volumes. The volumes directive represents external directories on the host computer so that when the simulated replica set is brought up or down, data is retained. The networks directives describe the overall network into which each member server fits, as shown in the following code snippet:
networks:
app_net:
ipam:
driver: default
config:
- subnet: "172.16.0.0/24"
volumes:
db_data_member_1: {}
db_data_member_2: {}
db_data_member_3: {}
We can then use the docker-compose up command to build (if necessary) and bring the three containers online, as shown in the following screenshot:

Now that you have an idea of how to model a replica set using Docker, let's turn our attention to replica set member configuration and deployment.
In order to deploy the replica set, the mongod.conf file for each mongod instance needs to be updated with the appropriate replication settings. In this section, we will examine important settings in the mongod.conf files for each type of replica set member (for example, primary and secondary). After that is an overview of the steps needed to get a replica set up and running.
Although you could use command-line options to bring up members of a replica set, aside from temporary testing and development, the preferred approach would be to add the appropriate replication options to the mongod.conf file for each member of the replica set.
The following table summarizes the more important options:
| mongod.conf Directive | Command Line | Notes |
| net.bindIp | --bind_ip | The mongod instance running on each replica set member must be configured to listen either on a specific IP address or all IP addresses. This is required so that replica set members can both synchronize data and send each other heartbeat transmissions. |
| replication.oplogSizeMB | --oplogSize | If desired, define this setting with an integer value representing the desired oplog size in megabytes. In order for this directive to be effective, the option needs to be set before the replica set is first initialized. The replSetResizeOplog administrative method can be used to dynamically resize the oplog later as the replica set is running (described in the next sub-section). |
| replication.replSetName | --replSet | This mandatory directive indicates the name of the replica set. The value for this directive needs to be the same on all replica set members. |
| replication.enableMajorityReadConcern | --enableMajorityReadConcern | For most situations, avoid disabling support for the majority read concern. The default for this setting is true. If you wish to disable this support, configure this setting to a value of false. |
| storage.oplogMinRetentionHours | --oplogMinRetentionHours | By default, MongoDB does not set a minimum time to retain oplog entries. Introduced in MongoDB 4.4, this setting allows you to override the default behavior and force MongoDB to retain oplog entries for a minimum number of hours. |
The main reason why majority read concern support might need to be disabled is in a situation where MongoDB cache pressure (work load) could end up immobilizing a deployment in an endless cycle of cache reads and writes. Have a look at this Stack Exchange article, Why did MongoDB reads from disk into cache suddenly increase?, for a good idea of what cache pressure appears in terms of performance metrics (https://dba.stackexchange.com/questions/250939/why-did-mongodb-reads-from-disk-into-cache-suddenly-increase).
Another consideration, not discussed in this chapter, is to configure replica set members to communicate internally using a secured (for example, TLS) connection. Only use a TLS connection between replica set members if internal communications absolutely need to be secured, as it slightly degrades database performance. Otherwise, use appropriate network and routing techniques to isolate the network segment used by replica set members to communicate with each other. Please refer to the MongoDB documentation on Internal Membership Authentication (https://docs.mongodb.com/manual/core/security-internal-authentication/#internal-membership-authentication). Also, for more information, refer to Chapter 11, Administering MongoDB Security.
Now, it's time to have a look at the steps required to deploy a replica set.
The overall procedure to get a replica set up and running is as follows:
Let's start by confirming network connectivity between replica set members.
For the purposes of this chapter, as described earlier, we will use docker-compose up to bring the replica set members online. As our replica set consists of Docker containers, we can create a simple shell script (/path/to/repo/chapters/13/verify_connections.sh) to confirm connectivity between members:
#!/bin/bash
docker exec learn-mongo-member-1 /bin/bash -c "ping -c1 member2.biglittle.local"
docker exec learn-mongo-member-2 /bin/bash -c "ping -c1 member3.biglittle.local"
docker exec learn-mongo-member-3 /bin/bash -c "ping -c1 member1.biglittle.local"
Here is the expected output from this sequence of ping commands:

If it appears that the members are not able to ping each other, confirm that the network configuration in the docker-compose.yml file is correctly stated. Also, confirm that Docker is installed correctly on the development server, and that Docker has set up its own internal virtual network. Next, we will quickly revisit configuring replica set members.
We have already examined replication settings in detail in the Replication configuration settings section of this chapter. As we are now ready to deploy the replica set, a simple Docker command shows the current mongod.conf file settings. As a minimum, we need to ensure that database is listening on the correct IP address (or all IP addresses, as seen in the preceding screenshot). We also need to confirm that all replica set members have the replication.replSetName parameter set to the same name. The next screenshot shows the result of the following Docker command:
docker exec -it learn-mongo-member-1 /bin/bash -c "cat /etc/mongod.conf"
Here is the result of the preceding command:

Assuming all the replica set members have the appropriate configuration, we will now have a look at starting (or restarting) the mongod instances involved in the replica set.
At this point, you can start the mongod instance on each server according to the recommended procedure for the host OS. Thus, on a Windows server, for example, the mongod instance would be started as a service using the Windows Task Manager or an equivalent GUI option. In a Linux environment, you could simply type the following:
mongod -f /etc/mongod.conf
As mentioned earlier, when starting the mongod instance manually (or using a shell script), you can specify replication options as command-line switches. The preferred approach, however, is to place replication settings into the mongod.conf (or equivalent) file on each member of the replica set.
For the purposes of this chapter, as mentioned previously, we will start the three Docker containers using the following command:
docker-compose up
The result is shown here:

We are now ready to initiate the replica set.
Initiating a replica set involves connecting to one of the member servers in the replica using the mongo shell. If database security has been activated, be sure to log in as a user with the appropriate cluster administration role. The key command used to initiate the replica set is rs.initiate(). The generic syntax is as follows:
rs.initiate( <DOCUMENT> );
The rs.initiate() shell method is actually a wrapper for the replSetInitiate database command. If preferred, you can directly access this command using the following syntax from the shell (or from programming application code):
db.runComand( replSetInitiate : <DOCUMENT> )
The replica set configuration document supplied as an argument is in JSON format and can contain any of the following replica set configuration fields. We will first summarize the main replica set configuration fields.
Here is a summary of the top-line replica set configuration fields:
| Directive | Data Type | Notes |
| _id | String | Replica set name; corresponds to the replication.replSetName parameter in the mongod.conf file. |
| version | Integer | Represents a value designed to increment as the replica set is revised |
| protocolVersion | Number | As of MongoDB version 4.0, the protocol version must be set to 1 (default). This setting is in place so that future enhancements to the replication process can be made backward-compatible. |
| writeConcernMajorityJournalDefault | Boolean | The default value of true instructs MongoDB to acknowledge the write operation after a majority of replica set members have written a write request to their respective journals. All replica set members need to have journaling active if this value is set to true. However, if any member uses the in-memory storage engine, the value must be set to false. |
| configsvr | Boolean | If this replica set is used to host the config server for a sharded cluster, set the value to true. The default for this setting is false. |
| members | Document | JSON sub-document describing each member server in turn. |
| settings | Document | JSON sub-document describing additional settings. |
As you will have noticed, both members and settings are themselves JSON documents with their own settings. Let's now see an example of the members settings.
Here is a summary of the directives available in the members sub-document. As a minimum, you need to specify the _id and host for each replica set member:
| Directive | Data Type | Notes |
| _id | Integer | Represents an arbitrary unique identifier for this member within the replica set. Values can range from 0 to 255 (mandatory). |
| host | String | Either the hostname or host name:port for this replica set member. This hostname must be resolvable (for example, reachable using ping) (mandatory). |
| arbiterOnly | Boolean | Set this value to true if you wish this replica set member to serve as an arbiter (does not have a copy of the database, but participates in elections in order to help prevent a deadlock). The default is false. |
| buildIndexes | Boolean | Set this value to false if you do not wish to have indexes built on this replica set member. Bear in mind that this is only in cases where this member only serves a backup/high-availability purpose and does not receive any client queries. If you choose to set this value to false, the MongoDB team recommends also setting members.priority to 0 and members.hidden to true. The default for this setting is true. |
| hidden | Boolean | Set this value to true if you do not want this replica set member to receive queries. The default is false. |
| priority | Number | Values can range from 0 to 1000. Use this setting to influence the eligibility of this replica set member for becoming a primary if an election occurs. The higher the value, the greater the chance this member is elected the primary. The default value is 0 for an arbiter and 1 for primary/secondaries. |
| tags | Document | You can define a JSON document in the form { "tag":"value" [, "tag":"value"]}, allowing you to sub-group replica set members. Tags can then be used to influence read and write operations (see the Application program code access section later in this chapter). |
| slaveDelay | Integer | This value represents the number of seconds that updates to this replica set member should lag. Use this setting to create a delayed replica set member. This allows you to store active data representing some time period in the past. The default value is 0 (that is, no delay). |
| votes | Number | Represents the number of votes this replica set member can produce in the case of an election. Set this value to 0 to establish a non-voting member. Note that non-voting members must also have their members.priority value set to 0. The default value for members.votes is 1. |
Finally, let's have a look at the settings directives.
Here is a summary of the directives available in the settings sub-document:
| Directive | Data Type | Notes |
| chainingAllowed | Boolean | Set this value to false if you wish secondary replica set members to replicate only from the primary. The default value is true, meaning secondaries can update each other, referred to as chained replication. |
| heartbeatIntervalMillis | Integer | This value is set internally. |
| heartbeatTimeoutSecs | Integer | The number of seconds allowed without receiving a heartbeat before a replica set member is considered unreachable. The default value is 10. |
| electionTimeoutMillis | Integer | The number of milliseconds allowed to elapse without receiving a heartbeat before an election is called. This only applies to members using protocolVersion set to 1. The default value is 10000 (that is, 10 seconds). Set this value high if you have frequent communication interruptions between replica set members (and fix the communication problems!). Set this value lower if communications are reliable (increases availability). |
| catchUpTimeoutMillis | Integer | Represents the time in milliseconds that a newly elected primary is allowed to synchronize its data with the other replica set members. The default is -1 (infinite time). Set a value above 0 for situations where network connections are slow. Warning: the higher the value, the greater the interval during which the new primary is unable to process write requests. |
| catchUpTakeoverDelayMillis | Integer | This value represents the number of milliseconds a secondary must wait before it is allowed to initiate a takeover. A takeover occurs when the secondary detects its data is ahead of the primary, at which point the secondary initiates a new election in an effort to become the new primary. The default value is 30000 (30 seconds). |
| getLastErrorDefaults | Document | A JSON document specifying the write concern for the replica set. This document is in the format { w: <value>, j: <boolean>, wtimeout: <number> }. For more information, see the documentation page for Write Concern Specification. This document is only used when no other write concerns have been expressed for a given request. |
| getLastErrorModes | Document | Allows you to group write concerns by tag. The JSON document takes the form { getLastErrorModes: { <LABEL>: { "<TAG>": <int> } } }, where <int> represents the number of different tag values needed to satisfy the write concern. For more information, see Custom Multi-Datacenter Write Concerns. |
| replicaSetId | ObjectId | Automatically generated, cannot be changed via settings. |
At this point, now that you have an idea of what goes into a replica set configuration document, we will turn our attention to the BigLittle, Ltd. replica set example.
For the purposes of this illustration, we will use an extremely simple example configuration, only specifying the name of the replica set and the host entries for each of the members:
doc = {
_id : "learn_mongodb",
members: [
{ _id: 1, host: "member1.biglittle.local" },
{ _id: 2, host: "member2.biglittle.local" },
{ _id: 3, host: "member3.biglittle.local" },
]
}
rs.initiate(doc);
We then connect to one of the member servers, run the mongo shell, and execute the command, as shown here:

After a brief period of time, database synchronization is complete. As we are operating from an empty database directory, only the default databases appear at first. In a later section, we will restore the sample data and observe the replication process.
Now that the replica set has been initiated, let's have a look at its status.
First of all, you can immediately distinguish the primary from a secondary from the mongo shell prompt. Otherwise, you can use the rs.status() command to determine how well (or badly!) the replica set is performing:

The rs.status() command includes some interesting sub-documents, including details on the election process, as captured here:
"electionCandidateMetrics" : {
"lastElectionReason" : "electionTimeout",
"lastElectionDate" : ISODate("2020-03-25T04:40:47.469Z"),
"electionTerm" : NumberLong(1),
...
"newTermStartDate" : ISODate("2020-03-25T04:40:48.014Z"),
"wMajorityWriteAvailabilityDate" : ISODate("2020-03-25T04:40:48.860Z")
},
You can also see detailed information on the members of the replica set. In this code block, you can see the details for the member1 server, which also happens to be the primary:
"members" : [ {
"_id" : 1,
"name" : "member1.biglittle.local:27017",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 2968,
...
"electionTime" : Timestamp(1585111247, 1),
"electionDate" : ISODate("2020-03-25T04:40:47Z"),
"configVersion" : 1,
"self" : true,
"lastHeartbeatMessage" : ""
}, ...
Next, we will have a look at restoring the sample data and reviewing data replication.
In order to verify the functionality of the replica set, we need to restore the sample data for the biglittle database onto the primary. To restore the sample data, proceed as follows:
docker exec -it learn-mongo-member-1 /bin/bash
mongo /data/common/biglittle_common_insert.js
mongo /data/common/biglittle_users_insert.js
mongo /data/common/biglittle_loans_insert.js

rs.slaveOk();
use biglittle;
show collections;
db.loans.find().count();
You should see something similar to the following screenshot:

In this chapter, you learned about replica sets in general: what they are, how they work, and what their benefits are. You were also given guidelines to establish a solid business rationale pursuant to implement a replica set. After that, you were shown how a DevOp might model a replica set using Docker and Docker Compose. Next, you learned about how to configure and deploy a replica set, with detailed coverage of the various replication configuration settings for each replica set member, as well as the overall settings.
In the next chapter, you will learn how to monitor and manage replica sets, as well as addressing program code that interacts with a replica set.
In this chapter, you'll learn how to monitor and manage a replica set, with a discussion of replica set runtime considerations, including member synchronization, backup, and restore. In addition, this chapter teaches you how to write programming code that addresses a replica set.
In this chapter, we cover the following topics:
Minimum recommended hardware:
Software requirements:
Chapter 2, Setting Up MongoDB 4.x covers required software installation and restoring the code repository for the book.
To run the code examples in this chapter, open a Terminal window (Command Prompt) and enter these commands:
cd /path/to/repo/chapters/14
docker-compose build
docker-compose up -d
This brings up three Docker containers that are used as part of the demonstration replica set configuration. Further instructions on the replica set setup are given in the chapter. When you have finished working with the examples covered in this chapter, return to the Terminal window (Command Prompt) and stop Docker, as follows:
cd /path/to/repo/chapters/14
docker-compose down
The code used in this chapter can be found in the book's GitHub repository at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/chapters/14.
There are several aspects of a running replica set of which MongoDB DevOps need to be aware. Considerations include the oplog size, monitoring, synchronization, backup, and restore. First, we'll have a look at managing the size of the replica set oplog.
As mentioned earlier in this book, synchronization is accomplished using the oplog (operations log). When the primary accepts any request causing the database to be modified, the actual operation performed is recorded in its oplog. The oplog is actually a capped collection named local.oplog.rs. This collection cannot be dropped.
The size of the oplog varies between host operating systems. For macOS systems, the default is set to 192 megabytes (MB). On Linux or Windows servers, conversely, the WiredTiger engine typically uses 5% of its available disk space, with a minimum setting of 990 MB and a maximum of 50 GB.
If using the In-Memory storage engine, on the other hand, the default for Windows and Linux servers is 5% of the available memory, with minimum and maximum boundaries of 50 MB to 50 GB.
The default is adequate for most purposes. There are some types of operations, however, that tend to consume a greater amount of oplog space. These include the following:
The size of the oplog can be adjusted, as follows:
use local
db.oplog.rs.stats().maxSize;
The generic form of this command is shown as follows:
db.adminCommand({
replSetResizeOplog: <int>,
size: <double>,
minRetentionHours: <double>
})
The directives are summarized in this table:
| Directive | Type | Notes |
| replSetResizeOplog | integer | Set this value to 1. |
| size | double | New size of the oplog in MB. Minimum value: 990 MB. Maximum value: 1 petabyte (PB). |
| minRetentionHours | double | The minimum number of hours this log size is retained. If you enter a value of 0, the current size is maintained and any excess is truncated (oldest operations first). Otherwise, decimal values represent fractions of an hour. Example: 0.5 indicates 30 minutes; 1.5 represents one hour and a half, and more. |
use local;
current = db.oplog.rs.stats().maxSize;
db.adminCommand({ replSetResizeOplog:1, size: current*2/1000000 });
Example results are shown in this screenshot:

use local
db.runCommand({ "compact" : "oplog.rs" } )
Next, we'll have a look at the overall replication status.
To monitor the overall replication status, use the rs.status() shell method. This shell method is actually a wrapper for the replSetGetStatus database command. Here is a summary of the information keys to monitor:
| Key | What to look for |
| optimes | There should not be a large difference between the values for lastCommittedOpTime and appliedOpTime. |
| electionCandidateMetrics | Introduced in MongoDB 4.2.1, this metric is only available when connecting to the primary. The difference between lastElectionDate and newTermStartDate should be a few milliseconds. If not, the election process is being drawn out. Consider adding an arbiter to the replica set. |
| members | Information presented under this key is further subdivided by a replica set member. For each given member, things to monitor include the following: state should be 1 (primary), 2 (secondary), or 7 (arbiter). A value of 0 or 5 means the replica set is starting up. If you see a value of 3 (recovering), 6 (unknown), or 8 (down), further investigation on that replica set member is warranted. See the Replica Set Member States link in the next information box for more information. strState gives you state-related messages for unhealthy servers. health should be 1. If not, connect to that replica set member to see what's going on. uptime is in seconds, and the values for all replica set members should be within a few seconds of each other. If you notice one member with a substantially smaller value, that server is a problem. lastHeartbeatMessage, if not empty, gives you information on potential communication problems between replica set members. |
| initialSyncStatus | This information block is only available during replica set initialization. If the replica set is not initializing properly, this information can provide hints as to the nature of the problem. If failedInitialSyncAttempts is high, check for connection problems. Under initialSyncAttempts, look for error messages in the status sub-key. In MongoDB 4.4, a new operationsRetried sub-key was added, which, if high, gives you a clue that there are communication problems with the server, indicated by the syncSource value. |
Here is a partial screenshot, showing the unhealthy state of member1:

Next, we'll have a look at the topic of replication lag.
Replication lag refers to the amount of time a secondary is behind the primary. The greater the lag time, the greater the possibility read operations could return outdated data. An excessive lag time can be the result of several factors, including the following:
To get information on oplog data synchronization for a given server, use the rs.printReplicationInfo() shell method, as seen in the following screenshot:

To get information on data synchronization between the primary and its secondaries, from the primary, use the rs.printSlaveReplicationInfo() shell method. Pay attention to the syncedTo information, which tells you the amount of time a secondary is lagging behind a primary, as seen here:

As a final consideration, MongoDB 4.2 introduced a new flow control mechanism. Enabled by default, as the lag time approaches the value of flowControlTargetLagSeconds, any write requests to the primary must first acquire a ticket before being allowed a lock to write. The flow control mechanism limits the number of tickets granted, thus allowing secondaries a chance to catch up on applying oplog write operations.
Although flow control is enabled by default in MongoDB 4.2 and above, set the values summarized in the following table to ensure flow control implementation:
| Setting | Default | Setting to enable flow control |
| replication.enableMajorityReadConcern | true | true |
| setParameter.enableFlowControl | true | true |
| setParameter.flowControlTargetLagSeconds | 10 | Set to the desired value |
| setFeatureCompatibilityVersion | -- | 4.2 |
To make these settings effective, add the following code to the mongod.conf file, where <int> is the desired maximum target lag value (the default is 10):
replication:
enableMajorityReadConcern: true
setParameter:
enableFlowControl: true
flowControlTargetLagSeconds: <int>
You would also need to set the feature compatibility value by connecting to a server in the replica set and issuing the following command as a user with sufficient rights:
db.adminCommand( { setFeatureCompatibilityVersion: "4.2" } )
Let's now have a look at backup and restore.
Backing up from and restoring to a replica set is not as simple as the basic backup procedure described in Chapter 3, Essential MongoDB Administration Techniques. One problem when using mongodump for backup is that all data needs to pass through the server memory, severely impacting performance.
Another consideration is that the very second the backup has completed, it's already out of date! To avoid overwriting newer data with stale data restored from a backup, you need to carefully consider your backup strategy. The choices for backing up a replica set are as follows:
The tools needed to create a filesystem snapshot depend on the operating system. As such, we do not cover that aspect in this book. If you wish to pursue this approach, a good starting point is the MongoDB documentation, Back Up and Restore Using Filesystem Snapshots.
It is also important to discuss the temptation to not do backups at all and simply rely upon the fact that the replica set itself serves as a live and dynamic backup. Although there is a certain amount of merit to this philosophy, please do your best to avoid falling into this trap. The proper approach to safeguarding your data would be to preferably use external operating system tools to capture regular filesystem snapshots. Barring that, use mongodump on a regular basis to back up your data. Remember: if the servers hosting the replica set are in the same building, what happens if the building is flooded and all servers are lost? Or if there is an earthquake? Or a wildfire? In the face of natural or man-made disasters, even the best replica set design can fail. Unless you've made regular backups, and—very importantly—store the backups offsite, you stand to lose all your data.
Next, we'll have a look at using mongodump to back up a replica set.
When using mongodump to back up a replica set, you need to use either the --uri or the --host option to specify a list of hosts in the replica set. Using our Docker replica set model as an example, the string would appear as follows:
mongodump --uri="mongodb://member1.biglittle.local:27017,\
member2.biglittle.local:27017,member1.biglittle.local:27017/? \
replicaSet=learn_mongodb"
Here is an alternate syntax that does the same thing:
mongodump --host="learn_mongodb/member1.biglittle.local:27017,\
member2.biglittle.local:27017,member1.biglittle.local:27017"
Data is normally read from the primary. If you wish to allow the backup to be read from a secondary, add the --readPreference option. If all you want to do is to indicate that the backup can occur from a secondary, add the following option to either of the preceding examples:
mongodump <...> --readPreference=secondary
A final consideration when using mongodump to back up a replica set is to include the --oplog option. This allows MongoDB to establish the backup within a specific point in time, which is highly advantageous when performing a restore.
There are several caveats when using this option, summarized here:
Here is an example using our replica set model:

The backup should have a directory tree resembling this:
/data/common/backup/
|-- admin
| |-- system.version.bson
| `-- system.version.metadata.json
|-- biglittle
| |-- common.bson
| |-- common.metadata.json
| |-- loans.bson
| |-- loans.metadata.json
| |-- users.bson
| `-- users.metadata.json
`-- oplog.bson
You can see that there is no backup of the local database. Also, you might note that the inclusion of the oplog is critical when restoring to a replica set. Next, we'll have a look at restoring data to a replica set.
Before we get into the details on how to restore a replica set, it must be made clear that if any members of a replica set survive some future disaster, you need to perform synchronization instead of restoration. The reason for this is that the data found on a functioning member of the replica set should be more up to date than the data in a backup. The next subsection deals with resynchronization.
Assuming that no member of the replica set still remains functional, we'll now examine what is involved in restoring the replica set. Let's have a look at the process step by step, as follows:
mongod --dbpath /path/to/backup
> use local;
> db.dropDatabase();
> use admin;
> db.shutdownServer();
doc = {
_id : "learn_mongodb",
members: [
{ _id: 1, host: "member1.biglittle.local" }
]
}
rs.initiate(doc);
The following screenshot shows the result of running rs.status().members on the restore target:

Once satisfied the restore target server MongoDB installation is functioning properly, having confirmed data was restored successfully, you can now start restoring the remaining servers back into the replica set. To do this, on each remaining unrestored former member of the replica set, proceed as follows:
rs.add(host, arbiterOnly);
host is a JavaScript Object Notation (JSON) document that contains exactly the same directives as described in the Initiating the replica set: Replica set members configuration directives sub section from the previous chapter, Chapter 13, Deploying a Replica Set.
arbiterOnly is a Boolean value; set it to true if you don't want this server to have voting rights, but do want it to synchronize data.
member = { _id: 2,host: "member2.biglittle.local" }
rs.add(member);
This screenshot shows the results:

Now, we'll turn our attention to resynchronization.
At some point, be it through network disruptions, natural disasters, or any number of other factors outside your control, it's possible for the data on a replica set member to become stale. What this means is that the data is so radically out of date that it is impossible for it to catch up through the normal synchronization process. If you have determined that network communications are not at fault, it is time to consider performing a manual resynchronization (https://docs.mongodb.com/manual/tutorial/resync-replica-set-member/).
There are two primary techniques used to perform resynchronization. These techniques are similar to the techniques discussed earlier in the chapter, in the Restoring a replica set sub section, and are detailed as follows:
Let's look at the initial sync approach first.
This technique requires a minimal number of manual steps as it relies upon the normal replica set synchronization process. The main disadvantage is that when you have a large amount of data, it inevitably generates a large amount of network traffic. This technique also impacts the primary by generating a larger-than-ordinary number of requests to the primary. Further, this technique can only work if other replica set members are up to date.
To resynchronize by forcing an initial sync, proceed as follows:
Once the server is back up and running, an initial sync request is issued, and data starts flowing to the restarted server. You can monitor the progress using the commands described earlier in this chapter. Now, let's look at resynchronization by manually copying files.
In this approach, the files from the data directory of an existing replica set member are directly copied to the replica set member in need of resynchronization. This approach is best in situations where the amount of data is sizeable, and where network communications would be adversely affected following the initial sync approach described in the previous sub section.
Unfortunately, trying to obtain a direct file copy while a mongod instance is running might prove difficult as database files are frequently locked, held open, and contents are changing. Accordingly, you either need to shut down the mongod instance in order to obtain a copy or use external operating system tools to obtain a filesystem snapshot.
Unlike the process of restoring a replica set described earlier in this chapter, you must include the local database in the copy.
In this procedure, we refer to the replica set member with up-to-date data as the source server. We refer to the server needed to be resynchronized as the target server. To resynchronize using the file copy approach, proceed as follows:
Once the server is back up and running, you might notice a small amount of synchronization traffic. This is normal, as it's possible changes occurred in the midst of the copy process. You can monitor the replica set status using the commands described earlier in this chapter.
Now that you understand how to create and manage a replica set, we'll turn our attention to potential program code modifications.
In practice, the application program code written for MongoDB does not need to change when performing reads and writes to a replica set. If your application code works well for a single mongod instance, it works well when accessing a replica set. In other words, just because you move your working application code over to a replica set does not mean it suddenly stops working!
There are some considerations, however, that might improve application performance by taking advantage of potential performance enhancements made possible by the replica set. There is also the question of which server to connect: should your application connect to the primary, or is it OK to connect to a secondary? Let's start with a basic replica set connection.
Let's say that you've developed a fantastic application. The original code, however, was designed to work with a single server running MongoDB. Now, the company has implemented a replica set. The first question on your mind might be: Will my existing code work on a replica set? Let's have a look.
Here is a simple test program that connects to a single server and returns the result from a simple query:
import pprint
from pymongo import MongoClient
hostName = 'member1.biglittle.local';
client = MongoClient(hostName);
result = client.biglittle.users.find_one({}, {"businessName":1,"address":1})
pprint.pprint(result)
The source code can be found at /path/to/repo/chapters/14/test_replica_set_connect_using_one_server.py.
From this screenshot, you can see that even though the member1 mongod instance is part of a replica set, the return results are the same, just as if member1 had been a standalone mongod instance:

However, since the server is a member of a replica set, a slight modification causes your code to take advantage of high availability. All you need to do is to change the hostname to a list, and add a **kwarg parameter with the name of the replica set, as shown here:
import pprint
from pymongo import MongoClient
hosts = ['member1.biglittle.local','member2.biglittle.local',\
'member3.biglittle.local']
replName = 'learn_mongodb'
### Note the use of the "replicaSet" **kwarg: ###
client = MongoClient(hosts, replicaSet=replName);
result = client.biglittle.users.find_one({}, \
{"businessName":1,"address":1})
pprint.pprint(result)
As you can see from the screenshot shown here, the results are exactly the same:

The difference, however, is that now, your application code has a list of fallback servers, any of which can be contacted, should the first on the list fail to deliver results. If you need to be more selective about which server delivers results, we'll next look at setting read preferences.
By default, all read operations (for example, database queries) are directed to the primary. On a busy website, however, this might not yield optimal performance, especially where the replica set is geographically dispersed. Accordingly, it's possible to specify a read preference when initiating a query.
In the case of a pymongo.mongo_client.MongoClient instance, simple read preferences are expressed using the following values in conjunction with the readPreference **kwarg parameter:
| Preference | Notes |
| primary | All read requests are directed to the primary. This is the default. |
| primaryPreferred | If this preference is set, requests are directed to the primary. If the primary is unavailable, however, the request falls back to one of the secondaries. |
| secondary | This preference causes requests to be directed to any secondary replica set member, but not to the primary. |
| secondaryPreferred | This read preference causes requests to be directed to any secondary replica set member, but if no secondaries are available, the request goes to the primary. |
| nearest | This preference causes MongoDB to ignore the primary or secondary state. Instead, the requests are directed to the replica set member with the least network latency. |
These class constants can be used when establishing the database connection using the readPreference **kwarg parameter. Here is a slightly modified version of the example test_replica_set_connect_using_one_server.py shown earlier in this chapter, specifying a preference for the nearest server (/path/to/repo/chapters/14/test_replica_set_connect_secondary_read_pref.py):
import pprint
from pymongo import MongoClient
hosts = ['member1.biglittle.local','member2.biglittle.local',\
'member3.biglittle.local']
replName = 'learn_mongodb'
readPref = 'nearest'
client = MongoClient(hosts, replicaSet=replName, readPreference=readPref);
result = client.biglittle.users.find_one({}, \
{"businessName":1,"address":1})
You can further influence operations where the read preference is anything other than primary, by adding the maxStalenessSeconds **kwarg parameter. The default value is -1, indicating there is no maximum. The implication of the default is that any data is acceptable, even if extremely stale.
Here is an example of setting the **kwargs parameter, maxStalenessSeconds. In this example, pay attention to the line where a MongoClient instance is created. After the hosts parameter, the remaining parameters in the constructor are **kwargs parameters:
import pprint
from pymongo import MongoClient
hosts = ['member1.biglittle.local','member2.biglittle.local',\
'member3.biglittle.local']
replName = 'learn_mongodb'
readPref = 'nearest'
maxStale = 90
client = MongoClient(hosts, replicaSet=replName, \
readPreference=readPref, maxStalenessSeconds=maxStale);
result = client.biglittle.users.find_one({}, \
{"businessName":1,"address":1})
This example can be found at /path/to/repo/chapters/14/ test_replica_set_connect_read_pref_max_staleness.py.
Now, let's have a look at manipulating write operations.
The nature of write concern management is quite different from that of establishing read preferences. The difference is that all replica set members are ultimately involved in a write operation. When the database is modified, the changes need to be synchronized by the entire replica set. More importantly, what is at stake in a write operation is how many (if any) acknowledgments must be received before the write operation is considered a success (https://docs.mongodb.com/manual/reference/write-concern/#acknowledgment-behavior).
As with read preferences, write concerns are expressed in the form of keyword arguments when defining a pymongo.mongo_client.MongoClient instance. The following list summarizes the applicable **kwargs parameters:
Alternatively, you can set the value to be majority, in which case the write is considered a success if the majority of replica set members acknowledge the write operation. Thus, for example, if you have five members in the replica set, a w=3 value would be the same as a value of w='majority'. Note that the timing behind the majority setting is controlled by the writeConcernMajorityJournalDefault setting when establishing the replica set. Also, note that starting in MongoDB 4.2, the current value used to determine the majority is revealed by rs.status().writeMajorityCount.
In the following example, a connection is made to the learn_mongodb replica set, consisting of a list of three members. The write preference is set to majority, fsync is True, and the maximum allowed acknowledgment time is 100 milliseconds:
import pprint
from pymongo import MongoClient
hosts = ['member1.biglittle.local','member2.biglittle.local',\
'member3.biglittle.local']
replName = 'learn_mongodb'
writePref = 'majority'
writeMs = 100
client = MongoClient(hosts, replicaSet=replName, w=writePref, \
wTimeoutMs=writeMs,fsync=True)
Here is a list of sample data documents set to be inserted:
insertData = [
{ "fname":"Fred","lname":"Flintstone"},
{ "fname":"Wilma","lname":"Flintstone"},
{ "fname":"Barney","lname":"Rubble"},
{ "fname":"Betty","lname":"Rubble"} ]
We then drop the test collection, insert the sample data, and use find() to retrieve results, as illustrated in the following code snippet:
client.biglittle.test.drop()
client.biglittle.test.insert_many(insertData)
result = client.biglittle.test.find()
for doc in result :
pprint.pprint(doc)
The full example is available here: /path/to/repo/chapters/14/ test_replica_set_connect_with_write_concerns.py
In this chapter, you were shown how to monitor replica set status and were given tips on factors to watch that might indicate a potential performance problem. Aspects covered in this section included managing the size of the oplog and viewing the overall replica set status.
You were also shown how to back up and restore a replica set, as well as how to resynchronize a replica set member.
You then learned that the application program code does not need to change when addressing a replica set. You were shown how to enhance your applications to take advantage of a replica set in terms of failover, directing preferences for read operations, and ensuring data integrity by manipulating write concerns.
In the next chapter, we will show you how to scale your database horizontally by implementing a sharded cluster.
At some point, the amount of data in a given collection might become so large that the company notices significant performance issues. In this chapter, you'll learn how to deploy a sharded cluster. The first section in this chapter focuses on the big picture: what a sharded cluster is, how it works, and what the business rationale might be for its deployment. The other two sections of this chapter show you how to configure and deploy a sharded cluster.
In this chapter, we'll cover the following topics:
Let's get started!
The following is the minimum recommended hardware for this chapter:
The following are the software requirements for this chapter:
The installation of the required software and how to restore the code repository for this book was explained in Chapter 2, Setting Up MongoDB 4.x. To run the code examples in this chapter, open a Terminal window (Command Prompt) and enter these commands:
cd /path/to/repo/chapters/14
docker-compose build
docker-compose up -d
This brings up three Docker containers that are used as part of the demonstration replica set configuration. Further instructions on the replica set's setup are provided in this chapter. When you have finished working with the examples covered in this chapter, return to the Terminal window (Command Prompt) and stop Docker, as follows:
cd /path/to/repo/chapters/14
docker-compose down
The code used in this chapter can be found in this book's GitHub repository at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/chapters/15.
MongoDB has a horizontal scaling capability known as sharding. In this section, we'll introduce you to the key terms, how sharding works, why you might want to deploy it, as well as how to establish a business rationale for deployment. Let's start with the basics: learning what a sharded cluster is.
In order to properly understand a MongoDB sharded cluster, you need to know something about its key components: shards, mongos, and the config server. Here is a brief summary of the key components:
Here is a diagram to give you a better idea of how a sharded cluster might appear conceptually. The yellow circles in the diagram shown here represent replica sets:

The client application connects to a mongos instance. The mongos instance conveys read and write requests to the sharded cluster. The config servers (deployed as a replica set) convey metadata information to the mongos instances and also manage information on the state of the sharded cluster. Now, let's have a look at why you might want to deploy a sharded cluster.
In order to understand why you might want to deploy a sharded cluster, we need to further expand upon two concepts you might have heard of before in other contexts: vertical scaling and horizontal scaling.
Vertical scaling is when you increase the power and capacity of a single server. Upgrades could include a more powerful CPU, adding memory, and increasing storage capacity. For example, you might have a server using a second-generation Intel Xeon Scalable processor. Vertical scaling could involve updating the CPU from 16 cores and 32 threads to 24 cores and 48 threads, as an example. Vertical scaling could also involve more obvious upgrades such as increasing the amount of server RAM, adding more and larger hard drives or SSDs to a RAID array, and so on.
Vertical scaling has an advantage in that you only need to worry about a single server: no additional network cards, IP addressing or routing problems, no further strain on server room power requirements, and no additional worries about heat buildup. Also, in some cases, costs might actually be lower for vertical scaling as you are not adding entire additional computers to your network, just internal components.
Vertical scaling has a serious disadvantage, however, in that there is a limit as to how far you can expand. Even the computational power and tremendous storage capacity of today's computers might still not be enough to handle massive amounts of streaming data. As just one example of massive data, imagine you are working with health officials to design an application that accepts mobile phone tracking information in order to help prevent the spread of a pandemic virus. If the health officials planned to implement this in a country as populous as India, how long would it be before a single server, even one that has been vertically scaled, became overwhelmed?
The answer to this would be to implement horizontal scaling, which we'll discuss next.
Horizontal scaling involves setting up additional servers that are collectively referred to as a cluster. The cluster is then managed by a control mechanism that is responsible for assigning tasks to cluster member servers. One example of horizontal scaling that many of you will be familiar with is the concept of a web server farm: where you have a group of web servers that service requests on the backend but appear to the internet as a single server. The control mechanism would be a scheduling daemon that determines which server in the web server farm has the most available bandwidth, and directs the next request to that server.
In the context of MongoDB, horizontal scaling is achieved by splitting data in a large collection into fragments referred to as shards. All the shards together are referred to as a sharded cluster.
The advantage of horizontal scaling is that you can simply continue to add servers as needed, giving you virtually unlimited storage. Another benefit is that read and write operations can be performed in parallel, producing a faster response time. Even if a server or replica set hosting a shard becomes unavailable, the remaining shards can still respond to read and write requests.
In many cases, the cost of adding additional off-the-shelf computers to create a sharded cluster is less expensive than vertically scaling an existing computer. Horizontal scaling can also lead to potentially better performance in that a certain amount of parallel processing is possible. However, please bear in mind that breaking up a collection into shards is risky if the server hosting one of the shards goes down. This is why MongoDB recommends that you implement each shard as a replica set. Accordingly, even though the cost of a single server might be less than gaining the equivalent power through vertical scaling efforts, in reality, you probably need to add at least three servers per shard, which might negate the cost differential.
With these considerations in mind, let's turn our attention to developing a business rationale for deploying a sharded cluster.
As we mentioned earlier in this chapter, the main driving factor is the amount of data your website needs to handle. Any time you need to handle large amounts of streaming data where the data needs to be stored for later review or analysis, the size of the database could quickly increase as the number of users increases. A decision point arrives when one of two things occurs:
Once either or both of these obstacles are encountered, you need to decide between vertical and horizontal scaling solutions. If you strongly feel that the amount of data does not increase exponentially, or if you feel that, by adding a bit more RAM, the current server could handle the load, a vertical solution might be in order. If, on the other hand, there is no end in sight for data expansion, and the server is already at its maximum RAM, a horizontal scaling solution is called for.
As we mentioned earlier, building a sharded cluster should be implemented in the form of one replica set per shard. Accordingly, right away, you need to calculate the cost of three additional servers (or Docker containers in a cloud environment), as well as the additional time needed for normal server maintenance. Each server has an operating system that needs to be updated periodically. Each server has to be added to the overall network infrastructure, configured for routing, their log files monitored, and so forth.
Next, you need to calculate the costs associated with the following:
Using the second example mentioned here, let's say that your local government depends on virus information that is updated hourly. If the last hour of data is not available, the local government would be caught unaware if a new outbreak developed, costing dozens of lives!
However, you may be receiving advertising revenue by attracting people to the site, in which case fewer people visiting means loss of advertising revenue.
The process of establishing a business rationale for deploying a sharded cluster thus becomes a matter of balancing the costs associated with adding three new servers against the potential costs associated with the considerations listed here.
Now, let's have a look at sharded cluster configuration.
As with establishing replica sets, setting up a sharded cluster involves making modifications to the mongod.conf files of the servers going to be involved in the deployment. Before we get into the configuration details, however, it's extremely important that you understand the importance of the shard key. The choice of shard key can make or break the success of a sharded cluster implementation.
The shard key is a field (or fields) in a collection document. At this point, you're probably wondering: okay... so, what's the big deal? To answer this rhetorical question: the shard key is used by the sharded cluster balancer (https://docs.mongodb.com/manual/core/sharding-balancer-administration/#sharded-cluster-balancer) to determine how documents are distributed into chunks, and how chunks are distributed between shards. This can be quite a weighty decision as a poor choice of shard key could result in an uneven distribution of documents, leading to poor performance and an overwhelmed server. What's even more important is that the choice of shard key cannot be changed once the sharded cluster is initiated. Accordingly, you need to put careful thought into your choice for a shard key!
In order to discuss shard keys, let's have a look at document distribution within shards.
For greater efficiency, the MongoDB sharded cluster balancer (the sharding mechanism) doesn't operate at the document level. Rather, documents are grouped into chunks. Which documents are placed into a given chunk is determined by the shard key range. Mongo shell helper methods exist that let you view the size of individual shards and manually move one or more chunks from one shard to another. This process is known as chunk migration (covered in the next chapter, Chapter 16, Sharded Cluster Management and Development).
Here is a diagram that illustrates the shard key's distributional role:

The overall diagram represents a sharded cluster. In this diagram, we're assuming the shard key is a field named X. Within each shard is a distribution of two chunks. Within each chunk is a group of documents. The key space for X ranges from its smallest value, labeled X:minKey,to its largest, labelled X:maxKey. The range of documents placed into chunk 1 are those where the value of the document field, X, is less than 1,000. In chunk 2, the documents have a value for X greater than or equal to 1,000, but less than 2,000, and so forth.
Next, we'll have a look at the guidelines for choosing a shard key.
As mentioned earlier, the value of the field or fields chosen as the shard key influences how the balancer decides to distribute documents into chunks. In order to choose a shard key that produces the evenest distribution of documents within chunks, careful consideration needs to be made of the following: cardinality, frequency, and change. Let's have a look at cardinality first.
According to the Merriam-Webster Dictionary, cardinality is defined as follows:
So, for example, if you have a Python list of x = [1,2,3], its cardinality would be three. The cardinality of the set represented by the shard key represents the maximum number of shards possible. As an example, if your shard key is a country field containing ISO 3 country codes, the cardinality of this set would be the number of ISO 3 country codes (https://unstats.un.org/unsd/tradekb/Knowledgebase/Country-Code). This number at the time of writing, and thus the maximum number of chunks that could be created, is 246.
If, on the other hand, the shard key chosen is the seasons field, the cardinality would be 4 as there are four seasons. This means the maximum number of chunks MongoDB could create would be limited to 4. If you find that the shard key you are considering has low cardinality, you might consider making it a compound key to increase the potential number of chunks created.
Now, let's have a look at frequency.
Shard key frequency refers to how many documents have the same shard key value. As an example, if you choose the ISO 3 country code as a shard key, let's say that 67% of your customers are in India and the other 33% are evenly spread across the collection. When the sharding process completes, you might end up with a skewed distribution that, visually, might appear like this:

In this example, the value of minKey would be ABW (Aruba), while the value of maxKey would be ZWE (Zimbabwe). To avoid the effect of uneven frequency, combining the ISO 3 country code field with another field such as telephone area dialing code might produce a more even frequency. Finally, we'll examine how the rate of change affects the sharding process.
If the field chosen as the shard key is incremented or decremented, or changes monotonically in some form (for example AA, AB, AC, AD ... AZ, BA, BB, and so on) as new documents are added to the collection, you are at risk of having all the new documents being inserted into the last chunk if the shard key value increases, or the first chunk where the value of the shard key decreases. The reason this happens is that when the collection is initially sharded, shard key ranges are established and documents are inserted into chunks. As new data is added to the collection, the values of minKey and maxKey are continually upgraded, but the chunk shard key ranges remain the same.
To illustrate this point, in the following diagram, the initial sharding process produces shard key boundaries that range from 101 to 130. As the client application continues to write data, the value of X increments, so the value of maxKey is continuously updated. However, as the boundaries have already been set, additional documents all end up in chunk 6:

Now that you have an idea of how to choose a shard key, let's have a look at shard key strategies.
Before you start configuring your database for sharding, another choice needs to be made: hashed sharding or ranged sharding. In either case, you still need to wisely choose a shard key. However, the role of the key differs, depending on the shard key strategy chosen. First, let's have a look at hashed sharding.
Hashed sharding causes MongoDB to create a hashed index on the value of the shard key, rather than using the key as is. Hashed sharding tends to produce a more even distribution of data within the sharded cluster, and is an ideal solution if the shard key has high cardinality or is a field that automatically increments. Because the hashed value is used to perform distribution rather than the actual value of the shard key field itself, a shard key that automatically increments does not cause MongoDB to eventually dump all data into the last shard (as discussed earlier).
The performance we get when using hashed sharding is very slightly lower. When a new document is added to the sharded collection, its shard key value needs to be hashed before it's added to the hashed index. However, the overall gain in performance resulting in even distribution could potentially compensate for this loss.
The main disadvantage of using hashed sharding is a decreased number of targeted operations and an increased number of broadcast operations. A targeted operation is where the mongos instance has already identified the exact shard where the requested data resides. This operation is immediately available if the shard key is not hashed as mongos is able to determine the exact shard due to its knowledge of shard key ranges assigned to shards. Because of the anonymous nature of a hashed value, however, mongos is forced to perform a broadcast operation instead, sending a request to all the shards first in order to locate the requested data.
Now, let's have a quick look at range-based sharding.
In range-based sharding (https://docs.mongodb.com/manual/core/ranged-sharding/#ranged-sharding), MongoDB simply uses the direct value of the chosen shard key to determine the shard boundaries. This has a distinct advantage in that application program code can perform read and write operations directly specifying the value of the shard key. This, in turn, allows mongos to perform a targeted operation, resulting in a performance boost. The disadvantage of this strategy is that the onus is on the MongoDB DevOp to wisely choose a shard key, paying close attention to cardinality, frequency, and the rate of change. A bad choice of shard key quickly results in poor performance and the uneven distribution of data among the shards.
In a range-based sharding implementation, because the shard key is known, the application code can include the shard key in its queries. If the shard key is compound (for example, ISO 3 country code plus telephone area dialing code), the query could alternatively include just the prefix (for example, just the ISO 3 country code). This information is enough to allow the mongos instance to conduct a targeted operation. If the shard key information is unknown, missing, or hashed, the mongos instance is forced to perform a broadcast in order to locate the appropriate shard to handle the request.
Now, we'll turn our attention to configuration.
The sharding options in the mongod.conf file include three sharding.* directives, as well as two related directives (replication.replSetname and net.bindIp), as summarized here:
sharding:
configDB: <REPLICA_SET>/server1.example.com:27019,\
server2.example.com:27019
Now that the mongod.conf file has been updated with the appropriate configuration settings, it's time to have a look at sharded cluster deployment.
The following is a general overview of the steps needed to deploy a sharded cluster. This overview is loosely based on the MongoDB documentation article Deploy a Sharded Cluster. The steps are summarized here:
Before we start down this list, however, let's take a quick look at how to use Docker to model a sharded cluster for testing and development purposes.
As we covered in Chapter 13, Deploying a Replica Set, Docker technology can be used to model a sharded cluster as well. In this case, however, in order to minimize the impact it has on a single development computer, all replica sets are modeled as single-node replica sets. This greatly reduces system requirements and allows DevOps to model the replica set on computers that are potentially less powerful than the ones used in actual production.
In this sub-section, we'll examine the Docker Compose configuration, individual Dockerfiles, a common hosts file, and the mongod.conf files used to create the sharded cluster model. Let's start with the docker-compose.yml file.
As in Chapter 13, Deploying a Replica Set, we start by defining a docker-compose.yml file that defines the five containers we use in this illustration to model a sharded cluster: one container to host the config server, one to host the mongos instance, and three to represent shards. At the top of the file, we place the version directive.
This informs Docker Compose that this file follows directives compatible with version 3:
version: "3"
We then define services, starting with the first member of the sharded cluster. In this definition, you can see that we assign a name of learn-mongo-shard-1 to the Docker container and a hostname of shard1. The image directive defines a name for the image (ultimately derived from the official MongoDB Docker image) created by Docker Compose, as shown here:
services:
learn-mongodb-shard-1:
container_name: learn-mongo-shard-1
hostname: shard1
image: learn-mongodb/shard-1
Under volumes, we define the location of the database, a common volume, and mappings to the hosts and mongod.conf files:
volumes:
- db_data_shard_1:/data/db
- ./common_data:/data/common
- ./common_data/hosts:/etc/hosts
- ./common_data/mongod_shard_1.conf:/etc/mongod.conf
Additional directives define the location of the Dockerfile, how to restart it, and what options to provide to the startup entry point shell script. Finally, you'll see a networks directive that defines the IP address of the learn-mongodb-shard-1 container:
ports:
- 27111:27018
build: ./shard_1
restart: always
command: -f /etc/mongod.conf
networks:
app_net:
ipv4_address: 172.16.1.11
The definitions for the next two containers (not shown) are identical, except the identifying numbers are 2 and 3 instead of 1.
Also, the outside port mapping needs to be different, as well as the IP address. You can view the entire file at /path/to/repo/chapters/14/docker-compose.yml.
Next, still under the services key, we have the config server definition. Note that it's almost identical to the definition for a shard, except that the port maps to 27019. Also, of course, the hostname, volume name, and IP address values are different:
learn-mongodb-config-svr:
container_name: learn-mongo-config
hostname: config1
image: learn-mongodb/config-1
volumes:
- db_data_config:/data/db
- ./common_data:/data/common
- ./common_data/hosts:/etc/hosts
- ./common_data/mongod_config.conf:/etc/mongod.conf
ports:
- 27444:27019
build: ./config_svr
restart: always
command: -f /etc/mongod.conf
networks:
app_net:
ipv4_address: 172.16.1.44
The last entry under services defines the mongos instance. Although this daemon could be run just about anywhere, including the application server, for the purposes of this chapter, we'll define it in its own container. Aside from the unique hostname, port, and IP address, this definition lacks a /data/db volume mapping. That is because a mongos instance does not handle MongoDB data: rather, it serves as a router between the application's MongoDB driver and the sharded cluster. It also communicates with the config server to gather sharded cluster metadata:
learn-mongodb-mongos:
container_name: learn-mongo-mongos
hostname: mongos1
image: learn-mongodb/mongos-1
volumes:
- ./common_data:/data/common
- ./common_data/hosts:/etc/hosts
- ./common_data/mongos.conf:/etc/mongos.conf
ports:
- 27555:27020
build: ./mongos
restart: always
command: mongos -f /etc/mongos.conf
networks:
app_net:
ipv4_address: 172.16.1.55
The last two sections in the docker-compose.yml file define the overall virtual network over which the containers communicate, as well as external volumes mapped to the /data/db folders:
networks:
app_net:
ipam:
driver: default
config:
- subnet: "172.16.1.0/24"
volumes:
db_data_shard_1: {}
db_data_shard_2: {}
db_data_shard_3: {}
db_data_config: {}
Next, we'll look at the additional configuration files needed to complete the model.
Each container needs to have a Dockerfile in order for docker-compose to work. Under /path/to/repo/chapters/14, you will see the following directory structure, where a directory exists for each type of container to be built:
├── docker-compose.yml
├── config_svr
│ └── Dockerfile
├── mongos
│ └── Dockerfile
├── shard_1
│ └── Dockerfile
├── shard_2
│ └── Dockerfile
└── shard_3
└── Dockerfile
Each Dockerfile is identical and contains instructions for the source Docker image, as well as information regarding what additional packages to install.
An example of this is as follows:
FROM mongo
RUN \
apt-get update && \
apt-get -y upgrade && \
apt-get -y install inetutils-ping && \
apt-get -y install net-tools
In the /path/to/repo/chapters/14/common_data directory, there are JavaScript files used to restore the BigLittle, Ltd. sample data. In addition, there are sample JavaScript commands to initialize the shards and replica sets. In this directory, you will also find a common /etc/hosts file and mongod.conf files for each respective container. This system of five containers (three shards, one config server, and one mongos instance) can be brought online using the docker-compose up command, as shown in the following screenshot:

A warning is displayed because running a config server with less than three members in its replica set is not considered a best practice, and should not be used for production. Also, bear in mind that none of the single-node replica sets have been initialized yet.
Now, let's take a look at the list of steps mentioned in the overview for this section, starting with confirming network communications.
As you might have noticed, we installed the inetutils-ping Linux package, which allows us to run the ping command. For that purpose, you'll find a script called /path/to/repo/chapters/14/verify_connections.sh that tests network communications between servers, as shown here:
#!/bin/bash
docker exec learn-mongo-shard-1 /bin/bash -c \
"ping -c1 shard2.biglittle.local"
docker exec learn-mongo-shard-2 /bin/bash -c \
"ping -c1 shard3.biglittle.local"
docker exec learn-mongo-shard-3 /bin/bash -c \
"ping -c1 config1.biglittle.local"
docker exec learn-mongo-config /bin/bash -c \
"ping -c1 mongos1.biglittle.local"
docker exec learn-mongo-mongos /bin/bash -c \
"ping -c1 shard1.biglittle.local"
After running docker-compose up, from another Terminal window, here is the output from the verification script:

Now, we'll have a look at deploying the replica set to represent the config server.
The next step is to deploy the config server. As mentioned earlier in this chapter, the config server stores sharding metadata and must be deployed as a replica set.
Accordingly, the mongod.conf file appears as follows:
systemLog:
destination: file
path: "/var/log/mongodb/mongodb.log"
logAppend: true
storage:
dbPath: "/data/db"
journal:
enabled: true
net:
bindIp: 0.0.0.0
port: 27019
replication:
replSetName: repl_config
sharding:
clusterRole: configsvr
To initialize the replica set, we use the rs.initiate() shell method. The following syntax is sufficient for the purposes of this illustration:
doc = {
_id : "repl_config",
members: [
{ _id: 1, host: "config1.biglittle.local:27019" }
]
}
rs.initiate(doc);
We open a mongo shell on the config server and issue the command shown in the preceding code block. As discussed earlier in this section, we initiate a single-node replica set for testing and development purposes only.
Initially, the single node runs an election and votes itself as the primary.
The result is shown in the following screenshot:

Next, we'll examine the process of deploying shard replica sets.
Now, we'll repeat a very similar process for each member of the sharded cluster. In our illustration, we'll initiate three single-node replica sets, each representing a shard. The mongod.conf file for each shard is similar to the following:
systemLog:
destination: file
path: "/var/log/mongodb/mongodb.log"
logAppend: true
storage:
dbPath: "/data/db"
journal:
enabled: true
net:
bindIp: 0.0.0.0
port: 27018
replication:
replSetName: repl_shard_1
sharding:
clusterRole: shardsvr
To initialize the shard replica set, as described in the previous sub-section, we open a mongo shell to each of the member servers in turn. It's important to add the --port 27018 option when opening the shell. We can then initialize the replica set using the following shell commands:
doc = {
_id : "repl_shard_1",
members: [
{ _id: 1, host: "shard1.biglittle.local:27018" }
]
}
rs.initiate(doc);
The resulting output is similar to the replica set initialization shown for the config server. Once each of the shard replica sets has been initialized, we turn our attention to starting a mongos instance to represent the sharded cluster.
The mongod.conf file for a mongos instance has one major difference from the shard configuration files: there is no storage option. The purpose of the mongos instance is not to handle data, but rather to serve as an intermediary between a MongoDB application programming language driver, the config server, and the sharded cluster. The following is the mongos.conf file that's used to start the mongos instance. It gives us access to our model's sharded cluster:
systemLog:
destination: file
path: "/var/log/mongodb/mongodb.log"
logAppend: true
net:
bindIp: 0.0.0.0
port: 27020
sharding:
configDB: repl_config/config1.biglittle.local:27019
To start the mongos instance, we use the following command:
mongos -f /etc/mongos.conf
The following screenshot shows the results:

When opening a shell, we can simply indicate the port that the mongos instance is listening on. Here is the result:

Now, we can restore the sample data before proceeding.
For the purposes of this example, we'll restore four collections of the biglittle database. The data is restored on the shard1 server only. Later, we'll shard the last collection, world_cities (covered in later subsections).
For the purposes of this illustration, the sample data is mapped to the /data/common directory in the Docker container representing shard1. You can perform the restore by running the shell scripts from a Terminal window inside the container, as shown here:
docker exec -it learn-mongo-shard-1 /bin/bash
mongo --port 27018 /data/common/biglittle_common_insert.js
mongo --port 27018 /data/common/biglittle_loans_insert.js
mongo --port 27018 /data/common/biglittle_users_insert.js
mongo --port 27018 /data/common/biglittle_world_cities_insert.js
Alternatively, from outside the container, use the /path/to/repo/chapters/14/restore_sample_data.sh script, as shown here:
#!/bin/bash
docker exec learn-mongo-shard-1 /bin/bash -c \
"mongo --port 27018 /data/common/biglittle_common_insert.js"
docker exec learn-mongo-shard-1 /bin/bash -c \
"mongo --port 27018 /data/common/biglittle_loans_insert.js"
docker exec learn-mongo-shard-1 /bin/bash -c \
"mongo --port 27018 /data/common/biglittle_users_insert.js"
docker exec learn-mongo-shard-1 /bin/bash -c \
"mongo --port 27018 /data/common/biglittle_world_cities_insert.js"
To confirm the restore, open a mongo shell on the shard1 server and confirm that the collections exist. At this time, the data only resides on the shard1 server. If you open a shell on either shard2 or shard3, you will see that there is no biglittle database. Now, it's time to add shards to the cluster.
To create a sharded cluster, you need to add servers configured as shard servers under the following conditions:
The shell command to add a shard is sh.addShard(). Here is the generic syntax:
sh.addShard("REPLICA_SET/MEMBER:PORT[,MEMBER:PORT]");
REPLICA_SET represents the name of the replica set hosting the shard. MEMBER is the DNS or resolvable hostname of the replica set member. PORT defaults to 27018 (not the same as a standalone mongod instance)!
In our model sharded cluster, first, we open a shell on the mongos instance. We then issue the sh.addShard() shell command to add the first shard, as shown in the following screenshot:

We then continue to add the shard2 and shard3 servers to complete the sharded cluster. Before actually sharding a collection, we need to enable sharding for the target database. We'll cover this next.
As an additional safeguard, just setting up the sharded cluster does not automatically start the sharding process. First, you need to enable sharding at the database level. Otherwise, just imagine the chaos that might occur! The primary command to enable sharding is sh.enableSharding(). The generic syntax is shown here. Obviously, you need to substitute the actual name of the database:
sh.enableSharding("DATABASE")
Continuing with our sharded cluster model, still connected to a mongos instance, we issue the appropriate command for the biglittle database, as shown here:

MongoDB elects a shard to serve as a primary, much as it does within members of a replica set. We use sh.status() to confirm that all three servers have been added to the sharded cluster, as shown here:

We are now ready to choose a shard key from the document fields of the target collection to be sharded.
For the purposes of this example, we'll shard the world_cities collection. The sample data for this collection can be found at /path/to/repo/sample_data/biglittle_world_cities_insert.js. Here is a sample document from that collection:
{
"_id" : ObjectId("5e8d4851049ec9ee6b5cfd37"),
"geonameid" : "3040051",
"name" : "les Escaldes",
"asciiname" : "les Escaldes",
"latitude" : "42.50729",
"longitude" : "1.53414",
"feature class" : "P",
"feature code" : "PPLA",
"country code" : "AD",
"cc2" : "",
"admin1_code" : "08",
"admin2_code" : "",
"admin3_code" : "",
"admin4_code" : "",
"population" : "15853",
"elevation" : "",
"dem" : "1033",
"timezone" : "Europe/Andorra",
"modification_date" : "2008-10-15"
}
To facilitate future application queries, we choose the range-based sharding strategy. This allows us to issue read and write requests with advance knowledge of the contents of the shard key. Some of the fields you can see here are unique (for example, _id and geonameid), giving us a maximum amount of cardinality. Some fields might be missing information (for example admin3_code and admin4_code), and are thus not good candidates for a shard key. The field labeled population is random and changes frequently, and is thus also unsuitable as a shard key.
Two potential candidates might be timezone and country code. country code might turn out to be too restrictive, however, as the distribution of data is skewed. On the other hand, timezone could potentially be extremely useful when generating requests. It is also possible to use both fields as the shard key.
Before we perform the actual collection sharding operation, however, as data already exists, we need to create an index on timezone, which is the chosen shard key field. Here is the command we issue:
db.world_cities.createIndex( { "timezone": 1} );
Now, it's time to shard the collection.
To shard the collection, you must open a shell to a mongos instance. The command to shard a collection is sh.shardCollection(). The generic syntax is shown here:
sh.shardCollection(DB.COLLECTION, KEY, UNIQUE, OPTIONS)
The parameters are summarized here:
The following is the command that's used to shard the world_cities collection.
In our example, this looks as follows:
collection = "biglittle.world_cities";
shardKey = { "timezone" : 1 };
sh.shardCollection(collection, shardKey);
Here is a screenshot of the result:

And that concludes this chapter!
In this chapter, you learned about MongoDB sharded clusters. We discussed establishing a business rationale for deployment that included a review of the concept of vertical scaling in contrast to horizontal scaling. You then learned about sharded cluster configuration and the key factors involved in choosing an appropriate shard key: cardinality, frequency, and rate of change. You also learned about the difference between hashed and range-based sharding strategies.
You then learned how to deploy a sharded cluster by using Docker technology to model a sharded cluster for testing and development purposes. You also learned about how to deploy the key components of a sharded cluster: the config server, shards, and the mongos instance. After that, you learned that the config server must be deployed as a replica set and that each shard in the sharded cluster should also be a replica set in order to maintain data integrity and best performance.
In the next chapter, you'll learn how to manage a sharded cluster, as well as learning about programming considerations when writing code that interacts with a sharded cluster.
In this chapter, you will learn how to manage a sharded cluster. Management aspects include monitoring the shard status and exerting control over the distribution of data between shards. You will also learn about assigning and managing chunks and zones. After that, you will learn about the impact of sharding on the code of an application program.
In this chapter, we will cover the following topics:
Minimum recommended hardware:
Software requirements:
Installation of the required software and how to restore the code repository for the book is explained in Chapter 2, Setting Up MongoDB 4.x.
To run the code examples in this chapter, open a Terminal window (Command Prompt) and enter these commands:
cd /path/to/repo/chapters/16
docker-compose build
docker-compose up -d
This brings up three Docker containers that are used as part of the demonstration replica set configuration. Further instructions on the replica set setup are given in the chapter. When you have finished working with the examples covered in this chapter, return to the Terminal window (Command Prompt) and stop Docker, as follows:
cd /path/to/repo/chapters/16
docker-compose down
The code used in this chapter can be found in the book's GitHub repository at https://github.com/PacktPublishing/Learn-MongoDB-4.x/tree/master/chapters/16.
In order to properly manage a sharded cluster, you need to be able to monitor its status. You might also need to divide the data into zones, which can then be assigned to shards. You also have the ability to manually split chunks, adjust the chunk size, and handle backup and restore operations. Another topic of interest is managing the sharded cluster balancer, a process that ensures an even distribution of documents into chunks, and chunks into shards. The first topic we'll now examine is monitoring the sharded cluster status.
The most important general-purpose monitoring command is sh.status(). You need to run this command from a shell connected to a mongos instance, as a database user with sufficient cluster administration rights. A screenshot of the output for our sharded cluster model is shown in the Enabling sharding on the database sub-section of Chapter 15, Deploying a Sharded Cluster.
Look carefully for the following information, summarized here:
| Information key | What to look for |
| shards | Should show a list of shards, with a list of tags exactly as you specified. |
| active mongoses | How many mongos instances are connected and their version numbers. |
| replica set | Whether or not autosplit (chunks are automatically split) is enabled on this sharded cluster. |
| balancer | Failed balancer rounds in last 5 attempts should be 0. Migration Results for the last 24 hours: You should see a number followed by Success. If you see any number greater than 0 in the Failed balancer rounds in the last 5 seconds value, look at the log files of each of the shard servers to determine the problem. Possible problems could include communications errors, permissions errors, incorrect shard configuration, and missing or invalid indexes. |
| databases | Under the key for the database you are monitoring, look for chunks. You should see an even distribution of chunks between shards, as per your zones. Next, you see a list of ranges. Make sure they are associated with the correct shard. You should then see a list of tags. These should correspond to the zones you created. |
As an example of an error, note the following entry after running sh.status():

If we then examine the log of the primary mongod instance in the repl_shard_1 replica set, we spot the cause of the problem, as illustrated in the following screenshot:

In this example, the shard migration failed due to a missing or invalid index. Other useful methods operate on the collection directly.
The db.collection.getShardDistribution() collection-level command gives you information on how many chunks exist and on which shards they reside. The output from our example system is shown here, using world_cities as the collection:

We now have a look at sharded cluster zones.
An advantage of range-based sharding is that you can create zones (https://docs.mongodb.com/manual/core/zone-sharding/#zones) within the sharded data. A zone is an arbitrary tag or label you can assign to a range of documents based on the shard key value. The zones can then be later associated with specific shards to allow for the geographic distribution of data. Ranges cannot overlap. Further, the field (or fields) representing the shard key must be indexed so that you are assured the range values are consecutive.
To work with zones within a sharded cluster, you need to do two things: define the zones and then assign ranges. Let's start with the zone definition.
When you define a zone, you also associate a shard to the zone. This can be accomplished with the sh.addShardToZone() helper method. Here is the generic syntax:
sh.addShardToZone(SHARD, ZONE);
Both arguments are strings. SHARD represents the name of the shard, and ZONE is an arbitrary tag or label. In our sharded cluster model, let's say we wish to create zones based on continents and oceans. We might issue the following set of commands to create zones and distribute them between shards:
sh.addShardToZone("repl_shard_1", "americas");
sh.addShardToZone("repl_shard_1", "atlantic");
sh.addShardToZone("repl_shard_2", "africa");
sh.addShardToZone("repl_shard_2", "europe");
sh.addShardToZone("repl_shard_3", "asia");
sh.addShardToZone("repl_shard_3", "australia");
sh.addShardToZone("repl_shard_3", "indian");
sh.addShardToZone("repl_shard_3", "pacific");
Here is a screenshot of the output from the preceding command:

Next, we have a look at assigning ranges to the newly added zones.
To assign ranges to zones, from a mongo shell connected to a mongos instance, use the sh.updateZoneKeyRange() helper method. The generic syntax for the shell helper method is as follows:
sh.updateZoneKeyRange(DB.COLLECTION, MIN, MAX, ZONE);
The arguments are summarized here:
In this example, we divide the world_cities collection into ranges based on the timezone field. Here is how this set of commands might appear:
target = "biglittle.world_cities";
sh.updateZoneKeyRange(target, { "timezone":MinKey},
{ "timezone":"America/Anchorage"}, "africa");
sh.updateZoneKeyRange(target, { "timezone":"America/Anchorage"},
{ "timezone":"Arctic/Longyearbyen"}, "americas");
sh.updateZoneKeyRange(target, { "timezone":"Arctic/Longyearbyen"},
{ "timezone":"Asia/Aden"}, "atlantic");
sh.updateZoneKeyRange(target, { "timezone":"Asia/Aden"},
{ "timezone":"Australia/Adelaide"}, "asia");
sh.updateZoneKeyRange(target, { "timezone":"Australia/Adelaide"},
{ "timezone":"Europe/Amsterdam"}, "australia");
sh.updateZoneKeyRange(target, { "timezone":"Europe/Amsterdam"},
{ "timezone":"Indian/Antananarivo"}, "europe");
sh.updateZoneKeyRange(target, { "timezone":"Indian/Antananarivo"},
{ "timezone":"Pacific/Apia"}, "indian");
sh.updateZoneKeyRange(target, { "timezone":"Pacific/Apia"},
{ "timezone":MaxKey}, "pacific");
Here is the output from the last command listed:

We now need to have a look at working with the sharded cluster balancer.
As mentioned in the previous chapter, the sharded cluster balancer continuously manages the distribution of documents into chunks and chunks into shards. The goal of the balancer is to ensure an even distribution of data between servers in the sharded cluster. It's extremely important to note that, for the most part, it's best to let the balancer do its job. However, situations may arise where you wish to manually intervene in this distribution process by managing the size of chunks, splitting chunks, and manually migrating chunks between shards. In such cases, you need to know how to manage the balancer (https://docs.mongodb.com/manual/tutorial/manage-sharded-cluster-balancer/#manage-sharded-cluster-balancer).
The first consideration is to determine the state of the balancer.
To determine the balancer state, here are two mongo shell helper methods that might prove to be useful. Bear in mind that these can only be issued when connected to an appropriate mongos instance:
Now, let's look at disabling the balancer.
In order to disable the balancer, especially before an operation that might impact sharding, you not only need to stop the balancer but you must also wait until it has stopped running before proceeding with the operation. Examples of where this is required would be prior to performing a backup, splitting a chunk, or manually migrating a chunk.
To disable the balancer, proceed as follows:
sh.stopBalancer("DB.COLLECTION");
sh.getBalancerState();
Here is the output from the sharded cluster model used as an example in this chapter:

Of course, you also need to know how to enable the balancer!
To enable the balancer, proceed as follows:
sh.startBalancer("DB.COLLECTION");
sh.getBalancerState();
Here is the output of this sequence from the sharded cluster model:

Let's now examine how to manage chunk size.
As mentioned earlier in the previous chapter, the chunk is the lowest block of data the sharded cluster balancer can manage. Normally, the sharded cluster balancer automatically creates a new chunk when it has grown past the default (or assigned) chunk size. The default chunk size is 64 megabytes (MB). Documents are then migrated between chunks until a balance occurs. Further, with zone-to-shard assignments in mind, the balancer also migrates chunks between shards to maintain a balanced load.
There are several approaches to managing chunk size: adjusting the chunk size, which triggers the automatic balancing process; or, manually splitting chunks. Let's now have a look at adjusting the chunk size.
The default chunk size of 64 MB works well for 90% of the deployments you make. However, due to the server or network constraints, you might notice that automatic migrations are producing input/output (I/O) bottlenecks. If you increase the default chunk size, you experience a lower number of automatic migrations (as there is more room within each chunk, giving the balancer more leeway in its allocations); however, each migration takes more time. On the other hand, if you decrease the default chunk size, you might notice an uptick in the number of automatic migrations; however, each migration occurs more rapidly.
To change the default chunk size, proceed as follows:
use config;
db.settings.save( { _id:"chunksize", value: SIZE } )
We now turn our attention to manually splitting a chunk.
The sh.status() and db.collection.getShardDistribution() commands, described earlier, allow you to determine the actual distribution of documents between chunks. Once you have determined that the distribution is uneven, you have the option to manually split a chunk. The shell helper method you can use is sh.splitAt(). The generic syntax is shown here:
sh.splitAt(DB.COLLECTION, QUERY);
The first argument, DB.COLLECTION, is a string representing the database and collection names. The second argument, QUERY, is a JSON document that describes the lower boundary shard key at which the split should occur.
Using our sharded cluster model, we first issue the db.collection.getShardDistribution() shell command to get an idea of the current distribution, using world_cities as the collection, as illustrated in the following screenshot:

We can then use sh.status() to review shard-to-zone distribution (not shown). Assuming the distribution is uneven, we proceed to use sh.splitAt() to break up the offending chunks. Based on status and shard distribution information, we decide to break up the chunks residing on repl_shard_2. In order to effectuate the split, we need to carefully consult our notes to determine the shard key ranges associated with the second shard: africa and europe.
As it's most likely the case that the number of documents representing Europe is large, we perform a number of database queries to determine what is the best split between European time zones. However, because there are many timezones that start with Europe, we need to aggregate totals from a substring of the timezone field that includes the letters Europe followed by a slash and only the first letter. Accordingly, here is a query using the aggregation framework to accomplish that purpose:
doc = [
{ $match : { "timezone" : /^Europe/ }},
{ $addFields : { "count" : 1, "prefix" : \
{"$substrBytes" : ["$timezone", 0, 8]}}},
{ $group : { _id : "$prefix", "total" : { "$sum" : "$count" }}},
{ $sort : { "_id" : 1 }}
]
db.world_cities.aggregate(doc);
And here are the results of that query:

As you can see from the query result, the number of documents for time zones starting with Europe appears to be evenly spread between the Europe/A* and Europe/L* ranges and the range from Europe/M* to Europe/Z*. In order to accomplish the split, let's first create and test a query document using db.collection.find(). Please note that in order for the split to be successful, the split_query document must identify an exact shard key value, not a pattern or substring. Here is an example of such a query:
split_query = {"timezone" : "Europe/Madrid"}
db.world_cities.find(split_query, {"timezone":1}).count();
If the results are within acceptable parameters, we can proceed with the split by first turning off the sharded cluster balancer. We then execute the split and turn the balancer back on. Here are the commands to issue:
sh.stopBalancer();
sh.getBalancerState();
sh.splitAt("biglittle.world_cities", split_query);
sh.startBalancer();
Here is the resulting output:

If you now rerun db.world_cities.getShardDistribution(), you can see that there are now nine chunks in our sharded cluster model. If we ran sh.status() right now (not shown), we would also see three shards now residing in shard 2.
You can also manually perform a migration, covered briefly next.
To manually migrate a chunk, use the following command:
db.adminCommand( { moveChunk : DB.COLLECTION, find : QUERY, to : TARGET } )
The parameters are summarized as follows:
The process of determining the best query document is identical to that described in the previous sub-section when determining a split point.
The last topic to cover is backup and restore, featured next.
There are several considerations when performing a backup on a sharded cluster. First of all, and most importantly, backing up a sharded cluster using mongodump and restoring using mongorestore cannot be used. The reason for this is because it is impossible for backups created using mongodump to guarantee database integrity if sharded transactions are in progress. Accordingly, the only realistic backup solution for a sharded collection is to use filesystem snapshots.
Another extremely important consideration is to disable the balancer before the filesystem snapshot backup is taken. Once the backup is complete, you can then re-enable the balancer. For more information on disabling and re-enabling the balancer, see the Disabling the balancer and Enabling the balancer sub-sections earlier in this chapter.
It is possible to schedule the balancer to run only at certain times. In this manner, you are able to open a window of time for backups to occur. Here is an example that sets up the balancer to run only between 02:00 in the morning until 23:59 at night. This leaves a 2-hour window during which backups can take place:
use config;
db.settings.update(
{ _id : "balancer" },
{ $set : { activeWindow : { start : "02:00", stop : "23:59" } } }, true );
Otherwise, bear in mind that it is also highly recommended that you implement each shard as a replica set. You are thus assured of a higher availability factor and are protected against a total crash of your database. We now turn out attention to application program development in sharded clusters.
The primary difference when writing application code that addresses a sharded collection is to make sure you connect to a mongos instance. Otherwise, as shards are normally implemented as replica sets, the same considerations with regard to access to a replica set, addressed in Chapter 13, Deploying a Replica Set, would apply. The first consideration we'll examine is how to perform a basic connection to a sharded cluster.
There are three important differences for an application accessing data on a sharded cluster, listed as follows:
To modify the port number, all you need to do is to modify the connection string argument supplied to the pymongo.mongo_client.MongoClient instance, as shown here, in a /path/to/repo/chapters/16/test_shard_one_server.py file:
hostName = 'server1.biglittle.local:27018';
client = MongoClient(hostName);
Another consideration, however, is that shards are most likely going to be configured as replica sets, requiring a further alteration of the connection string, as illustrated in the following code snippet:
hostName = 'repl_shard_1/server1.biglittle.local:27018, \
server2.biglittle.local:27018';
client = MongoClient(hostName);
Unless you connect to the mongos instance, however, you only receive results from data stored on that shard. As an example, have a look at this block of code. First, we connect to a single shard:
import pprint
from pymongo import MongoClient
hostName = 'shard1.biglittle.local:27018';
client = MongoClient(hostName);
We then execute three queries from the world_cities collection, knowing the data resides on three different shards. The code for this can be seen in the following snippet:
count = {}
tz = 'America/Anchorage'
docs = client.biglittle.world_cities.count_documents({'timezone':tz})
count.update({tz : docs})
tz = 'Europe/Amsterdam'
docs = client.biglittle.world_cities.count_documents({'timezone':tz})
count.update({tz : docs})
tz = 'Asia/Aden'
docs = client.biglittle.world_cities.count_documents({'timezone':tz})
count.update({tz : docs})
And finally, here is a simple output routine:
print('{:>20} | {:>6s}'.format('Timezone', 'Count'))
print('{:>20} | {:>6s}'.format('--------', '-----'))
for key,val in count.items() :
output = '{:>20} | {:6d}'.format(key, val)
print(output)
When we run the example, note here that the output only draws data from the first shard:

To correct this situation, we modify the connection string to the mongos instance instead, as follows (full code example can be found at: /path/to/repo/chapters/16/test_shard_mongos.py):
hostName = 'mongos1.biglittle.local:27020';
client = MongoClient(hostName);
And here are the revised results:

Next, we look at manipulating read preferences when working with a sharded cluster.
As mentioned earlier, you need to configure your application to connect to a mongos instance in order to retrieve full results from all shards.
However, it is extremely important to note that although you can perform queries without using the shard key, it forces the mongos instance to perform a broadcast operation. As mentioned earlier, this operation first sends out a query to all shards before retrieving the data. In addition, if you do not include the shard key in the query, its index is not used, further degrading performance.
As an example, from the sample data, let's assume that the management wants a count of all world_cities documents in England. The main difference in your program code is that you need to connect to a mongos instance rather than a mongod instance. Here is a sample program that achieves this result:
from pymongo import MongoClient
hostName = 'mongos1.biglittle.local:27020';
client = MongoClient(hostName);
code = 'ENG'
query = {'admin1_code' : code}
proj = {'name' : 1}
result = client.biglittle.world_cities.find(query, proj)
plan = result.explain()
count = 0
for doc in result :
count += 1
print('There are {} documents for the admin code {}'.format(count, code))
pprint.pprint(plan)
The resulting partial output appears as follows:

The output produced by pymongo.cursor.Cursor.explain() includes an executeStats.executionStages.executionTimeMillis parameter. As you can see from the screenshot, the performance was an abysmal 12 milliseconds. To remedy the situation, we hint to the shard by including the shard key in the query. Here is the modified query document:
code = 'ENG'
tz = 'Europe/London'
query = {'$and' : [{'timezone' : tz},{'admin1_code' : code}]}
The remaining code is exactly the same. Notice here the difference in performance:

The final value for the execution time dropped to 4 milliseconds, representing a 300% improvement in response time. The complete code for the two contrasting examples just shown can be found here:
Now, we look at how to write to a sharded cluster.
Assuming that your sharded cluster has been implemented as a series of replica sets, one per shard, writing data to a sharded cluster is no different than writing data to a replica set. Here are some considerations for insert, update, and delete operations:
And that concludes the discussion for this chapter.
This chapter showed you how to monitor and manage the sharded cluster. You learned about creating zones based on the shard key, and how to manage the sharded cluster balancer, adjust chunk sizes, and split a chunk. This was followed by a discussion of the impact of sharding on your backup strategy.
The last section covered the impact of a sharded cluster on application code. You learned, first of all, that the application must connect to a mongos instance in order to reach the entire sharded collection. You then learned that database read operations experience improved performance if the shard key is included in the query. Likewise, you learned that it's best to include the shard key in the query document associated with update and delete operations.
If you enjoyed this book, you may be interested in these other books by Packt:
MongoDB 4 Quick Start Guide
Doug Bierer
ISBN: 978-1-78934-353-3
Mastering MongoDB 4.x - Second Edition
Alex Giamas
ISBN: 978-1-78961-787-0
Please share your thoughts on this book with others by leaving a review on the site that you bought it from. If you purchased the book from Amazon, please leave us an honest review on this book's Amazon page. This is vital so that other potential readers can see and use your unbiased opinion to make purchasing decisions, we can understand what our customers think about our products, and our authors can see your feedback on the title that they have worked with Packt to create. It will only take a few minutes of your time, but is valuable to other potential customers, our authors, and Packt. Thank you!