© Charles Bell 2018
Charles BellIntroducing the MySQL 8 Document Storehttps://doi.org/10.1007/978-1-4842-2725-1_9

9. Library Application: Database Implementations

Charles Bell1 
(1)
Warsaw, Virginia, USA
 

Now that we have a firm foundation in Flask and how the user interface is designed, we are ready to begin writing the code for each of the versions of the application starting with the relational database version.

As you will see, the evolution of the application from a pure relational model to a document model demonstrates how we can avoid some of the messier aspects of using relational data—even in the hybrid example. One element that may surprise you is the length and complexity of the code for the document store version is considerably shorter and easier to understand. What better reason to consider writing all your future applications using the MySQL Document Store!

The following sections describe the three versions of the library application. Because they all use the same user interface code, we omit discussions of the user interface for brevity and present only snapshots of the application executing where appropriate to illustrate the differences. The following briefly recaps the versions. Each version implements a different form of data storage.
  • Version 1—relational database: implements a traditional relational database model using only the nondocument store features.

  • Version 2—relational database + JSON fields (hybrid): implements a relational database model augmented with JSON fields.

  • Version 3—document store: implements a pure document store (NoSQL) solution.

The following sections present the entire code for the database components for each version along with the appropriate changes to the user interface. Rather than present snippets of code potentially out of context, the entire code for each version is presented for clarity and completeness. As a result, this chapter is a bit longer.

Note

Recall from Chapter 8 that we used a directory structure to organize the code. For example, we have folders named version1, version2, and version3 for each version of the application. If you are following along, be sure to place the files discussed in the appropriate folder.

Version 1: Relational Database

This version implements a traditional relational database solution in which we model the data based on the views or data items. For the purposes of demonstration, we will implement the database code in a code module that we can import into the application code. This code module will use the older MySQL Connector/Python protocol and API. That is, we will not be using the X DevAPI and will rely on SQL statements to work with the data.

Let’s start with a brief overview of the database design. Because relational database is familiar to most readers interested in the MySQL Document Store, we will skip any lengthy discussions and present the database with a brief introduction and a look at the SQL CREATE statements.

Database Design

The database for this version is named library_v1. In the spirit of good relational database design, we will create a table to store the data in discrete tables for authors, publishers, and books. We will also create a separate table to store the notes because this data is referenced less often and could be potentially long strings. We will use foreign keys to ensure consistency between these three tables. Because we can have more than one author for each book, we need to create a join table to manage the many-to-many relationship between books and authors. Thus, we will create five tables in total. Figure 9-1 shows the Entity Relationship Diagram (ERD) for the library_v1 database complete with indexes and foreign keys.
../images/432285_1_En_9_Chapter/432285_1_En_9_Fig1_HTML.jpg
Figure 9-1

ERD diagram—library database (version 1)

We also need a way to retrieve the primary keys from the authors table for a given book via the ISBN column. We use this data when we query the database for the data for a given book. To make it easier to maintain, we will create a stored routine (function) to retrieve a comma-separated list of the AuthorId column in the authors table. We use this to populate the SelectMultipleField in the book template. Finally, we will need another stored routine (function) that retrieves the author names for a given book via the ISBN column. We then will use this data to populate the list view of the books table.

Listing 9-1 shows the CREATE statements for all seven of these objects. If you want to follow along building this version of the application as you read, you should create a file named library_v1.sql so you can recreate the database later if needed. The database uses only tables and stored routines to keep the discussion short.

CREATE DATABASE `library_v1`;
CREATE TABLE `library_v1`.`authors` (
    `AuthorId` int(11) NOT NULL AUTO_INCREMENT,
    `FirstName` varchar(64) DEFAULT NULL,
    `LastName` varchar(64) DEFAULT NULL,
    PRIMARY KEY (`AuthorId`)
) ENGINE=InnoDB;
CREATE TABLE `library_v1`.`publishers` (
  `PublisherId` int(11) NOT NULL AUTO_INCREMENT,
  `Name` varchar(128) NOT NULL,
  `City` varchar(32) DEFAULT NULL,
  `URL` text,
  PRIMARY KEY (`PublisherId`)
) ENGINE=InnoDB;
CREATE TABLE `library_v1`.`books` (
  `ISBN` char(32) NOT NULL,
  `Title` text NOT NULL,
  `Year` int(11) NOT NULL DEFAULT '2017',
  `Edition` int(11) DEFAULT '1',
  `PublisherId` int(11) DEFAULT NULL,
  `Language` char(24) NOT NULL DEFAULT 'English',
  PRIMARY KEY (`ISBN`),
  KEY `pub_id` (`PublisherId`),
  CONSTRAINT `books_ibfk_1` FOREIGN KEY (`PublisherId`)
    REFERENCES `library_v1`.`publishers` (`publisherid`)
) ENGINE=InnoDB;
CREATE TABLE `library_v1`.`notes` (
  `NoteId` int(11) NOT NULL AUTO_INCREMENT,
  `ISBN` char(32) NOT NULL,
  `Note` text,
  PRIMARY KEY (`NoteId`,`ISBN`),
  KEY `ISBN` (`ISBN`),
  CONSTRAINT `notes_fk_1` FOREIGN KEY (`ISBN`)
    REFERENCES `library_v1`.`books` (`isbn`)
) ENGINE=InnoDB;
CREATE TABLE `library_v1`.`books_authors` (
  `ISBN` char(32) NOT NULL,
  `AuthorId` int(11) DEFAULT NULL,
  KEY `auth_id` (`AuthorId`),
  KEY `isbn_id` (`ISBN`),
  CONSTRAINT `books_authors_fk_1` FOREIGN KEY (`ISBN`)
    REFERENCES `library_v1`.`books` (`isbn`),
  CONSTRAINT `books_authors_fk_2` FOREIGN KEY (`AuthorId`)
    REFERENCES `library_v1`.`authors` (`authorid`)
) ENGINE=InnoDB;
DELIMITER //
CREATE FUNCTION `library_v1`.`get_author_ids`(isbn_lookup char(32))
  RETURNS varchar(128) DETERMINISTIC
  RETURN (
    SELECT GROUP_CONCAT(library_v1.authors.AuthorId SEPARATOR ', ') AS author_ids
      FROM library_v1.books_authors JOIN library_v1.authors
        ON books_authors.AuthorId = authors.AuthorId
      WHERE ISBN = isbn_lookup GROUP BY library_v1.books_authors.ISBN
)//
CREATE FUNCTION `library_v1`.`get_author_names`(isbn_lookup char(32))
  RETURNS varchar(128) DETERMINISTIC
    RETURN (
      SELECT GROUP_CONCAT(library_v1.authors.LastName SEPARATOR ', ') AS author_names
        FROM library_v1.books_authors JOIN library_v1.authors
          ON books_authors.AuthorId = authors.AuthorId
      WHERE ISBN = isbn_lookup GROUP BY library_v1.books_authors.ISBN
)//
DELIMITER ;
Listing 9-1

Library Version 1 Database Create Script (library_v1.sql)

Now that the database has been created, let’s see the code for the database class.

Tip

There is a database creation script for each version in the sample code for this book. See the Apress web site for this book to download the source code.

Database Code

The code for working with the database is placed in a file named library_v1.py in the database folder under the version1 folder as described in Chapter 8 under the section, “Preparing the Directory Structure.” Because most of the code is common to older Python applications that use the MySQL Connector/Python connector, we discuss only the salient points for each portion of the code.

That said the code implements four classes: one for each of the data views (author, publisher, book) and another class for interfacing with the server. These classes are named Author, Publisher, Book, and Library, respectfully.

Note

To use the library placed in the database folder, you must create an empty file named __init__.py in the database folder.

SQL Strings

To make the code easier to maintain and to modify it if any changes are needed for the SQL statements, we place these in the preamble of the code module as strings that we can reference later in the code. Doing this also helps keep code line lengths shorter. Listing 9-2 shows the preamble for the library_v1.py code module. Note that it begins with importing the MySQL Connector/Python library.

import mysql.connector
ALL_BOOKS = """
    SELECT DISTINCT book.ISBN, book.ISBN, Title, publisher.Name as Publisher,
                    Year, library_v1.get_author_names(book.ISBN) as Authors
    FROM library_v1.books As book
        INNER JOIN library_v1.publishers as publisher ON
                   book.PublisherId=publisher.PublisherId
        INNER JOIN library_v1.books_authors as book_author ON
                   book.ISBN = book_author.ISBN
        INNER JOIN library_v1.authors as a ON book_author.AuthorId = a.AuthorId
    ORDER BY book.ISBN DESC
"""
GET_LASTID = "SELECT @@last_insert_id"
#
# Author SQL Statements
#
INSERT_AUTHOR = """
    INSERT INTO library_v1.authors (LastName, FirstName) VALUES ('{0}','{1}')
"""
GET_AUTHORS = "SELECT AuthorId, LastName, FirstName FROM library_v1.authors {0}"
UPDATE_AUTHOR = """
    UPDATE library_v1.authors SET LastName = '{0}',
    FirstName='{1}' WHERE AuthorId = {2}
"""
DELETE_AUTHOR = """
    DELETE FROM library_v1.authors WHERE AuthorId = {0}
"""
#
# Publisher SQL Statements
#
INSERT_PUBLISHER = """
    INSERT INTO library_v1.publishers (Name, City, URL) VALUES ('{0}','{1}','{2}')
"""
GET_PUBLISHERS = "SELECT * FROM library_v1.publishers {0}"
UPDATE_PUBLISHER = "UPDATE library_v1.publishers SET Name = '{0}'"
DELETE_PUBLISHER = "DELETE FROM library_v1.publishers WHERE PublisherId = {0}"
#
# Book SQL Statements
#
INSERT_BOOK = """
    INSERT INTO library_v1.books (ISBN, Title, Year, PublisherId, Edition,
    Language) VALUES ('{0}','{1}','{2}','{3}',{4},'{5}')
"""
INSERT_BOOK_AUTHOR = """
    INSERT INTO library_v1.books_authors (ISBN, AuthorId) VALUES ('{0}', {1})
"""
INSERT_NOTE = "INSERT INTO library_v1.notes (ISBN, Note) VALUES ('{0}','{1}')"
GET_BOOKS = "SELECT * FROM library_v1.books {0}"
GET_NOTES = "SELECT * FROM library_v1.notes WHERE ISBN = '{0}'"
GET_AUTHOR_IDS = "SELECT library_v1.get_author_ids('{0}')"
UPDATE_BOOK = "UPDATE library_v1.books SET ISBN = '{0}'"
DELETE_BOOK = "DELETE FROM library_v1.books WHERE ISBN = '{0}'"
DELETE_BOOK_AUTHOR = "DELETE FROM library_v1.books_authors WHERE ISBN = '{0}'"
DELETE_NOTES = "DELETE FROM library_v1.notes WHERE ISBN = '{0}'"
Listing 9-2

Initialization and SQL Statements (library_v1.py)

That’s a lot of SQL, isn’t it? If it seems daunting, consider that most relational database applications have a similar set of SQL statements. Consider also that this example application is purposefully small and limited. Taking those into account, imagine the number and complexity of SQL statements for a much larger application. Wow.

Next, let’s look at the Author class.

Author Class

The Author class is the least complicated and forms a model for how the other data classes are constructed. In particular, we save an instance of the Library class via the constructor and reference this instance whenever we execute queries (or use any of the methods in the Library class). We then build four functions—one each for create, read, update, and delete. Listing 9-3 shows the Author class code. The read operation is designed to return one row if the primary key is passed as a parameter or all rows if no parameter is supplied.

Note that we use the Library class function sql() to execute queries. For example, self.library.sql(“COMMIT”) executes the COMMIT SQL command. We use the strings created previously using the format() function to fill in the optional parameters. We will see this function in more detail later in this section. Take some time to read through the code to ensure you understand it.

class Author(object):
    """Author class
    This class encapsulates the authors table permitting CRUD operations
    on the data.
    """
    def __init__(self, library):
        self.library = library
    def create(self, LastName, FirstName):
        assert LastName, "You must supply a LastName for a new author."
        assert FirstName, "You must supply a FirstName for a new author."
        query_str = INSERT_AUTHOR
        last_id = None
        try:
            self.library.sql(query_str.format(LastName, FirstName))
            last_id = self.library.sql(GET_LASTID)
            self.library.sql("COMMIT")
        except Exception as err:
            print("ERROR: Cannot add author: {0}".format(err))
        return last_id
    def read(self, AuthorId=None):
        query_str = GET_AUTHORS
        if not AuthorId:
            # return all authors
            query_str = query_str.format("")
        else:
            # return specific author
            query_str = query_str.format("WHERE AuthorId = '{0}'".format(AuthorId))
        return self.library.sql(query_str)
    def update(self, AuthorId, LastName, FirstName):
        assert AuthorId, "You must supply an AuthorId to update the author."
        assert LastName, "You must supply a LastName for the author."
        assert FirstName, "You must supply a FirstName for the author."
        query_str = UPDATE_AUTHOR
        try:
            self.library.sql(query_str.format(LastName, FirstName, AuthorId))
            self.library.sql("COMMIT")
        except Exception as err:
            print("ERROR: Cannot update author: {0}".format(err))
    def delete(self, AuthorId):
        assert AuthorId, "You must supply an AuthorId to delete the author."
        query_str = DELETE_AUTHOR.format(AuthorId)
        try:
            self.library.sql(query_str)
            self.library.sql("COMMIT")
        except Exception as err:
            print("ERROR: Cannot delete author: {0}".format(err))
Listing 9-3

Author Class (library_v1.py)

Next, let’s look at the Publisher class.

Publisher Class

The Publisher class is very similar to the Author class and is implemented in the same manner. The only difference is in the SQL statements used. To be complete, Listing 9-4 shows the complete code for the Publisher class.

class Publisher(object):
    """Publisher class
    This class encapsulates the publishers table permitting CRUD operations
    on the data.
    """
    def __init__(self, library):
        self.library = library
    def create(self, Name, City=None, URL=None):
        assert Name, "You must supply a Name for a new publisher."
        query_str = INSERT_PUBLISHER
        last_id = None
        try:
            self.library.sql(query_str.format(Name, City, URL))
            last_id = self.library.sql(GET_LASTID)
            self.library.sql("COMMIT")
        except Exception as err:
            print("ERROR: Cannot add publisher: {0}".format(err))
        return last_id
    def read(self, PublisherId=None):
        query_str = GET_PUBLISHERS
        if not PublisherId:
            # return all authors
            query_str = query_str.format("")
        else:
            # return specific author
            query_str = query_str.format(
                "WHERE PublisherId = '{0}'".format(PublisherId))
        return self.library.sql(query_str)
    def update(self, PublisherId, Name, City=None, URL=None):
        assert PublisherId, "You must supply a publisher to update the author."
        query_str = UPDATE_PUBLISHER.format(Name)
        if City:
            query_str = query_str + ", City = '{0}'".format(City)
        if URL:
            query_str = query_str + ", URL = '{0}'".format(URL)
        query_str = query_str + " WHERE PublisherId = {0}".format(PublisherId)
        try:
            self.library.sql(query_str)
            self.library.sql("COMMIT")
        except Exception as err:
            print("ERROR: Cannot update publisher: {0}".format(err))
    def delete(self, PublisherId):
        assert PublisherId, "You must supply a publisher to delete the publisher."
        query_str = DELETE_PUBLISHER.format(PublisherId)
        try:
            self.library.sql(query_str)
            self.library.sql("COMMIT")
        except Exception as err:
            print("ERROR: Cannot delete publisher: {0}".format(err))
Listing 9-4

Publisher Class (library_v1.py)

Next, a look at the Book class.

Book Class

The Book class has the same methods as the last two, but the code for create, update, and delete is a little more complex. This is because we must execute multiple statements to work with the data. Therefore, we implicitly start a transaction inside a try block and if any of the queries fail, we rollback the transaction. This is very common for relational database solutions. Listing 9-5 shows the complete code for the Book class. Take your time to read through the code to understand how it was constructed.

class Book(object):
    """Book class
    This class encapsulates the books table permitting CRUD operations
    on the data.
    """
    def __init__(self, library):
        self.library = library
    def create(self, ISBN, Title, Year, PublisherId, Authors=[], Edition=1,
               Language='English'):
        assert ISBN, "You must supply an ISBN for a new book."
        assert Title, "You must supply Title for a new book."
        assert Year, "You must supply a Year for a new book."
        assert PublisherId, "You must supply a PublisherId for a new book."
        assert Authors, "You must supply at least one AuthorId for a new book."
        last_id = ISBN
        #
        # We must do this as a transaction to ensure all tables are updated.
        #
        try:
            self.library.sql("START TRANSACTION")
            query_str = INSERT_BOOK.format(ISBN, Title, Year, PublisherId,
                                           Edition, Language)
            self.library.sql(query_str)
            query_str = INSERT_BOOK_AUTHOR
            for AuthorId in Authors.split(","):
                self.library.sql(query_str.format(ISBN, AuthorId))
            self.library.sql("COMMIT")
        except Exception as err:
            print("ERROR: Cannot add book: {0}".format(err))
            self.library.sql("ROLLBACK")
        return last_id
    def read(self, ISBN=None):
        query_str = GET_BOOKS
        if not ISBN:
            # return all authors
            query_str = query_str.format("")
        else:
            # return specific author
            query_str = query_str.format("WHERE ISBN = '{0}'".format(ISBN))
        return self.library.sql(query_str)
    def read_notes(self, ISBN):
        assert ISBN, "You must supply an ISBN to get the notes."
        query_str = GET_NOTES.format(ISBN)
        return self.library.sql(query_str)
    def read_author_ids(self, ISBN):
        assert ISBN, "You must supply an ISBN to get the list of author ids."
        query_str = GET_AUTHOR_IDS.format(ISBN)
        return self.library.sql(query_str)
    def update(self, old_isbn, ISBN, Title=None, Year=None, PublisherId=None,
               Authors=None, Edition=None, Language=None, Note=None):
        assert ISBN, "You must supply an ISBN to update the book."
        last_id = None
        #
        # Build the book update query
        #
        book_query_str = UPDATE_BOOK.format(ISBN)
        if Title:
            book_query_str += ", Title = '{0}'".format(Title)
        if Year:
            book_query_str += ", Year = {0}".format(Year)
        if PublisherId:
            book_query_str += ", PublisherId = {0}".format(PublisherId)
        if Edition:
            book_query_str += ", Edition = {0}".format(Edition)
        book_query_str += " WHERE ISBN = '{0}'".format(old_isbn)
        #
        # We must do this as a transaction to ensure all tables are updated.
        #
        try:
            self.library.sql("START TRANSACTION")
            #
            # If the ISBN changes, we must remove the author ids first to
            # avoid the foreign key constraint error.
            #
            if old_isbn != ISBN:
                self.library.sql(DELETE_BOOK_AUTHOR.format(old_isbn))
            self.library.sql(book_query_str)
            last_id = self.library.sql(GET_LASTID)
            if Authors:
                # First, clear the author list.
                self.library.sql(DELETE_BOOK_AUTHOR.format(ISBN))
                query_str = INSERT_BOOK_AUTHOR
                for AuthorId in Authors:
                    self.library.sql(query_str.format(ISBN,AuthorId))
            if Note:
                self.add_note(ISBN, Note)
            self.library.sql("COMMIT")
        except Exception as err:
            print("ERROR: Cannot update book: {0}".format(err))
            self.library.sql("ROLLBACK")
        return last_id
    def delete(self, ISBN):
        assert ISBN, "You must supply a ISBN to delete the book."
        #
        # Here, we must cascade delete the notes when we delete a book.
        # We must do this as a transaction to ensure all tables are updated.
        #
        try:
            self.library.sql("START TRANSACTION")
            query_str = DELETE_NOTES.format(ISBN)
            self.library.sql(query_str)
            query_str = DELETE_BOOK_AUTHOR.format(ISBN)
            self.library.sql(query_str)
            query_str = DELETE_BOOK.format(ISBN)
            self.library.sql(query_str)
            self.library.sql("COMMIT")
        except Exception as err:
            print("ERROR: Cannot delete book: {0}".format(err))
            self.library.sql("ROLLBACK")
    def add_note(self, ISBN, Note):
        assert ISBN, "You must supply a ISBN to add a note for the book."
        assert Note, "You must supply text (Note) to add a note for the book."
        query_str = INSERT_NOTE.format(ISBN, Note)
        try:
            self.library.sql(query_str)
            self.library.sql("COMMIT")
        except Exception as err:
            print("ERROR: Cannot add publisher: {0}".format(err))
Listing 9-5

Book Class (library_v1.py)

Finally, we look at the Library class.

Library Class

Recall, the Library class is used to encapsulate working with the MySQL server. Thus, we create functions for working with the connection (connect, disconnect, is_connected). We also create a function that we can use to execute queries. This is mainly for convenience and not generally required. The function is named sql() and handles returning result sets or errors as necessary. The last function is used to return an abbreviated dataset of the books in the database, which is used to populate the book list page. Listing 9-6 shows the code for the Library class. As you will see, it too is very straightforward.

class Library(object):
    """Library master class
    Use this class to interface with the library database. It includes
    utility functions for connections to the server as well as running
    queries.
    """
    def __init__(self):
        self.db_conn = None
    def connect(self, username, passwd, host, port, db=None):
        config = {
            'user': username,
            'password': passwd,
            'host': host,
            'port': port,
            'database': db,
        }
        try:
            self.db_conn = mysql.connector.connect(**config)
        except mysql.connector.Error as err:
            print("CONNECTION ERROR:", err)
            self.db_conn = None
            raise
    #
    # Return the connection for use in other classes
    #
    def get_connection(self):
        return self.db_conn
    #
    # Check to see if connected to the server
    #
    def is_connected(self):
        return (self.db_conn and (self.db_conn.is_connected()))
    #
    # Disconnect from the server
    #
    def disconnect(self):
        try:
            self.db_conn.disconnect()
        except:
            pass
    #
    # Execute a query and return any results
    #
    # query_str[in]      The query to execute
    # fetch          Execute the fetch as part of the operation and
    #                use a buffered cursor (default is True)
    # buffered       If True, use a buffered raw cursor (default is False)
    #
    # Returns result set or cursor
    #
    def sql(self, query_str, fetch=True, buffered=False):
        # If we are fetching all, we need to use a buffered
        if fetch:
            cur = self.db_conn.cursor(buffered=True)
        else:
            cur = self.db_conn.cursor(raw=True)
        try:
            cur.execute(query_str)
        except Exception as err:
            cur.close()
            print("Query error. Command: {0}:{1}".format(query_str, err))
            raise
        # Fetch rows (only if available or fetch = True).
        if cur.with_rows:
            if fetch:
                try:
                    results = cur.fetchall()
                except mysql.connector.Error as err:
                    print("Error fetching all query data: {0}".format(err))
                    raise
                finally:
                    cur.close()
                return results
            else:
                # Return cursor to fetch rows elsewhere (fetch = false).
                return cur
        else:
            return cur
    #
    # Get list of books
    #
    def get_books(self):
        try:
            results = self.sql(ALL_BOOKS)
        except Exception as err:
            print("ERROR: {0}".format(err))
            raise
        return results
Listing 9-6

Library Class (library_v1.py)

Now that we have the database code module, let’s look at the application code.

Application Code

There are only a few areas in the application code from the base code we saw earlier where we need to add more code. This includes adding the import statement for the database code module, setting up the Library class instance, and adding code to the author, publisher, and book view functions to use the classes in the database code module. It is fortunate that the template files we created in the user interface discussion can be used without modification.

To build this version of the application, you should copy the base/mylibrary_base.py file to version1/mylibrary_v1.py and either enter the code below or retrieve it from the Apress book web site.

Although the code may appear to be long, it isn’t complicated. Further, except for the book view function, the logic is the same pattern for the author and publisher view. The book view has more logic to enable the add note feature. The following sections discuss the changes needed for each area. Recall, we need the mylibrary_base.py code we saw in an earlier section.

Setup and Initialization

The code for setup and initialization for the Library class is simple. We need only import the classes from the code module and then create an instance of the Library class and call the connect() function as shown in the following. The import statement goes at the end of the other import statements and the Library setup code can go anywhere after that. In the example code, this code is placed before the first form class function.

from database.library_v1 import Library, Author, Publisher, Book
[...]
#
# Setup the library database class
#
library = Library()
# Provide your user credentials here
library.connect(<user>, <password>, 'localhost', 3306)

Note

Be sure to change the <user> and <password> entries to match your MySQL configuration. These are placeholders for the user account and password.

List View Function

The list view function requires only a few modifications. We will use the Library class instance (named library) to get the data from the database to be displayed in the list on the page. For books, this is simply calling the library.get_books() function. For authors, we instantiate an instance of the Author and Publisher classes then call the read() function without any parameters. Recall from the previous sections, this results in reading all the rows in the table. Listing 9-7 shows the changes needed for the simple_list() view function. The new lines of code are in bold. As you can see, we’re only adding five lines of code. Simple!

def simple_list(kind=None):
    rows = []
    columns = []
    form = ListForm()
    if kind == 'book' or not kind:
        if request.method == 'POST':
            return redirect('book')
        columns = (
            '<td style="width:200px">ISBN</td>',
            '<td style="width:400px">Title</td>',
            '<td style="width:200px">Publisher</td>',
            '<td style="width:80px">Year</td>',
            '<td style="width:300px">Authors</td>',
        )
        kind = 'book'
        # Here, we get all books in the database
        rows = library.get_books()
        return render_template("list.html", form=form, rows=rows,
                               columns=columns, kind=kind)
    elif kind == 'author':
        if request.method == 'POST':
            return redirect('author')
        # Just list the authors
        columns = (
            '<td style="width:100px">Lastname</td>',
            '<td style="width:200px">Firstname</td>',
        )
        kind = 'author'
        # Here, we get all authors in the database
        author = Author(library)
        rows = author.read()
        return render_template("list.html", form=form, rows=rows,
                               columns=columns, kind=kind)
    elif kind == 'publisher':
        if request.method == 'POST':
            return redirect('publisher')
        columns = (
            '<td style="width:300px">Name</td>',
            '<td style="width:100px">City</td>',
            '<td style="width:300px">URL/Website</td>',
        )
        kind = 'publisher'
        # Here, we get all publishers in the database
        publisher = Publisher(library)
        rows = publisher.read()
        return render_template("list.html", form=form, rows=rows,
                               columns=columns, kind=kind)
    else:
        flash("Something is wrong!")
        return
Listing 9-7

List View Function (Version 1)

Author View Function

The changes for the author view function are a bit more complicated. Because the author, publisher, and book view functions follow the same pattern, we will discuss the pattern in general first then see the code for view each function. Because the concept of GET and POST operations may be new to you, we will take a moment and discuss the differences.

We want to use this view function for both GET and POST operations. In particular, when the user clicks on an author in the list, we want to display the data from that row in the table. Or, if the user clicks on the New button, we want to present an empty HTML form for the user to complete. So far, these are GET operations. If the user then clicks on the submit field (either Add, Update, or Delete), we then want to take the data from the user and either create, update, or delete the data. These are POST operations. How this works in a view function is not immediately obvious. However, it makes sense once you get used to it. Let’s run through the conditions for how a view function is called. Table 9-1 lists the different conditions (or mode).
Table 9-1

Operations (Modes) for View Functions

Operation

Type

Action

Add

GET

Present an empty form and provide a submit field named Add

Create

POST

Save data to the database for one data item

Read

GET

Show data from the database for one data item and provide submit fields named Update and Delete

Update

POST

Save updated data to the database for existing data item

Delete

POST

Remove the data item from the database

Note that there are two GET operations and three POST operations. The GET operations are to present either an empty form or to read from a row in the table. The POST operations are events that occur when the user clicks on one of the submit fields resulting in either a create, update, or delete.

Returning the to the author view function, we need to add code for the operations listed above. Rather than discuss the code at length then present it, Listing 9-8 shows the completed code for the author view function. Line numbers have been added to make it easier to see the lines of code discussed. Detailed discussions of the database code are included after the listing.

01    def author(author_id=None):
02        author = Author(library)
03        form = AuthorForm()
04        # Get data from the form if present
05        form_authorid = form.authorid.data
06        firstname = form.firstname.data
07        lastname = form.lastname.data
08        # If the route with the variable is called, change the create button to update
09        # then populate the form with the data from the row in the table. Otherwise,
10        # remove the delete button because this will be a new data item.
11        if author_id:
12            form.create_button.label.text = "Update"
13            # Here, we get the data and populate the form
14            data = author.read(author_id)
15            if data == []:
16                flash("Author not found!")
17            form.authorid.data = data[0][0]
18            form.firstname.data = data[0][1]
19            form.lastname.data = data[0][2]
20        else:
21            del form.del_button
22        if request.method == 'POST':
23            # First, determine if we must create, update, or delete when form posts.
24            operation = "Create"
25            if form.create_button.data:
26                if form.create_button.label.text == "Update":
27                    operation = "Update"
28            if form.del_button and form.del_button.data:
29                operation = "Delete"
30            if form.validate_on_submit():
31                # Get the data from the form here
32                if operation == "Create":
33                    try:
34                        author.create(LastName=lastname, FirstName=firstname)
35                        flash("Added.")
36                        return redirect('/list/author')
37                    except Exception as err:
38                        flash(err)
39                elif operation == "Update":
40                    try:
41                        author.update(AuthorId=form_authorid, LastName=lastname,
42                                      FirstName=firstname)
43                        flash("Updated.")
44                        return redirect('/list/author')
45                    except Exception as err:
46                        flash(err)
47                else:
48                    try:
49                        author.delete(form_authorid)
50                        flash("Deleted.")
51                        return redirect('/list/author')
52                    except Exception as err:
53                        flash(err)
54            else:
55                flash_errors(form)
56        return render_template("author.html", form=form)
Listing 9-8

Author View Function (Version 1)

The first thing we do is add an instance of the Author class and pass it to the Library class instance as shown in line 2. Next, to cover operations where we need the data from the form, we place code at the top of the view function to copy data from the form to local variables as shown in lines 4–7. This ensures if the view function is called again for a POST operation that we capture any data entered by the user. If we hadn’t done this, we could not use the view function for new and existing data.

Next, we must cover the route where we pass in the primary key (in this case author_id). If the author_id is present, we change the label of one of the submit fields (add) to Update. We also know we must read data from the database, which we do with the author.read(author_id) call on line 14 and if we retrieve the row without errors, we place the data from the table into the fields in lines 17—19. If the author_id variable is not present, we remove the delete submit field on line 21.

At this point, we’ve covered the operations of add and read shown in Table 9-1. The create, update, and delete operations are executed only when the request is a POST. To determine this, we check the value of the request.method attribute on line 22. If it is POST, we then must decide which operation is active. We can do this by checking the text of the submit fields. We use a default of create but change it to update or delete based on which submit field was clicked. You can see these operations in lines 24–29.

In particular, if a submit field is clicked, on POST, the data attribute will be True. Thus, we can see which operation we need to perform based on which button was clicked. In the case of the create button, we know it is create unless the label was changed to update and in that case the operation matches—update. On the other hand, if the delete button was not removed and it was clicked, the operation is delete. This is one method of how you can reuse a view function for multiple operations.

Now that we know which operation is active, we execute the action. However, we only do so if the fields have all passed their validation checks. The code on line 30 will return True if all fields have been validated. Thus, we only execute the active operation if the form fields are validated.

The create operation is shown in lines 33–38. Note that we use a try block to detect errors. To create a new author, we simply call the author.create() function with the data from the form. Likewise, the update operation is shown in lines 40–45. Once again, we use a try block to detect errors. To update an existing author, we call the author.update() function with the data from the form. Finally, the delete operation is shown in lines 46–53. Again, we use a try block to detect errors. To delete an existing author, we call the author.delete() function with the author_id from the form.

Now, let’s look at the publisher view function, which is very similar.

Publisher View Function

Because the publisher view function is very similar to the author view function (it is the same design or pattern), I only summarize the code describing only the database operations in detail. Listing 9-9 shows the completed code for the publisher view function. Line numbers have been added to make it easier to see the lines of code discussed. Detailed discussions of the database code are included after the listing.

01    def publisher(publisher_id=None):
02        publisher = Publisher(library)
03        form = PublisherForm()
04        # Get data from the form if present
05        form_publisherid = form.publisherid.data
06        name = form.name.data
07        city = form.city.data
08        url = form.url.data
09        # If the route with the variable is called, change the create button to update then populate the form with the data from the
10        # row in the table. Otherwise, remove the delete button because
11        # this will be a new data item.
12        if publisher_id:
13            # Here, we get the data and populate the form
14            form.create_button.label.text = "Update"
15            # Here, we get the data and populate the form
16            data = publisher.read(publisher_id)
17            if data == []:
18                flash("Publisher not found!")
19            form.publisherid.data = data[0][0]
20            form.name.data = data[0][1]
21            form.city.data = data[0][2]
22            form.url.data = data[0][3]
23        else:
24            del form.del_button
25        if request.method == 'POST':
26            # First, determine if we must create, update, or delete when form posts.
27            operation = "Create"
28            if form.create_button.data:
29                if form.create_button.label.text == "Update":
30                    operation = "Update"
31            if form.del_button and form.del_button.data:
32                operation = "Delete"
33            if form.validate_on_submit():
34                # Get the data from the form here
35                if operation == "Create":
36                    try:
37                        publisher.create(Name=name, City=city, URL=url)
38                        flash("Added.")
39                        return redirect('/list/publisher')
40                    except Exception as err:
41                        flash(err)
42                elif operation == "Update":
43                    try:
44                        publisher.update(PublisherId=form_publisherid, Name=name,
45                                         City=city, URL=url)
46                        flash("Updated.")
47                        return redirect('/list/publisher')
48                    except Exception as err:
49                        flash(err)
50                else:
51                    try:
52                        publisher.delete(form_publisherid)
53                        flash("Deleted.")
54                        return redirect('/list/publisher')
55                    except Exception as err:
56                        flash(err)
57            else:
58                flash_errors(form)
59        return render_template("publisher.html", form=form)
Listing 9-9

Publisher View Function (Version 1)

A with the author view function, line 2 instantiates an instance of the Publisher class and lines 4–8 fetch data from the form for use later. Line 12 begins the section to read data from the database, line 14 changes the label of the add submit button to update, and lines 16–22 store that data in the form. Finally, lines 27–32 determine the active operation for a POST request and line 33 ensures the form fields are validated before we execute the database operations.

The create operation is shown in lines 36–41. To create a new publisher, we simply call the publisher.create() function with the data from the form. Likewise, the update operation is shown in lines 43–49. To update an existing publisher, we call the publisher.update() function with the data from the form. Finally, the delete operation is shown in lines 51–56. To delete an existing publisher, we call the publisher.delete() function with the publisher_id from the form.

Pop Quiz

Did you notice a small difference in how we handled the publisher_id? Instead of using the variable from the route, we get the publisher id from the hidden field on the form. This was done intentionally to show an alternative way to save data to the form.

But there is a good reason to use this technique even though it does duplicate a small bit of data. For example, it is possible the user will want to change the ISBN. Because the ISBN is the primary key for the table, if we use the ISBN from the GET request (the /book/978-1-4842-2724-4 route), the database operation will fail to located the row because the ISBN was changed on the form.

This also demonstrates how surrogate primary keys such as auto increment fields can help save you from this potential data land mine.

Now, let’s look at the book view function, which follows the same pattern but requires a bit more logic.

Book View Function

The book view function is more complicated than the author or publisher view function for three reasons: 1) it has more fields, 2) there are select fields that require populating, and 3) there is an additional feature for update operations to add notes to the database for the book.

However, the code follows the same pattern as the previous view functions. Listing 9-10 shows the book view function code in its entirety. Once again, line numbers have been added to enhance readability and discussion of the code follows the listing.

01    def book(isbn_selected=None):
02        notes = None
03        book = Book(library)
04        form = BookForm()
05        # Get data from the form if present
06        isbn = form.isbn.data
07        title = form.title.data
08        year = form.year.data
09        authorids = form.authors.data
10        publisherid = form.publisher.data
11        edition = form.edition.data
12        language = form.language.data
13        #
14        # Here, we get the choices for the select lists
15        #
16        publisher = Publisher(library)
17        publishers = publisher.read()
18        publisher_list = []
19        for pub in publishers:
20            publisher_list.append((pub[0], '{0}'.format(pub[1])))
21        form.publisher.choices = publisher_list
22        author = Author(library)
23        authors = author.read()
24        author_list = []
25        for author in authors:
26            author_list.append((author[0],'{0}, {1}'.format(author[2], author[1])))
27        form.authors.choices = author_list
28        new_note = form.new_note.data
29        # If the route with the variable is called, change the create button to update then populate the form with the data from
30        # the row in the table. Otherwise, remove the delete button
31        # because this will be a new data item.
32        if isbn_selected:
33            # Here, we get the data and populate the form
34            data = book.read(isbn_selected)
35            if data == []:
36                flash("Book not found!")
37
38            #
39            # Here, we populate the data
40            #
41            form.isbn.data = data[0][0]
42            form.title.data = data[0][1]
43            form.year.data = data[0][2]
44            form.edition.data = data[0][3]
45            form.publisher.process_data(data[0][4])
46            form.language.data = data[0][5]
47            #
48            # Here, we get the author_ids for the authors
49            #
50            author_ids = book.read_author_ids(isbn_selected)[0][0]
51            form.authors.data = set(author_ids)
52
53            # We also must retrieve the notes for the book.
54            all_notes = book.read_notes(isbn_selected)
55            notes = []
56            for note in all_notes:
57                notes.append(note[2])
58            form.create_button.label.text = "Update"
59        else:
60            del form.del_button
61        if request.method == 'POST':
62            # First, determine if we must create, update, or delete when form posts.
63            operation = "Create"
64            if form.create_button.data:
65                if form.create_button.label.text == "Update":
66                    operation = "Update"
67            if form.del_button and form.del_button.data:
68                operation = "Delete"
69            if form.validate_on_submit():
70                # Get the data from the form here
71                if operation == "Create":
72                    try:
73                        book.create(ISBN=isbn, Title=title, Year=year,
74                                    PublisherId=publisherid, Authors=authorids,
75                                    Edition=edition, Language=language)
76                        flash("Added.")
77                        return redirect('/list/book')
78                    except Exception as err:
79                        flash(err)
80                elif operation == "Update":
81                    try:
82                        book.update(isbn_selected, ISBN=isbn, Title=title, Year=year,
83                                    PublisherId=publisherid, Authors=authorids,
84                                    Edition=edition, Language=language,
85                                    Note=new_note)
86                        flash("Updated.")
87                        return redirect('/list/book')
88                    except Exception as err:
89                        flash(err)
90                else:
91                    try:
92                        book.delete(isbn)
93                        flash("Deleted.")
94                        return redirect('/list/book')
95                    except Exception as err:
96                        flash(err)
97            else:
98                flash_errors(form)
99        return render_template("book.html", form=form, notes=notes)
Listing 9-10

Book View Function (Version 1)

First, you may notice we have a new variable named notes, which is set to None. We do this here because we will use this variable to contain all the notes for the book as read from the database. More on that later.

As with the author and publisher view functions, line 3 instantiates an instance of the Book class and lines 6–12 fetch data from the form for use later. Next is the code for populating the select fields with values from the database. We do this because the book table is dependent on the authors (technically via the books_authors join table, and publishers tables. Thus, we need to fetch the rows from both tables to populate the dropdown and multiple select lists.

Lines 16–21 are for the publisher data. Here, we first instantiate an instance of the Publisher class then retrieve all the data from the table. Next, we loop through the rows adding the publisher id and name to a list, which is then assigned to the data attribute for the select field choices attribute (form.publisher.choices). Why do we include the publisher id? Because the publisher id is only stored in the book table.

Likewise, lines 22–27 do the same for author data creating an instance of the Author class retrieving all the rows then looping through the data to add the author id, and concatenated last and first name. As with the select field, we populate the field data with the new array. At this point, we have both select fields populated. How to set the value to match the rows comes next along with retrieving the data from the database.

Line 32 begins the section to read data from the database. Lines 34–57 retrieve the data from the database and populates for form. For the select fields, setting the data attribute ensures the values are selected. In the case of the publisher, we set the select field data and the item that matches is selected by default. In the case of the select multiple field, we pass a comma-separated list as shown in lines 50–51 where we retrieve a list of author ids from the database. Next, we retrieve the notes for the book and populate an array we use in the template to populate the HTML table.

Wow! That’s a lot of work, isn’t it? All of that work was to setup the form for the add and read operations. Fortunately, the create, update, and delete setup is the same as the other view functions. You can see this in lines 63–9.

The database operations thankfully are familiar. The create operation is shown in lines 72–79. To create a new book, we simply call the book.create() function with the data from the form. Likewise, the update operation is shown in lines 81–89. To update an existing book, we call the book.update() function with the data from the form. Finally, the delete operation is shown in lines 91–96. To delete an existing book, we call the book.delete() function with the isbn from the form.

Templates

There were no changes to the template files from the base version. All you need to do is create a new folder and copy the files from the base. In particular, copy base/templates/* to version1/templates/.

The only change you need to make is to change the “base” text in the base.html file to “V1” as shown in the difference example in the following where the line with the “-” is removed and the line with the “+” is added.

-            <a class="navbar-brand" href="/">MyLibrary Base</a>
+            <a class="navbar-brand" href="/">MyLibrary v1</a>

Now that we have the code updated, let’s see how it works!

Executing the Code

Now that the code is written, let’s give it a test drive. Be sure to create the database and any tables necessary first. If you saved the previous SQL statements in a file named library_v1.sql, you can use the SOURCE command in the mysql client as follows.

mysql> SOURCE <path>/version1/library_v1.sql;

To execute the application, you can launch it with the Python interpreter specifying the runserver command. The following shows an example of executing the application. Note that we used the port option to specify the port. You should enter this command from the version1 folder. Note that we specify the port as 5001. We will use 5002 for version 2 and 5003 for version 3. This will allow you to run all three versions simultaneously.

$ cd version1
$ python ./mylibrary_v1.py runserver -p 5001
 * Running on http://127.0.0.1:5001/ (Press CTRL+C to quit)
The application will launch and run but there isn’t any data in the database yet. You should start by grabbing a couple of your favorite books and enter the authors and publishers first then enter the book data. Don’t worry about the notes just yet. Once you’ve added a few books, you should see them in the default view (by clicking on MyLibrary v1 or Books in the navigation bar. Figure 9-2 shows an example of what you should see. The other views are similar and are left as an exercise for the reader to explore.
../images/432285_1_En_9_Chapter/432285_1_En_9_Fig2_HTML.jpg
Figure 9-2

Library application book list (version 1)

Next, try out the notes feature. Click on the Modify link in the list for one of the books in the books list and then add a note and click Update. When you next view the data by clicking on the Modify link, you will see the note appear. Figure 9-3 shows an excerpt of the notes list for a book.
../images/432285_1_En_9_Chapter/432285_1_En_9_Fig3_HTML.jpg
Figure 9-3

Notes list example (version 1)

Before we move on to version 2, let’s take a moment to discuss some observations about this version of the application.

Observations

The following are some observations about this version of the application. Some are consequences of the database design, others are from the code, and others are things we may want to change to make the application a bit better. The observations are presented in the form of an unordered list. If you want to experiment with this version of the application, you can consider some of these are challenges for improving the application. Otherwise, consider this list something to think about for the next version.
  • Lengthy code: The code for the application is quite long (over 400 lines).

  • Lengthy database code module: The code for the database code module is also quite long (over 400 lines).

  • Over-designed tables: The many-to-many table is unnecessarily complicated, which makes working with SQL a bit more difficult.

  • Database design can be improved: Savvy database administrators will undoubtedly spot areas that can be improved in the database design. For example, the use of a view can replace the query used in the get_books() function of the Library class.

  • Over-analyzed data: One of the banes of relational database design is overuse of normal forms in the face of usability. In this case, it is unlikely the user will care to know a list of authors because there is no additional meaningful information—just the author’s first and last name.

  • Simplistic read: The default mechanism to view data is a list. Although this works fine for authors and publishers, it is restrictive for books because you must click on the Modify link to see the notes for the book. This can be improved with a simple read-only mode rather than update.

  • Older protocols: There is no X DevAPI integration.

Now, let’s look at the next version of the application.

Version 2: Relational Database + JSON Fields (Hybrid)

This version implements a relational database augmented with JSON fields. We model the data based on the views or data items but use a JSON field to eliminate one of the issues of traditional relational database solutions: many-to-many joins. For the purposes of demonstration, we will implement the database code in a code module that we can import into the application code. Although we will use MySQL Connector/Python as in version 1, we will be using the X DevAPI using SQL statements to work with the data. The goal is to demonstrate how to migrate to using the X DevAPI but preserving the SQL interface. Thus, this version presents a hybrid solution.

The many-to-many relationship in the version 1 database was so that we could make a link from a book to one or more authors and that we may have more than one book with the same authors. However, like most applications, the database design has revealed a case where we have more sophistication than what is needed. In particular, we have a table for authors but find that we only store (or care about) the first and last name. Further, use of the application has shown we have no use cases for querying author data other than to list them with the book.

Therefore, we can eliminate the many-to-many relationship storing the list of author names in a JSON field instead. This has led to other minor changes such as the stored routines and other additions.

Let’s start with a brief overview of the database design after the changes. Because the database is the same as version 1 with minor changes, we will present a brief overview concentrating only on the differences.

Database Design

The database for this version is named library_v2. Because the goal is to remove the many-to-many relationship, we removed the books_authors join table replacing it with a JSON field in the books table and removed the authors table. Thus, we have reduced the database from five tables to three. Figure 9-4 shows the ERD for the library_v2 database complete with indexes and foreign keys.
../images/432285_1_En_9_Chapter/432285_1_En_9_Fig4_HTML.jpg
Figure 9-4

ERD diagram—library database (version 2)

By eliminating the many-to-many relationship, we can remove the select multiple field for the authors on the book view. We can replace it with a simple comma-separated list, which is easy to convert to JSON. Thus, we need a way to retrieve the names from the JSON field returning the comma-separated list. We can do this with a stored routine (function).

Listing 9-11 shows the CREATE statements for all the objects. If you want to follow along building this version of the application while you read, you should create a file named library_v2.sql so that you can recreate the database later.

CREATE DATABASE `library_v2`;
CREATE TABLE `library_v2`.`publishers` (
  `PublisherId` int(11) NOT NULL AUTO_INCREMENT,
  `Name` varchar(128) NOT NULL,
  `City` varchar(32) DEFAULT NULL,
  `URL` text,
  PRIMARY KEY (`PublisherId`)
) ENGINE=InnoDB;
CREATE TABLE `library_v2`.`books` (
  `ISBN` char(32) NOT NULL,
  `Title` text NOT NULL,
  `Year` int(11) NOT NULL DEFAULT '2017',
  `Edition` int(11) DEFAULT '1',
  `PublisherId` int(11) DEFAULT NULL,
  `Language` char(24) NOT NULL DEFAULT 'English',
  `Authors` JSON NOT NULL,
  PRIMARY KEY (`ISBN`),
  KEY `Pub_id` (`PublisherId`),
  CONSTRAINT `books_fk_1` FOREIGN KEY (`PublisherId`)
    REFERENCES `library_v2`.`publishers` (`publisherid`)
) ENGINE=InnoDB;
CREATE TABLE `library_v2`.`notes` (
  `NoteId` int(11) NOT NULL AUTO_INCREMENT,
  `ISBN` char(32) NOT NULL,
  `Note` text,
  PRIMARY KEY (`NoteId`,`ISBN`),
  KEY `ISBN` (`ISBN`),
  CONSTRAINT `notes_fk_1` FOREIGN KEY (`ISBN`)
    REFERENCES `library_v2`.`books` (`isbn`)
) ENGINE=InnoDB;
DELIMITER //
CREATE FUNCTION `library_v2`.`get_author_names`(isbn_lookup char(32))
  RETURNS text DETERMINISTIC
BEGIN
  DECLARE j_array varchar(255);
  DECLARE num_items int;
  DECLARE i int;
  DECLARE last char(20);
  DECLARE first char(20);
  DECLARE csv varchar(255);
  SET j_array = (SELECT JSON_EXTRACT(Authors,'$.authors')
                 FROM library_v2.books WHERE ISBN = isbn_lookup);
  SET num_items = JSON_LENGTH(j_array);
  SET csv = "";
  SET i = 0;
  author_loop: LOOP
    IF i < num_items THEN
      SET last = CONCAT('$[',i,'].LastName');
      SET first = CONCAT('$[',i,'].FirstName');
      IF i > 0 THEN
        SET csv = CONCAT(csv,", ",JSON_UNQUOTE(JSON_EXTRACT(j_array,last)),' ',
                         JSON_UNQUOTE(JSON_EXTRACT(j_array,first)));
      ELSE
        SET csv = CONCAT(JSON_UNQUOTE(JSON_EXTRACT(j_array,last)),' ',
                         JSON_UNQUOTE(JSON_EXTRACT(j_array,first)));
      END IF;
      SET i = i + 1;
    ELSE
      LEAVE author_loop;
    END IF;
  END LOOP;
  RETURN csv;
END//
DELIMITER ;
Listing 9-11

Library Version 2 Database Create Script (library_v2.sql)

Note the new function named get_author_names(). The function retrieves the JSON document from the row matching the ISBN and creates a comma-separated list of authors. This is used in the presentation of the author data to make it easier for users to view.

Now that we’ve got the database created, let’s see the code for the database class.

Database Code

The code for working with the database is placed in a file named library_v2.py in the database folder under the version2 folder as described in Chapter 8 under the section, “Preparing the Directory Structure.” The code is based on version 1 converted to use the X DevAPI, and there is no longer a need for a class for the authors table. That said the code implements three classes: one for each of the data views—publisher and book—and another class for interfacing with the server. These classes are named Publisher, Book, and Library, respectfully.

However, because the code is based on version 1, I discuss the changes rather than another lengthy discussion on the classes and how they work. The following summarizes the changes.
  • The ALL_BOOKS query is considerably shorter and easier to maintain.

  • A new GET_PUBLISHER_NAME query is added to populate the book list.

  • The INSERT_BOOK query needs an additional column for the authors JSON document.

  • All the queries for the authors table are removed.

  • We change GET_AUTHOR_IDS to GET_AUTHOR_NAMES since we're only working with names in the JSON document.

  • The database name changes from library_v1 to library_v2.

To create the file, you can simply copy the file from version1/database/library_v1.py to version2/database/library_v2.py.

Code Deleted

Begin by deleting the Authors class and the SQL statements for the author table. They will not be needed.

SQL Strings

Because this version also uses SQL statements, place these in the preamble of the code module as strings that can be referenced later in the code. Listing 9-12 shows the preamble for the library_v2.py code module, which replaces what was used for the first version. Note that it begins with importing the MySQL Connector/Python X DevAPI library. The changes listed earlier (aside from the version 1 to 2 rename) are shown in bold in the listing.

import mysqlx
ALL_BOOKS = """
    SELECT DISTINCT book.ISBN, book.ISBN, Title, PublisherId, Year,
                    library_v2.get_author_names(book.ISBN) as Authors
    FROM library_v2.books As book
    ORDER BY book.ISBN DESC
"""
GET_PUBLISHER_NAME = """
    SELECT Name
    FROM library_v2.publishers
    WHERE PublisherId = {0}
"""
GET_LASTID = "SELECT @@last_insert_id"
INSERT_PUBLISHER = """
    INSERT INTO library_v2.publishers (Name, City, URL) VALUES ('{0}','{1}','{2}')
"""
GET_PUBLISHERS = "SELECT * FROM library_v2.publishers {0}"
UPDATE_PUBLISHER = "UPDATE library_v2.publishers SET Name = '{0}'"
DELETE_PUBLISHER = "DELETE FROM library_v2.publishers WHERE PublisherId = {0}"
INSERT_BOOK = """
    INSERT INTO library_v2.books (ISBN, Title, Year, PublisherId, Edition,
    Language, Authors) VALUES ('{0}','{1}','{2}','{3}',{4},'{5}','{6}')
"""
INSERT_NOTE = "INSERT INTO library_v2.notes (ISBN, Note) VALUES ('{0}','{1}')"
GET_BOOKS = "SELECT * FROM library_v2.books {0}"
GET_NOTES = "SELECT * FROM library_v2.notes WHERE ISBN = '{0}'"
GET_AUTHOR_NAMES = "SELECT library_v2.get_author_names('{0}')"
UPDATE_BOOK = "UPDATE library_v2.books SET ISBN = '{0}'"
DELETE_NOTES = "DELETE FROM library_v2.notes WHERE ISBN = '{0}'"
DELETE_BOOK = "DELETE FROM library_v2.books WHERE ISBN = '{0}'"
Listing 9-12

Initialization and SQL Statements (library_v2.py)

If you recall the length of this same code from version 1, note that we’ve reduced the number of strings quite a lot. This is largely due to removing the authors table and the many-to-many relationship. So, adding a single JSON field has had a huge impact!

Before we discuss the changes to the Publisher and Book classes, let’s discuss the changes to the Library class.

Library Class

The library class is based on version 1, but because we’re using the X DevAPI, things will work quite differently. In particular, we will open a session to make a connection to the MySQL server on port 33060 (the default for the X Protocol), and we will use a SQLStatement object for executing SQL statements. The following summarizes the changes to the Library class.

The following lists a summary of the changes for the Library class.
  • We use a session object instead of a connection object.

  • The connect() function is changed to retrieve a session from the mysql_x library.

  • The sql() function is vastly simplified to only return the result from session.sql()—a SQLStatment object.

  • We add a make_rows() function to convert the row results from the SQLStatement object into an array.

  • The get_books() function calls the make_rows() function chaining the SQLStatement execute() function (passed as a parameter).

Note

The changes to the Library class are designed to demonstrate how to migrate from the old protocol to using the X DevAPI. As you will see, it is possible to minimize changes to existing database libraries as well as dependent code by using the same methods but with different database access methods.

Listing 9-13 shows the modified Library class. The changes from version 1 are shown in bold. Note that we have the same methods as the first version but instead of a connection object, we use a session object and the renamed functions to get and check the session. These are always good functions to include as you may need them as you develop more advanced features.

class Library(object):
    """Library master class
    Use this class to interface with the library database. It includes
    utility functions for connections to the server and returning a
    SQLStatement object.
    """
    def __init__(self):
        self.session = None
    #
    # Connect to a MySQL server at host, port
    #
    # Attempts to connect to the server as specified by the connection
    # parameters.
    #
    def connect(self, username, passwd, host, port):
        config = {
            'user': username,
            'password': passwd,
            'host': host,
            'port': port,
        }
        try:
            self.session = mysqlx.get_session(**config)
        except Exception as err:
            print("CONNECTION ERROR:", err)
            self.session = None
            raise
    #
    # Return the session for use in other classes
    #
    def get_session(self):
        return self.session
    #
    # Check to see if connected to the server
    #
    def is_connected(self):
        return (self.session and (self.session.is_open()))
    #
    # Disconnect from the server
    #
    def disconnect(self):
        try:
            self.session.close()
        except:
            pass
    #
    # Get an SQLStatement object
    #
    def sql(self, query_str):
        return self.session.sql(query_str)
    #
    #  Build row array
    #
    #  Here, we cheat a bit and give an option to substitute the publisher name
    #  for publisher Id column.
    #
    def make_rows(self, sql_res, get_publisher=False):
        cols = []
        for col in sql_res.columns:
            cols.append(col.get_column_name())
        rows = []
        for row in sql_res.fetch_all():
            row_item = []
            for col in cols:
                if get_publisher and (col == 'PublisherId'):
                    query_str = GET_PUBLISHER_NAME.format(row.get_string(col))
                    name = self.session.sql(query_str).execute().fetch_one()[0]
                    row_item.append("{0}".format(name))
                else:
                    row_item.append("{0}".format(row.get_string(col)))
            rows.append(row_item)
        return rows
    #
    # Get list of books
    #
    def get_books(self):
        try:
            sql_stmt = self.sql(ALL_BOOKS)
            results = self.make_rows(sql_stmt.execute(), True)
        except Exception as err:
            print("ERROR: {0}".format(err))
            raise
        return results
Listing 9-13

Library Class (library_v2.py)

Note how much shorter the sql() function is compared to version 1. Recall, the sql() function from version 1 was 30 lines in length. Using the SQLStatement object instance has saved us a lot of coding! We will see this theme continue in version 3. In fact, we see the get_books() function is also a bit shorter. Nice.

There is a new function as previously mentioned. The function, make_rows() takes the result object, fetches all of the rows, and converts it to a list. There may be more effective ways to do this, but this demonstrates some of what you may need to do to transition your existing code to use the X DevAPI.

Next, let’s look at the Publisher class.

Publisher Class

The Publisher class is nearly the same as the version 1 code except we adapt it for use with the X DevAPI. In particular, because we are getting a SQLStatement object returned from the sql() function in the Library class, we can chain that with the execute() function of the SQLStatement instance and get the result. We also utilize the make_rows() function of the Library class to make an array for the rows in a result. Listing 9-14 shows the complete code for the Publisher class with the changes shown in bold for clarity.

class Publisher(object):
    """Publisher class
    This class encapsulates the publishers table permitting CRUD operations
    on the data.
    """
    def __init__(self, library):
        self.library = library
    def create(self, Name, City=None, URL=None):
        assert Name, "You must supply a Name for a new publisher."
        query_str = INSERT_PUBLISHER
        last_id = None
        try:
            self.library.sql(query_str.format(Name, City, URL)).execute()
            last_id = self.library.make_rows(
                self.library.sql(GET_LASTID).execute())[0][0]
            self.library.sql("COMMIT").execute()
        except Exception as err:
            print("ERROR: Cannot add publisher: {0}".format(err))
        return last_id
    def read(self, PublisherId=None):
        query_str = GET_PUBLISHERS
        if not PublisherId:
            # return all authors
            query_str = query_str.format("")
        else:
            # return specific author
            query_str = query_str.format(
                "WHERE PublisherId = '{0}'".format(PublisherId))
        sql_stmt = self.library.sql(query_str)
        return self.library.make_rows(sql_stmt.execute())
    def update(self, PublisherId, Name, City=None, URL=None):
        assert PublisherId, "You must supply a publisher to update the author."
        query_str = UPDATE_PUBLISHER.format(Name)
        if City:
            query_str = query_str + ", City = '{0}'".format(City)
        if URL:
            query_str = query_str + ", URL = '{0}'".format(URL)
        query_str = query_str + " WHERE PublisherId = {0}".format(PublisherId)
        try:
            self.library.sql(query_str).execute()
            self.library.sql("COMMIT").execute()
        except Exception as err:
            print("ERROR: Cannot update publisher: {0}".format(err))
    def delete(self, PublisherId):
        assert PublisherId, "You must supply a publisher to delete the publisher."
        query_str = DELETE_PUBLISHER.format(PublisherId)
        try:
            self.library.sql(query_str).execute()
            self.library.sql("COMMIT").execute()
        except Exception as err:
            print("ERROR: Cannot delete publisher: {0}".format(err))
Listing 9-14

Publisher Class (library_v2.py)

As you can see, the changes are minimal and once again demonstrate how easy it is to migrate code to the new X DevAPI.

Now let’s look at the Book class, which has a similar short list of changes.

Book Class

The Book class, like the Publisher class, has few changes from the version 1 code. We have the same changes for using the X DevAPI but we also need to handle converting the comma-separated list of authors to a JSON document. We will use a helper function for this. We also reduce the complexity of the code by removing the join table. The following summarizes the changes to this version of the Book class.
  • We use the sql() function of the Library class chaining the execute() function to execute the SQL statement.

  • We prepare the SQLStatement object instance before we call the make_rows() function of the Library class.

  • We add a function make_authors_json() to convert a comma-separated list of author names to a JSON document.

  • We remove the code for working with the books_authors table.

Listing 9-15 shows the complete code for the Book class for clarity with the changes shown in bold. As you will see, despite adding more lines for working with JSON documents, the code is a bit shorter than the previous version.

class Book(object):
    """Book class
    This class encapsulates the books table permitting CRUD operations
    on the data.
    """
    def __init__(self, library):
        self.library = library
    def make_authors_json(self, author_list=None):
        from json import JSONEncoder
        if not author_list:
            return None
        author_dict = {"authors":[]}
        authors = author_list.split(",")
        for author in authors:
            try:
                last, first = author.strip(' ').split(' ')
            except Exception as err:
                last = author.strip(' ')
                first = ''
            author_dict["authors"].append({"LastName":last,"FirstName":first})
        author_json = JSONEncoder().encode(author_dict)
        return author_json
    def create(self, ISBN, Title, Year, PublisherId, Authors=[], Edition=1,
               Language='English'):
        assert ISBN, "You must supply an ISBN for a new book."
        assert Title, "You must supply Title for a new book."
        assert Year, "You must supply a Year for a new book."
        assert PublisherId, "You must supply a publisher for a new book."
        assert Authors, "You must supply at least one Author for a new book."
        query_str = INSERT_BOOK
        last_id = ISBN
        try:
            author_json = self.make_authors_json(Authors)
            self.library.sql(query_str.format(ISBN, Title, Year, PublisherId,
                                              Edition, Language,
                                              author_json)).execute()
            self.library.sql("COMMIT").execute()
        except Exception as err:
            print("ERROR: Cannot add book: {0}".format(err))
            self.library.sql("ROLLBACK").execute()
        return last_id
    def read(self, ISBN=None):
        query_str = GET_BOOKS
        if not ISBN:
            # return all authors
            query_str = query_str.format("")
        else:
            # return specific author
            query_str = query_str.format("WHERE ISBN = '{0}'".format(ISBN))
        sql_stmt = self.library.sql(query_str)
        return self.library.make_rows(sql_stmt.execute())
    #
    # Get the notes for this book
    #
    def read_notes(self, ISBN):
        assert ISBN, "You must supply an ISBN to get the notes."
        query_str = GET_NOTES.format(ISBN)
        sql_stmt = self.library.sql(query_str)
        return self.library.make_rows(sql_stmt.execute())
    #
    # Get the authors for this book
    #
    def read_authors(self, ISBN):
        assert ISBN, "You must supply an ISBN to get the list of author ids."
        query_str = GET_AUTHOR_NAMES.format(ISBN)
        sql_stmt = self.library.sql(query_str)
        return self.library.make_rows(sql_stmt.execute())
    def update(self, old_isbn, ISBN, Title=None, Year=None, PublisherId=None,
               Authors=None, Edition=None, Language=None, Note=None):
        assert ISBN, "You must supply an ISBN to update the book."
        last_id = None
        #
        # Build the book update query
        #
        book_query_str = UPDATE_BOOK.format(ISBN)
        if Title:
            book_query_str += ", Title = '{0}'".format(Title)
        if Year:
            book_query_str += ", Year = {0}".format(Year)
        if PublisherId:
            book_query_str += ", PublisherId = {0}".format(PublisherId)
        if Edition:
            book_query_str += ", Edition = {0}".format(Edition)
        if Authors:
            author_json = self.make_authors_json(Authors)
            book_query_str += ", Authors = '{0}'".format(author_json)
        book_query_str += " WHERE ISBN = '{0}'".format(old_isbn)
        #
        # We must do this as a transaction to ensure all tables are updated.
        #
        try:
            self.library.sql("START TRANSACTION").execute()
            self.library.sql(book_query_str).execute()
            if Note:
                self.add_note(ISBN, Note)
            self.library.sql("COMMIT").execute()
        except Exception as err:
            print("ERROR: Cannot update book: {0}".format(err))
            self.library.sql("ROLLBACK").execute()
        return last_id
    def delete(self, ISBN):
        assert ISBN, "You must supply a ISBN to delete the book."
        #
        # Here, we must cascade delete the notes when we delete a book.
        # We must do this as a transaction to ensure all tables are updated.
        #
        try:
            self.library.sql("START TRANSACTION").execute()
            query_str = DELETE_NOTES.format(ISBN)
            self.library.sql(query_str).execute()
            query_str = DELETE_BOOK.format(ISBN)
            self.library.sql(query_str).execute()
            self.library.sql("COMMIT").execute()
        except Exception as err:
            print("ERROR: Cannot delete book: {0}".format(err))
            self.library.sql("ROLLBACK").execute()
    #
    # Add a note for this book
    #
    def add_note(self, ISBN, Note):
        assert ISBN, "You must supply a ISBN to add a note for the book."
        assert Note, "You must supply text (Note) to add a note for the book."
        query_str = INSERT_NOTE.format(ISBN, Note)
        try:
            self.library.sql(query_str).execute()
            self.library.sql("COMMIT").execute()
        except Exception as err:
            print("ERROR: Cannot add note: {0}".format(err))
Listing 9-15

Book Class (library_v2.py)

Note the new function, make_author_json(), which demonstrates how to build a JSON document. In this case, it is a simple JSON array built using the Python JSON encoder. We also see in the update() function how to incorporate the JSON document into our UPDATE SQL statement. Sweet!

That wasn’t too bad, was it? Now, let’s look at the changes to the application code.

Application Code

There are some minor changes in the application code from the version 1 code we saw earlier. This includes adapting the user interface to remove the authors view and add the authors list to the book view form. Fortunately, most of the code from version 1 can be used without modification.

To build this version of the application, you should copy the version1/mylibrary_v1.py file to version2/mylibrary_v2.py and either enter the code below or retrieve it from the Apress book web site. The following lists the changes for the application code. Although this looks like a long list, most are trivial changes. The following sections describe the changes in more detail.
  • Remove the Author class from the import statement.

  • Change the port from 3306 to 33060.

  • Remove the NewSelectMultipleField class as it is no longer needed (it was used in the book view form to show a list of authors to choose).

  • Remove the author view function and template.

  • Replace the multiple select field on the book detail page to a text field.

  • Remove the author list from the list view function.

  • Change the code to read a list of author names rather than ids.

  • Add the author list to create and update calls to the Book class.

  • Pass the list of author names from the new text field to the render_template() function in the book view function.

  • No changes are needed for the publisher view function, form class, or template.

  • No changes are needed for the list form class or template.

  • The base template was changed to indicate version 2 of the application.

We look at the changes starting with the changes to setup and initialization.

Setup and Initialization

Changes to the setup and initialization are trivial. We need only remove the Author class from the imports, change library_v1 to library_v2, and change the default port in the connect() function as shown in the following.

from database.library_v2 import Library, Publisher, Book
...
library.connect(<user>, <password>, 'localhost', 33060)

Form Classes

First, we can remove the AuthorForm and NewSelectMultipleField classes because we don't need them. It is fortunate that there are no changes needed for the PublisherForm class. Even the BookForm class has only a minor change to switch the multiple select field to a text field. Listing 9-16 shows the modified BookForm class code with the changes in bold. As you will see, it is only one line of code to change.

class BookForm(FlaskForm):
    isbn = TextField('ISBN ', validators=[
            Required(message=REQUIRED.format("ISBN")),
            Length(min=1, max=32, message=RANGE.format("ISBN", 1, 32))
        ])
    title = TextField('Title ',
                      validators=[Required(message=REQUIRED.format("Title"))])
    year = IntegerField('Year ',
                        validators=[Required(message=REQUIRED.format("Year"))])
    edition = IntegerField('Edition ')
    language = TextField('Language ', validators=[
            Required(message=REQUIRED.format("Language")),
            Length(min=1, max=24, message=RANGE.format("Language", 1, 24))
        ])
    publisher = NewSelectField('Publisher ',
                    validators=[Required(message=REQUIRED.format("Publisher"))])
    authors = TextField('Authors (comma separated by LastName FirstName)',
                    validators=[Required(message=REQUIRED.format("Author"))])
    create_button = SubmitField('Add')
    del_button = SubmitField('Delete')
    new_note = TextAreaField('Add Note')
Listing 9-16

Book Form Class (Version 2)

View Functions

First, we can remove the author() view function as it is no longer needed. It is fortunate that there are no changes needed for the publisher view function.

However, we need to modify the simple_list() view function to remove the author list option. Listing 9-17 shows the modified template with the area where code was removed as shown in bold.

def simple_list(kind=None):
    rows = []
    columns = []
    form = ListForm()
    if kind == 'book' or not kind:
        if request.method == 'POST':
            return redirect('book')
        columns = (
            '<td style="width:200px">ISBN</td>',
            '<td style="width:400px">Title</td>',
            '<td style="width:200px">Publisher</td>',
            '<td style="width:80px">Year</td>',
            '<td style="width:300px">Authors</td>',
        )
        kind = 'book'
        # Here, we get all books in the database
        rows = library.get_books()
        return render_template("list.html", form=form, rows=rows,
                               columns=columns, kind=kind)
    elif kind == 'publisher':
        if request.method == 'POST':
            return redirect('publisher')
        columns = (
            '<td style="width:300px">Name</td>',
            '<td style="width:100px">City</td>',
            '<td style="width:300px">URL/Website</td>',
        )
        kind = 'publisher'
        # Here, we get all publishers in the database
        publisher = Publisher(library)
        rows = publisher.read()
        return render_template("list.html", form=form, rows=rows,
                               columns=columns, kind=kind)
    else:
        flash("Something is wrong!")
        return
Listing 9-17

List View Function (Version 2)

We also need to modify the book view function. There are more changes needed in this section because the authors for a book are now a JSON document and we use a comma-separated list to specify them in the book detail form. The following lists the changes needed for this code.
  • We change the authorids list of ids to author_list to contain the comma-separated list.

  • We remove the Author() class code.

  • We change fetching a list of author ids to a list retrieving the comma-separated list.

  • We do not need the author list for the template file.

Listing 9-18 shows the changes to the book view function with the changes shown in bold.

def book(isbn_selected=None):
    notes = None
    book = Book(library)
    form = BookForm()
    # Get data from the form if present
    isbn = form.isbn.data
    title = form.title.data
    year = form.year.data
    author_list = form.authors.data
    publisherid = form.publisher.data
    edition = form.edition.data
    language = form.language.data
    #
    # Here, we get the choices for the select lists
    #
    publisher = Publisher(library)
    publishers = publisher.read()
    publisher_list = []
    for pub in publishers:
        publisher_list.append((pub[0], '{0}'.format(pub[1])))
    form.publisher.choices = publisher_list
    new_note = form.new_note.data
    # If the route with the variable is called, change the create button to update
    # then populate the form with the data from the row in the table. Otherwise,
    # remove the delete button because this will be a new data item.
    if isbn_selected:
        # Here, we get the data and populate the form
        data = book.read(isbn_selected)
        if data == []:
            flash("Book not found!")
        #
        # Here, we populate the data
        #
        form.isbn.data = data[0][0]
        form.title.data = data[0][1]
        form.year.data = data[0][2]
        form.edition.data = data[0][3]
        form.publisher.process_data(data[0][4])
        form.language.data = data[0][5]
        form.authors.data = book.read_authors(isbn_selected)[0][0]
        # We also must retrieve the notes for the book.
        all_notes = book.read_notes(isbn_selected)
        notes = []
        for note in all_notes:
            notes.append(note[2])
        form.create_button.label.text = "Update"
    else:
        del form.del_button
    if request.method == 'POST':
        # First, determine if we must create, update, or delete when form posts.
        operation = "Create"
        if form.create_button.data:
            if form.create_button.label.text == "Update":
                operation = "Update"
        if form.del_button and form.del_button.data:
            operation = "Delete"
        if form.validate_on_submit():
            # Get the data from the form here
            if operation == "Create":
                try:
                    book.create(ISBN=isbn, Title=title, Year=year,
                                PublisherId=publisherid, Authors=author_list,
                                Edition=edition, Language=language)
                    flash("Added.")
                    return redirect('/list/book')
                except Exception as err:
                    flash(err)
            elif operation == "Update":
                try:
                    book.update(isbn_selected, isbn, Title=title, Year=year,
                                PublisherId=publisherid, Authors=author_list,
                                Edition=edition, Language=language,
                                Note=new_note)
                    flash("Updated.")
                    return redirect('/list/book')
                except Exception as err:
                    flash(err)
            else:
                try:
                    book.delete(isbn)
                    flash("Deleted.")
                    return redirect('/list/book')
                except Exception as err:
                    flash(err)
        else:
            flash_errors(form)
    return render_template("book.html", form=form, notes=notes,
                           authors=author_list)
Listing 9-18

Book View Function (Version 2)

No additional changes are needed for the application code. Once again, that wasn’t so bad. We’re not done just yet. There are some minor changes needed for the templates.

Templates

The changes to the template files are minor. If you haven’t already done so, copy the templates from version 1 to version 2. For example, copy all the files from version1/templates/* to version2/templates. Once copied, you can remove the author.html template as we no longer need it.

We also need to make two small changes to the base.html file to change the version number and remove the author list from the navigation bar. Listing 9-19 shows an excerpt from the base.html file with the changes shown in bold.

<div class="navbar navbar-inverse" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">MyLibrary v2</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a href="/list/book">Books</a></li>
            </ul>
            <ul class="nav navbar-nav">
                <li><a href="/list/publisher">Publishers</a></li>
            </ul>
        </div>
    </div>
</div>
Listing 9-19

Base Template (Version 2)

We must also make two small changes to the book.html template to show a text field for the comma-separated list of authors. Listing 9-20 shows an excerpt of the modified template with the changes in bold.

{% extends "base.html" %}
{% block title %}MyLibrary Search{% endblock %}
{% block page_content %}
  <form method=post> {{ form.csrf_token }}
    <fieldset>
      <legend>Book - Detail</legend>
      {{ form.hidden_tag() }}
      <div style=font-size:20pz; font-weight:bold; margin-left:150px;>
        {{ form.isbn.label }} <br>
        {{ form.isbn(size=32) }} <br>
        {{ form.title.label }} <br>
        {{ form.title(size=100) }} <br>
        {{ form.year.label }} <br>
        {{ form.year(size=10) }} <br>
        {{ form.edition.label }} <br>
        {{ form.edition(size=10) }} <br>
        {{ form.language.label }} <br>
        {{ form.language(size=34) }} <br>
        {{ form.publisher.label }} <br>
        {{ form.publisher(style="width: 300px;") }} <br><br>
        {{ form.authors.label }} <br>
        {{ form.authors(size=100) }} <br>
        {# Show the new note text field if this is an update. #}
        {% if form.create_button.label.text == "Update" %}
          <br>{{ form.new_note.label }} <br>
          {{ form.new_note(rows='2',cols='100') }}
        {% endif %}
...
Listing 9-20

Book Template (Version 2)

Ok, that’s it for the changes now let’s see the code in action.

Executing the Code

Now that we’ve got the code written, let’s give it a test drive. To execute the application, you can launch it with the Python interpreter specifying the runserver command. The following shows an example of executing the application. Note that we used the port option to specify the port. You should enter this command from the version2 folder.

$ cd version2
$ python ./mylibrary_v2.py runserver -p 5002
 * Running on http://127.0.0.1:5002/ (Press CTRL+C to quit)
The application will launch and run but there isn’t any data in the database yet. You should start by grabbing a couple of your favorite books and enter the authors and publishers first then enter the book data. Don’t worry about the notes just yet. Once you’ve added a few books, you should see them in the default view (by clicking on MyLibrary v2 or Books in the navigation bar. Figure 9-5 shows an example of what you should see. The other views are similar and are left as an exercise for the reader to explore.
../images/432285_1_En_9_Chapter/432285_1_En_9_Fig5_HTML.jpg
Figure 9-5

Library application book list (version 2)

Note that we removed the author entry in the navigation bar because we no longer have a detailed view. Rather, the author list is stored in a JSON document with the book. Figure 9-6 shows the new form.
../images/432285_1_En_9_Chapter/432285_1_En_9_Fig6_HTML.jpg
Figure 9-6

Book detailed view (version 2)

Note that the authors entry is now a text field instead of a multiple select list. Some may see this as more intuitive while others may feel the multiple select list is better. The comma-separated list was chosen for demonstration purposes, but feel free to experiment with your own ideas for how to collect and display information about the authors for a book.

The publisher view is unchanged from version 1.

Before we move on to version 3, let’s take a moment to discuss some observations about this version of the application.

Observations

The following are some observations about this version of the application. Some are consequences of the database design, others are from the code, and others are things we may want to change to make the application a bit better. The observations are presented in the form of an unordered list. If you want to experiment with this version of the application, you can consider some of them challenges for improving the application. Otherwise, consider this list something to think about for the next version.
  • Further simplify database with JSON: The notes table also can be converted to a JSON field in the books table because there is no need to query the notes table without viewing it in context with a book and one row in the notes table matches one and only one row in the books table.

  • The database code is shorter: We need less code in the database code module to implement the application.

  • The application code is shorter: We need less code in the application.

  • Some conversion code is needed for JSON: Although Python provides a library for working with JSON and it is possible to use JSON documents directly in Python as data structures, we need to add code to convert JSON to a more human readable form. In this case, it was working with a list of author names.

  • Author list may need to be improved: Although designed for demonstration purposes, the comma-separated list may not be the best choice for novice users.

Now, let’s look at the last version of the application.

Version 3: Document Store

This version implements a pure document store version of the data. For the purposes of demonstration, we will implement the database code in a code module that we can import into the application code. We will be using the X DevAPI managing a collection to store and retrieve the data. The goal is to demonstrate how to migrate to using JSON documents instead of the SQL interface.

To do this, we will flatten the database from multiple tables to a single collection of documents—more specifically, a collection of books. Let’s start with a brief overview of the design of the database.

Database Design

Calling this a database design is a little archaic as we aren’t working with a database logically but a schema in the X DevAPI terminology. Implementation wise it is still a database in MySQL and will show as such in the SHOW DATABASES command as shown in the following (library_v3) in the output from the MySQL Shell.

$ mysqlsh root@localhost:33060 -mx --sql
Creating an X protocol session to 'root@localhost:33060'
...
 MySQL  localhost:33060+ ssl  SQL > SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| animals            |
| information_schema |
| library_v1         |
| library_v2         |
| library_v3         |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
8 rows in set (0.00 sec)

The database (schema) contains only one table, which was created as a collection. You can do this using the commands shown here in the MySQL Shell.

$ mysqlsh root@localhost:33060 -mx --py
Creating an X protocol session to 'root@localhost:33060'
...
 MySQL  localhost:33060+ ssl  Py > import mysqlx
 MySQL  localhost:33060+ ssl  Py > session = mysqlx.get_session('root:password@localhost:33060')
 MySQL  localhost:33060+ ssl  Py > schema = session.create_schema('library_v3')
 MySQL  localhost:33060+ ssl  Py > collection = schema.create_collection('books')

Note that we get a session then create the schema and finally create the collection. This new collection will appear in the library_v3 database as a table named books, but its CREATE statement looks very different. The following shows the CREATE statement for the table. You should never need to use this statement and should always use the MySQL Shell and X DevAPI to create schemas, collections, or any object in the X DevAPI pantheon.

MySQL  localhost:33060+ ssl  SQL > SHOW CREATE TABLE library_v3.books G
*************************** 1. row ***************************
       Table: books
Create Table: CREATE TABLE `books` (
  `doc` json DEFAULT NULL,
  `_id` varchar(32) GENERATED ALWAYS AS (json_unquote(json_extract(`doc`,_utf8mb4'$._id'))) STORED NOT NULL,
  PRIMARY KEY (`_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

Don’t worry that the CREATE statement looks strange. It’s supposed to look this way. After all, it is a collection implemented as a table of rows containing a document id and a JSON field. Note that there is a primary key defined so that will make lookups by id fast. Cool.

Now that we’ve got the schema (database) and collection (table) created, let’s see the code for the database class.

Database Code

The changes to the database code for this version of the application are a bit longer than the last version. Although we switched to using the X DevAPI in version 2, we were still using an SQL interface. This version uses a pure X DevAPI interface.

The code for working with the database is placed in a file named library_v3.py in the database folder under the version3 folder as described in Chapter 8 under the section, “Preparing the Directory Structure.” The code is based on version 2 converted to use the X DevAPI, and we no longer need a class for the publishers table. However, because the code is based on version 2, we will discuss the changes rather than another lengthy discussion on the classes and how they work. The following summarizes the changes.
  • Add the JSONEncoder import statement.

  • Remove all the SQL statements (yes!).

  • Remove the Publisher class.

  • Rename the Book class to Books.

  • Change the Books class to use session, schema, and collection object instances.

  • Remove the make_authors_json() function.

  • Remove the Library class moving the utility functions to the Books class.

  • The database name changes from library_v2 to library_v3.

To create the file, you can simply copy the file from version2/database/library_v2.py to version3/database/library_v3.py.

Code Deleted

Begin by deleting the Publishers class and all the SQL statements. We won’t need those. You also will need to delete the Library class, but we will still use some of the methods in that class. See the section on the Books class for more details.

Setup and Initialization

Once you’ve deleted the SQL statements, we need to add an import statement to import the Python JSON encoder class. The following code should be placed at the end of the imports section.

from json import JSONEncoder as encoder

Books Class

This class is where the rest of the code changes appear. The following sections briefly describe each of the changes followed by a complete listing of the modified class. Note that the first thing we do is change the class name from Book to Books because we are modeling a collection of books rather than a single book.

Tip

This is one of the fundamental “think” changes one must make when working with collections. Although you can model a single document, most will gravitate naturally to modeling a collection of document (things).

Class Declaration

The following shows the modified class declaration. Note that we renamed the class and comments to reflect the changes to the class model. Note we also declare two more class variables in the constructor; book_schema and book_col. The class variable book_schema is used to store an instance of the schema object and the book_col variable is used to store an instance of the collection object. These will be initialized in the connect() function.

#
# Books collection simple abstraction (document store)
#
class Books(object):
    """Books class
    This class encapsulates the books collection permitting CRUD operations
    on the data.
    """
    def __init__(self):
        self.session = None
        self.book_schema = None
        self.book_col =  None
Create Function

Next, we change the create() function to work better with a collection. In this case, we change the parameter list to include the three pieces of data for the publisher; name, city, and URL. We also use a function we will write to make the JSON complex document. The new function is named make_book_json() and will be described later. You also can remove the make_authors_json() function. Finally, we change the code to use the book_col class variable to add the book to the collection followed by a lookup for the document id of the new book. Recall, the document id is assigned by the server so retrieving it this way allows us to use it to quickly locate the document. Listing 9-21 shows the modified create() statement with the changed areas in bold.

Note

The indentation for the functions in the library class should be indented 4 spaces. Spaces are omitted in the listings for readability.

def create(self, ISBN, Title, Pub_Year, Pub_Name, Pub_City, Pub_URL,
           Authors=[], Notes=[], Edition=1, Language="English"):
    assert ISBN, "You must supply an ISBN for a new book."
    assert Title, "You must supply Title for a new book."
    assert Pub_Year, "You must supply a Year for a new book."
    assert Pub_Name, "You must supply a Publisher Name for a new book."
    assert Authors, "You must supply at least one Author Name for a new book."
    last_id = None
    try:
        book_json = self.make_book_json(ISBN, Title, Pub_Year, Pub_Name,
                                        Pub_City, Pub_URL, Authors, Notes,
                                        Edition, Language)
        self.book_col.add(book_json).execute()
        last_id = self.book_col.find(
            "ISBN = '{0}'".format(ISBN)).execute().fetch_all()[0]["_id"]
    except Exception as err:
        print("ERROR: Cannot add book: {0}".format(err))
    return last_id
Listing 9-21

Create Function (Version 3)

Read Function

The read() function is greatly simplified now that we are using the collection object. Indeed, all we need to do is call the find() function passing in the document id and then fetch the document—all in one line of code! Auxiliary read functions from version 2 are removed, see the “Utility Functions” section later for more details.

def read(self, bookid=None):
    return self.book_col.find("_id = '{0}'".format(bookid)).execute().fetch_one()
Update Function

The update function is where a lot of code is changed. This is due to how we form the chain of modify() clauses for updating a collection. More specific, the code has been changed to detect when a data element is changed and if it has changed, call the modify().set().execute() chain to modify the data. Because we are doing more than one set of these changes potentially, we use the session class variable to start a transaction then if all statements succeed, commit it to the collection. If not, we rollback the changes.

The other changes have to do with how we handle the notes and author arrays. The publisher data is easy because we will place text boxes on the web page to hold the data. Listing 9-22 shows the modified create() function. Most of this code differs from version 2.

def update(self, book_id, book_data, ISBN, Title, Pub_Year, Pub_Name, Pub_City,
           Pub_URL, Authors=[], New_Note=None, Edition=1, Language="English"):
    assert book_id, "You must supply an book id to update the book."
    try:
        bkid = "_id = '{0}'".format(book_id)
        self.session.start_transaction()
        if ISBN != book_data["ISBN"]:
            self.book_col.modify(bkid).set("ISBN", ISBN).execute()
        if Title != book_data["Title"]:
            self.book_col.modify(bkid).set("Title", Title).execute()
        if Pub_Year != book_data["Pub_Year"]:
            self.book_col.modify(bkid).set("Pub_Year", Pub_Year).execute()
        if Pub_Name != book_data["Publisher"]["Name"]:
            self.book_col.modify(bkid).set("$.Publisher.Name", Pub_Name).execute()
        if Pub_City != book_data["Publisher"]["City"]:
            self.book_col.modify(bkid).set("$.Publisher.City", Pub_City).execute()
        if Pub_URL != book_data["Publisher"]["URL"]:
            self.book_col.modify(bkid).set("$.Publisher.URL", Pub_URL).execute()
        if Edition != book_data["Edition"]:
            self.book_col.modify(bkid).set("Edition", Edition).execute()
        if Language != book_data["Language"]:
            self.book_col.modify(bkid).set("Language", Language).execute()
        if New_Note:
            #
            # If this is the first note, we create the array otherwise,
            # we append to it.
            #
            if not "Notes" in book_data.keys():
                mod_book = self.book_col.modify(bkid)
                mod_book.set("Notes", [{"Text":New_Note}]).execute()
            else:
                mod_book =  self.book_col.modify(bkid)
                mod_book.array_append("Notes", {"Text":New_Note}).execute()
        if Authors and (Authors != self.make_authors_str(book_data['Authors'])):
            authors_json = self.make_authors_dict_list(Authors)
            self.book_col.modify(bkid).set("Authors", authors_json).execute()
        self.session.commit()
    except Exception as err:
        print("ERROR: Cannot update book: {0}".format(err))
        self.session.rollback()
Listing 9-22

Update Function (Version 3)

Take some time to read through this code to ensure you see how we’ve gone from simply updating the entire data row to checking to see what items need to change and to set those. The nature of the X DevAPI is such that it enables (and encourages) such behavior as we only want to change what has changed and nothing else to save processing time (and more).

Delete Function

The delete() function also undergoes some change, but much less than the create() function. As with the read() function, the X DevAPI makes it much easier for us to perform a delete operation. Instead of having to execute a series of deletes as we did in version 1 and 2, we only need to use the remove_one() convenience function of the collection to find the book by document id and delete it. Nice! The following shows the modified delete() function.

def delete(self, book_id):
    assert book_id, "You must supply a book id to delete the book."
    try:
        self.book_col.remove_one(book_id).execute()
    except Exception as err:
        print("ERROR: Cannot delete book: {0}".format(err))
        self.session.rollback()
Utility Functions
There are also a number of changes to the utility functions. The following summarizes the changes needed and later paragraphs provide more details on the change.
  • Remove functions no longer needed.

  • Move the connect() and get_books() functions from the old Library class to the Books.

  • Add new functions for working with JSON documents.

There are a lot of extra functions that are not needed for a pure document store code module. We deleted the make_authors_json(), read_notes(), add_note(), read_authors(), get_session(), is_connected(), disconnect(), make_rows(), and sql() functions from the library class as we do not need them anymore. Because the author and publisher data is part of the document, we treat the collection as an object rather than a gateway to a database server.

The connect() function requires some minor changes to allow us to work with the session object. Listing 9-23 shows the modified connect() function with the changes in bold. This function is moved to the Books class. Here, we attempt to get a session using the connection parameters passed (note there is no database parameter), then get the schema object for the library_v3 schema and finally get the collection object for the books collection.

def connect(self, username, passwd, host, port):
    config = {
        'user': username,
        'password': passwd,
        'host': host,
        'port': port,
    }
    try:
        self.session = mysqlx.get_session(**config)
        if self.session.is_open():
            self.book_schema = self.session.get_schema("library_v3")
            self.book_col = self.book_schema.get_collection("books")
    except Exception as err:
        print("CONNECTION ERROR:", err)
        self.session = None
        raise
Listing 9-23

Connect( ) Function (Version 3)

The get_books( ) function is simplified over version 2 because instead of issuing an SQL statement read a book, we use the collection object to find all of the books. We also use a rewrite of an older function to return an array of documents that we can use in Python. This new function is named make_row_array() and will be explained in the next sections.

def get_books(self):
    rows = []
    try:
        book_docs = self.book_col.find().sort("ISBN").execute().fetch_all();
        rows = self.make_row_array(book_docs)
    except Exception as err:
        print("ERROR: {0}".format(err))
        raise
    return rows
Finally, there are a number of new functions we need to add to make working with the JSON documents a bit easier. The following lists and summarizes the new functions. We leave the code for these to the next section where we list the complete code for the Books class. As you will see, there are no surprises in how these functions were coded.
  • make_authors_str(<array>): Given an array of authors, return a comma-separated list of first name last name.

  • make_authors_dict_list(<string>): Given a comma-separated list of author names, return a list (array) of dictionaries containing author first and last names.

  • make_book_json(<params>): Given a parameter list of the data from the fields on the web page, return a JSON document populated with the data.

  • make_row_array(<array or JSON documents>): Given an array of JSON documents, return an array of dictionaries containing a subset of the JSON document elements. Note that this is used to show the list of books in the collection.

Completed Code

Because there are a lot of changes necessary to this version of the database code and the Books class in particular, it is a good idea to see the completed code in its entirety. Listing 9-24 shows the complete code for the Books class. You can study this code to see how the many changes above were implemented.

class Books(object):
    """Books class
    This class encapsulates the books collection permitting CRUD operations
    on the data.
    """
    def __init__(self):
        self.session = None
        self.book_schema = None
        self.book_col =  None
    def create(self, ISBN, Title, Pub_Year, Pub_Name, Pub_City, Pub_URL,
               Authors=[], Notes=[], Edition=1, Language="English"):
        assert ISBN, "You must supply an ISBN for a new book."
        assert Title, "You must supply Title for a new book."
        assert Pub_Year, "You must supply a Year for a new book."
        assert Pub_Name, "You must supply a Publisher Name for a new book."
        assert Authors, "You must supply at least one Author Name for a new book."
        last_id = None
        try:
            book_json = self.make_book_json(ISBN, Title, Pub_Year, Pub_Name,
                                            Pub_City, Pub_URL, Authors, Notes,
                                            Edition, Language)
            self.book_col.add(book_json).execute()
            last_id = self.book_col.find(
                "ISBN = '{0}'".format(ISBN)).execute().fetch_all()[0]["_id"]
        except Exception as err:
            print("ERROR: Cannot add book: {0}".format(err))
        return last_id
    def read(self, bookid=None):
        return self.book_col.find("_id = '{0}'".format(bookid)).execute().fetch_one()
    def update(self, book_id, book_data, ISBN, Title, Pub_Year, Pub_Name, Pub_City,
               Pub_URL, Authors=[], New_Note=None, Edition=1, Language="English"):
        assert book_id, "You must supply an book id to update the book."
        try:
            bkid = "_id = '{0}'".format(book_id)
            self.session.start_transaction()
            if ISBN != book_data["ISBN"]:
                self.book_col.modify(bkid).set("ISBN", ISBN).execute()
            if Title != book_data["Title"]:
                self.book_col.modify(bkid).set("Title", Title).execute()
            if Pub_Year != book_data["Pub_Year"]:
                self.book_col.modify(bkid).set("Pub_Year", Pub_Year).execute()
            if Pub_Name != book_data["Publisher"]["Name"]:
                self.book_col.modify(bkid).set("$.Publisher.Name", Pub_Name).execute()
            if Pub_City != book_data["Publisher"]["City"]:
                self.book_col.modify(bkid).set("$.Publisher.City", Pub_City).execute()
            if Pub_URL != book_data["Publisher"]["URL"]:
                self.book_col.modify(bkid).set("$.Publisher.URL", Pub_URL).execute()
            if Edition != book_data["Edition"]:
                self.book_col.modify(bkid).set("Edition", Edition).execute()
            if Language != book_data["Language"]:
                self.book_col.modify(bkid).set("Language", Language).execute()
            if New_Note:
                #
                # If this is the first note, we create the array otherwise,
                # we append to it.
                #
                if not "Notes" in book_data.keys():
                    mod_book = self.book_col.modify(bkid)
                    mod_book.set("Notes", [{"Text":New_Note}]).execute()
                else:
                    mod_book =  self.book_col.modify(bkid)
                    mod_book.array_append("Notes", {"Text":New_Note}).execute()
            if Authors and (Authors != self.make_authors_str(book_data['Authors'])):
                authors_json = self.make_authors_dict_list(Authors)
                self.book_col.modify(bkid).set("Authors", authors_json).execute()
            self.session.commit()
        except Exception as err:
            print("ERROR: Cannot update book: {0}".format(err))
            self.session.rollback()
    def delete(self, book_id):
        assert book_id, "You must supply a book id to delete the book."
        try:
            self.book_col.remove_one(book_id).execute()
        except Exception as err:
            print("ERROR: Cannot delete book: {0}".format(err))
            self.session.rollback()
    #
    # Connect to a MySQL server at host, port
    #
    # Attempts to connect to the server as specified by the connection
    # parameters.
    #
    def connect(self, username, passwd, host, port):
        config = {
            'user': username,
            'password': passwd,
            'host': host,
            'port': port,
        }
        try:
            self.session = mysqlx.get_session(**config)
            if self.session.is_open():
                self.book_schema = self.session.get_schema("library_v3")
                self.book_col = self.book_schema.get_collection("books")
        except Exception as err:
            print("CONNECTION ERROR:", err)
            self.session = None
            raise
    def make_authors_str(self, authors):
        author_str = ""
        num = len(authors)
        i = 0
        while (i < num):
            author_str += "{0} {1}".format(authors[i]["LastName"],
                                           authors[i]["FirstName"])
            i += 1
            if (i < num):
                author_str += ", "
        return author_str
    def make_authors_dict_list(self, author_list=None):
        if not author_list:
            return None
        author_dict_list = []
        authors = author_list.split(",")
        for author in authors:
            try:
                last, first = author.strip(' ').split(' ')
            except Exception as err:
                last = author.strip(' ')
                first = ''
            author_dict_list.append({"LastName":last,"FirstName":first})
        return author_dict_list
    def make_book_json(self, ISBN, Title, Pub_Year, Pub_Name, Pub_City, Pub_URL,
                       Authors=[], Notes=[], Edition=1, Language="English"):
        notes_list = []
        for note in Notes:
            notes_list.append({"Text":"{0}".format(note)})
        book_dict = {
            "ISBN": ISBN,
            "Title": Title,
            "Pub_Year": Pub_Year,
            "Edition": Edition,
            "Language": Language,
            "Authors": self.make_authors_dict_list(Authors),
            "Publisher": {
                "Name": Pub_Name,
                "City": Pub_City,
                "URL": Pub_URL,
            },
            "Notes": notes_list,
        }
        return encoder().encode(book_dict)
    #
    #  Build row array
    #
    def make_row_array(self, book_doc_list):
        rows = []
        for book in book_doc_list:
            book_dict = book
            # Now, we build the row for the book list
            row_item = (
                book_dict["_id"],
                book_dict["ISBN"],
                book_dict["Title"],
                book_dict["Publisher"]["Name"],
                book_dict["Pub_Year"],
                self.make_authors_str(book_dict["Authors"]),
            )
            rows.append(row_item)
        return rows
    #
    # Get list of books
    #
    def get_books(self):
        rows = []
        try:
            book_docs = self.book_col.find().sort("ISBN").execute().fetch_all();
            rows = self.make_row_array(book_docs)
        except Exception as err:
            print("ERROR: {0}".format(err))
            raise
        return rows
Listing 9-24

Books Class (Version 3)

Wow, that was a lot of changes! This version shows how much different the code for working with collections is from even a hybrid solution. Now, let’s look at the changes to the application code.

Application Code

The changes to the application code for this version are not as long as the database code module changes. In essence, we remove the publisher list and detail view and convert the book view function to work the JSON document. There are a number of other small changes as well. The following summarizes the changes. Later sections describe the changes in more detail.
  • Import the Books class from the library_v3 module.

  • Switch the Library class to the Books class and call the connect() function.

  • Remove the NewSelectField() class.

  • Remove the PublisherForm() class.

  • Change the BookForm class to list the publisher data as fields.

  • Change the BookForm class to add hidden fields for the document id and JSON string.

  • Remove the publisher option from the ListForm template.

  • Remove the publisher view function.

  • Modify the book view function to work with the JSON document.

The following sections show the details of the three major areas for changes: setup and initialization, form class, and view functions. To create the file, you can simply copy the file from version1/database/library_v2.py to version2/database/library_v3.py.

Setup and Initialization

The changes to the setup and initialization sections are minor. We must import the Books class from the library_v3 code module and change the code to use the Books() object instead of the Library() object in version 2. The following shows the changes in bold.

from wtforms import (HiddenField, TextField, TextAreaField,
                     IntegerField, SubmitField)
from wtforms.validators import Required, Length
from database.library_v3 import Books
...
#
# Setup the books document store class
#
books = Books()
# Provide your user credentials here
books.connect(<user>, <password>, 'localhost', 33060)

Form Classes

The form classes changes are also minor. First, we delete the NewSelectField() and PublisherForm() classes as we don’t need them anymore. Second, we must modify the BookForm() form class to use text fields for the publisher data. Recall, this is name, city, and URL. We also want to add two hidden fields: one for the document id, and another for the JSON document.

The document id will be critical in making it easy to retrieve or update the JSON document and the JSON document stored in the form will allow us to detect when data has changed. Recall from the discussion of the Books class in the database code module we do exactly that in the update() function. Using hidden fields to contain data like this is common, but you should use the technique sparingly because data in hidden fields are like any other field—you must ensure you update the data in your code otherwise you could be working with stale data.

Listing 9-25 shows the updated BookForm for class with changes shown in bold.

class BookForm(FlaskForm):
    isbn = TextField('ISBN ', validators=[
            Required(message=REQUIRED.format("ISBN")),
            Length(min=1, max=32, message=RANGE.format("ISBN", 1, 32))
        ])
    title = TextField('Title ',
                      validators=[Required(message=REQUIRED.format("Title"))])
    year = IntegerField('Year ',
                        validators=[Required(message=REQUIRED.format("Year"))])
    edition = IntegerField('Edition ')
    language = TextField('Language ', validators=[
            Required(message=REQUIRED.format("Language")),
            Length(min=1, max=24, message=RANGE.format("Language", 1, 24))
        ])
    pub_name = TextField('Publisher Name', validators=[
            Required(message=REQUIRED.format("Name")),
            Length(min=1, max=128, message=RANGE.format("Name", 1, 128))
        ])
    pub_city = TextField('Publisher City', validators=[
            Required(message=REQUIRED.format("City")),
            Length(min=1, max=32, message=RANGE.format("City", 1, 32))
        ])
    pub_url = TextField('Publisher URL/Website')
    authors = TextField('Authors (comma separated by LastName FirstName)',
                        validators=[Required(message=REQUIRED.format("Author"))])
    create_button = SubmitField('Add')
    del_button = SubmitField('Delete')
    new_note = TextAreaField('Add Note')
    # Here, we book id for faster updates
    book_id = HiddenField("BookId")
    # Here, we store the book data structure (document)
    book_dict = HiddenField("BookData")
Listing 9-25

Book Form Class (Version 3)

Book View Function

The view functions area is where most of the changes to the application take place. This is because we must modify the function to use JSON documents (data). It is fortunate that JSON objects translate to code nicely allowing us to use path expressions in the form of array and dictionary key lookups. Cool! The following lists the changes needed to the book view function. Later paragraphs explain the changes in more detail.
  • Change publisher list to fields.

  • Remove populating the publisher select field.

  • Change the variable for the route from the ISBN to document id.

  • Use the books() instance instead of book().

  • When retrieving data from the collection, use the JSON document directly in Python accessing data items by array index and dictionary keys.

  • Detect when data elements are missing for optional fields.

  • Call the CRUD functions with the modified parameter lists adding the publisher fields.

As mentioned, the book view function requires modification to include publisher data that is now represented as fields, so we no longer have to populate a select field thereby simplifying the code a bit.

Because the data is now in JSON, we can use the document id as the key thereby eliminating the concern over users changing the primary key (e.g. the ISBN). In fact, using JSON documents allows users to change any field (or add new ones) without creating problems with keys and indexes. Neat!

When retrieving information from the books collection, we have a JSON document that we can access the data in Python as if it were a big dictionary. For example, we can access data items by name like data["ISBN"] where ISBN is the key in the dictionary, data. Nice! We see these changes in the section after the if id_selected: conditional. For those fields that are optional, we can check the dictionary (JSON object) to see if the key exists and if it does, retrieve the data.

We also see where we’ve added assignments to save the document id and the original JSON document to the hidden fields. Finally, we must also make a small change to how we call the CRUD functions as we have the extra parameters for the publisher data. Listing 9-26 shows the complete, modified code for the book view function with the modified sections in bold.

def book(id_selected=None):
    notes = []
    form = BookForm()
    # Get data from the form if present
    bookid = form.book_id.data
    isbn = form.isbn.data
    title = form.title.data
    year = form.year.data
    author_list = form.authors.data
    pub_name = form.pub_name.data
    pub_city = form.pub_city.data
    pub_url = form.pub_url.data
    edition = form.edition.data
    language = form.language.data
    new_note = form.new_note.data
    # If the route with the variable is called, change the create button to update
    # then populate the form with the data from the row in the table. Otherwise,
    # remove the delete button because this will be a new data item.
    if id_selected:
        # Here, we get the data and populate the form
        data = books.read(id_selected)
        if data == []:
            flash("Book not found!")
        #
        # Here, we populate the data
        #
        form.book_dict.data = data
        form.book_id.data = data["_id"]
        form.isbn.data = data["ISBN"]
        form.title.data = data["Title"]
        form.year.data = data["Pub_Year"]
        #
        # Since edition is optional, we must check for it first.
        #
        if "Edition" in data.keys():
            form.edition.data = data["Edition"]
        else:
            form.edition.data = '1'
        form.pub_name.data = data["Publisher"]["Name"]
        #
        # Since publisher city is optional, we must check for it first.
        #
        if "City" in data["Publisher"].keys():
            form.pub_city.data = data["Publisher"]["City"]
        else:
            form.pub_city = ""
        #
        # Since publisher URL is optional, we must check for it first.
        #
        if "URL" in data["Publisher"].keys():
            form.pub_url.data = data["Publisher"]["URL"]
        else:
            form.pub_url.data = ""
        #
        # Since language is optional, we must check for it first.
        #
        if "Language" in data.keys():
            form.language.data = data["Language"]
        else:
            form.language.data = "English"
        form.authors.data = books.make_authors_str(data["Authors"])
        # We also must retrieve the notes for the book.
        if "Notes" in data.keys():
            all_notes = data["Notes"]
        else:
            all_notes = []
        notes = []
        for note in all_notes:
            notes.append(note["Text"])
        form.create_button.label.text = "Update"
    else:
        del form.del_button
    if request.method == 'POST':
        # First, determine if we must create, update, or delete when form posts.
        operation = "Create"
        if form.create_button.data:
            if form.create_button.label.text == "Update":
                operation = "Update"
        if form.del_button and form.del_button.data:
            operation = "Delete"
        if form.validate_on_submit():
            # Get the data from the form here
            if operation == "Create":
                try:
                    books.create(ISBN=isbn, Title=title, Pub_Year=year,
                                 Pub_Name=pub_name, Pub_City=pub_city,
                                 Pub_URL=pub_url, Authors=author_list,
                                 Notes=notes, Edition=edition,
                                 Language=language)
                    flash("Added.")
                    return redirect('/list/book')
                except Exception as err:
                    flash(err)
            elif operation == "Update":
                try:
                    books.update(id_selected, form.book_dict.data, ISBN=isbn,
                                 Title=title, Pub_Year=year, Pub_Name=pub_name,
                                 Pub_City=pub_city, Pub_URL=pub_url,
                                 Authors=author_list, Edition=edition,
                                 Language=language, New_Note=new_note)
                    flash("Updated.")
                    return redirect('/list/book')
                except Exception as err:
                    flash(err)
            else:
                try:
                    books.delete(form.book_id.data)
                    flash("Deleted.")
                    return redirect('/list/book')
                except Exception as err:
                    flash(err)
        else:
            flash_errors(form)
    return render_template("book.html", form=form, notes=notes,
                           authors=author_list)
Listing 9-26

Book View Function (Version 3)

Finally, we can remove the publisher() view function and the publisher section in the simple_list() view as we don’t need those either.

Templates

The changes to the template files are minor. If you haven’t already done so, copy the templates from version 2 to version 3. For example, copy all the files from version2/templates/* to version3/templates. Once copied, you can remove the publisher.html template as it is no longer needed.

We also need to make two small changes to the base.html file to change the version number and remove the publisher list from the navigation bar. Listing 9-27 shows an excerpt from the base.html file with the changes shown in bold.

<div class="navbar navbar-inverse" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">MyLibrary v3</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a href="/list/book">Books</a></li>
            </ul>
        </div>
    </div>
</div>
Listing 9-27

Base Template (Version 3)

We also must make small changes to the book.html template to show a text field for the comma-separated list of authors. Listing 9-28 shows an excerpt of the modified template with the changes in bold.

...
{% block page_content %}
  <form method=post> {{ form.csrf_token }}
    <fieldset>
      <legend>Book - Detail</legend>
      {{ form.hidden_tag() }}
      <div style=font-size:20pz; font-weight:bold; margin-left:150px;>
        {{ form.isbn.label }} <br>
        {{ form.isbn(size=32) }} <br>
        {{ form.title.label }} <br>
        {{ form.title(size=100) }} <br>
        {{ form.year.label }} <br>
        {{ form.year(size=10) }} <br>
        {{ form.edition.label }} <br>
        {{ form.edition(size=10) }} <br>
        {{ form.language.label }} <br>
        {{ form.language(size=34) }} <br>
        {{ form.pub_name.label }} <br>
        {{ form.pub_name(style="width: 300px;") }} <br>
        {{ form.pub_city.label }} <br>
        {{ form.pub_city(style="width: 300px;") }} <br>
        {{ form.pub_url.label }} <br>
        {{ form.pub_url(style="width: 300px;") }} <br><br>
        {{ form.authors.label }} <br>
        {{ form.authors(size=100) }} <br>
...
Listing 9-28

Book Template (Version 3)

Ok, that’s it for the changes; now let’s see the code in action.

Executing the Code

Now that we’ve got the code written, let’s give it a test drive. To execute the application, you can launch it with the Python interpreter specifying the runserver command. The following shows an example of executing the application. Note that we used the port option to specify the port. You should enter this command from the version3 folder.

$ cd version3
$ python ./mylibrary_v3.py runserver -p 5003
 * Running on http://127.0.0.1:5003/ (Press CTRL+C to quit)
The application will launch and run but there isn’t any data in the database yet. You should start by grabbing a couple of your favorite books and enter the book data. Don’t worry about the notes just yet. Once you’ve added a few books, you should see them in the default view (by clicking on MyLibrary v3 or Books in the navigation bar. Figure 9-7 shows an example of what you should see.
../images/432285_1_En_9_Chapter/432285_1_En_9_Fig7_HTML.jpg
Figure 9-7

Library application book list (version 3)

If this is starting to look a little familiar, you’re right, it is. This version implements the same interface except without the publisher and author views. As in the other versions, you can click on the Modify link for any book and see the book details. Figure 9-8 shows the updated book detail view. Note that the publisher entry is now a set of three text fields instead of a dropdown list. The author list is unchanged from version 2.
../images/432285_1_En_9_Chapter/432285_1_En_9_Fig8_HTML.jpg
Figure 9-8

Book detailed view (version 3)

Now, let’s take a moment to discuss some observations about this version of the application.

Observations

The following are some observations about this version of the application. Some are consequences of the database design, others are from the code, and others are things we made a bit better. The observations are presented in the form of an unordered list. This list is shorter than the previous versions because we have achieved a better application! So, this is a list of successes rather than improvements.
  • Database code much shorter: We need less code for working with collections and documents using the X DevAPI.

  • Database code easier to understand: Working with JSON documents are a natural extension of Python (and other languages).

  • Application code significantly shorter: We need less code for the application because we simplified the user interface. In fact, the code is almost 50% of the size of version 1.

There is one other observation that bears discussing. The changes to the user experience using the three versions of the application are minor. In fact, one of the goals was to keep the user interface changes to a minimum to demonstrate that migrating from a traditional relational database model to a hybrid and ultimately a pure document store model does not mean one must redesign the entire user interface!

Although there may be some changes necessary to facilitate changes in how the data is stored and retrieved—like what we saw with the authors and books_authors join tables, the changes often help solve problems with the database design or in this case help eliminate a false premise that the specific author and publisher data is meaningful outside the context of a book. In this example application, it was not. So, designing separate tables (or documents) for storing the information wasn’t necessary and added complexity we didn’t need. Such are the challenges and rewards of a designing your data around JSON documents and the MySQL document store engine.

Challenges

The application in either version is very basic in functionality. If you find the application is a good fit for further experimentation or even to base another effort, there are a few areas where you may want to consider improving the design and code. The following includes a brief list of things that can be improved.
  • ISBN lookup service: Add the ability to retrieve information about books using an ISBN lookup service such as isbntools ( http://isbntools.readthedocs.io/en/latest/ ) or SearchUPC ( http://www.searchupc.com ). Some services require creating accounts while others may be fee-based services.

  • Separate library modules: Break the library file into separate code modules (book, author, publisher, library) for version 1 and 2.

  • Separate code modules: Larger Flask applications typically break out the views (form classes) in the main code file into separate code modules.

  • Remove hardcoded values: Make the user, password, host, and port data for the MySQL parameters rather than hard coded values (hint: use argparse).

  • Expand the data: Modify the database or document store to store additional media such as magazines, articles, and online references.

Summary

There is no doubt that the new MySQL 8 release is set to be the biggest, most significant release in MySQL history. The addition of the JSON data type and the X DevAPI is simply groundbreaking for MySQL applications.

In this chapter, we explored the differences between a relational database solution and a relational database solution augmented with JSON fields, and finally a pure document store solution. As we discovered, the new X DevAPI makes developing MySQL solutions easier, faster, and with less code than a relational database solution. This gives us many reasons to start adopting the document store going forward.

In Chapter 10, I conclude my exploration of the MySQL 8 Document Store with a look at how you can prepare your existing and future application plans to incorporate the document store. This includes notes about upgrading to MySQL 8 and tips for how to migrate to document store solutions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset