Client Server Architecture Overview
As an example, from the PCDE course, we often connect to a SQL server via some client. Often it was using MySQL Workbench a GUI desktop application. This may be a local database server & workbench client. However, this was still a server-client interface going on.
This common interface allows having servers located anywhere in the world, while still being able to connect with any client in the world so long as each client is using a compatible interface. In the SQL world, this is using the built-in SQL HTTP interface. MySQL actually comes with its own command line interface that gives you a terminal client to connect to the database.
What Is a Server? What Is a Client?
Within a network, a computer machine that shares resources, such as memory, hard drives, and printers, with other computers is called a server. servers also provide access to data or particular services, such as large-scale computations. Within the same networks, any other computer that is not a server is called a client. Clients are generally capable of receiving information or using a particular service from the server.
Therefore, servers are usually more powerful and expensive machines because they share their resources with other computers in the network. Clients are less powerful machines, and you can think of them as personal computers that users operate to connect to a server. The clients’ machines don’t need to be extremely powerful because, generally, they don’t need to share resources with other computers.
What Is a Client-Server Interface?
As you know, a user can perform data analysis on a database using SQL locally. This means that the database is stored on the user machine and all the operations happen on the user’s computer. However, SQL works particularly well on a client-server system. In this case, users on different clients’ machines can access a database that is stored on a server machine via wired communications (i.e., LAN) or online channels.
In this specific case, the computer you are using to manipulate a database and where you have installed MySQL Workbench is the client and, so far, you have only performed operations locally.
You learned how to extend the applications of SQL by using a different type of client by using a Python driver that acts on the database.
What Is a Driver?
Let’s try to understand what a driver is with an example.
You have probably heard of the word driver when installing a printer on your machine. Imagine you are working in an office where multiple printers are connected to a network and you want to print a certain document from a specific printer.
It would be convenient to simply say "print this document" but, of course, because each printer is unique, the signal cannot be exactly the same for all the printers in your office. In fact, as you probably know, if you don’t specify which printer you want to connect to, you won’t be able to print anything.
Here’s where drivers become useful. Rather than having to send a different signal to each type of printer (and hoping that one works for you), it is more convenient to locally install a driver for a particular printer that translates the "print this document" command into something more specific that the desired printer hardware needs to understand.
In other words, a driver is a piece of software related to hardware that contains a mapping/translation so that an operating system can communicate with hardware. It's a piece of code that sits between a program and hardware.
In conclusion, both servers and clients can use drivers for communication whenever it is necessary.
Connecting to a Database using a Driver
Using the mysql.connector
module for python,
we'll start connecting to a database using python.
First we do this using a connection string. A connection string is a string that provides information about a data server and how the connection to it is made.
cnx = mysql.connector.connect(user='root',
password='MyNewPass',
host='127.0.0.1',
database='education',
auth_plugin='mysql_native_password')
As you see here we have user
, password
, host
, database
, and
an authentication type we want to use.
This is the connection string we want to use to connect to a database.
Note the education
database is the same database we've used before.
Reading a Table Using a Driver
Now let's run a simple SELECT * FROM colleges
query.
cursor = cnx.cursor()
query = ('SELECT * FROM colleges')
cursor.execute(query)
This uses a cursor
class, which allows code to execute SQL commands in
a database session.
Most database drivers for most languages will have this notion of a cursor.
We simply define the SQL query for the cursor
object,
using a SQL query string,
then cursor.execute
that query to get the results.
Now to print out those results:
#print all the rows
for row in cursor.fetchall():
print(row)
cursor.close()
cnx.close()
The cursor
object has a fetchall
function to
provide an iterator for each resulting records' row.
Simply iterate the fetchall
row return then print the result.
Note that you also want to remember to close
both the
cursor
& cnx
objects when you're done with them.
The results from this code should look something like this:
1, 'MIT', 11, 'Cambridge', 'MA', 'USA')
(2, 'Brown', 9, 'Providence', 'RI', 'USA')
(3, 'Dartmouth', 6, 'Hanover', 'NH', 'USA')
(4, 'Stanford', 17, 'Stanford', 'CA', 'USA')
(5, 'Yale', 12, 'New Haven', 'CT', 'USA')
(6, 'Columbia', 31, 'New York', 'NY', 'USA')
(7, 'Harvard', 23, 'Cambridge', 'MA', 'USA')
(8, 'Princeton', 9, 'Princeton', 'NJ', 'USA')
(9, 'Johns Hopkins', 24, 'Baltimore', 'MD', 'USA')
(10, 'Northwestern', 21, 'Evanston', 'IL', 'USA')
(11, 'Duke', 15, 'Durham', 'NC', 'USA')
(12, 'Cornell', 22, 'Ithaca', 'NY', 'USA')
(13, 'Notre Dame', 9, 'Notre Dame', 'IN', 'USA')
(14, 'UCLA', 32, 'Los Angeles', 'CA', 'USA')
(15, 'Berkeley', 42, 'Berkeley', 'CA', 'USA')
(16, 'Georgetown', 5, 'Washington', 'DC', 'USA')
(17, 'Michigan', 45, 'Ann Arbor', 'MI', 'USA')
(18, 'USC', 44, 'Los Angeles', 'CA', 'USA')
(19, 'Tufts', 11, 'Medford', 'MA', 'USA')
(20, 'NYU', 51, 'New York', 'NY', 'USA')
Writing Data Using a Driver
Before we read data from the database using a driver. Now let's write to it. As before use the connection string to connect to the database.
cnx = mysql.connector.connect(user='root',
password='some_password',
host='127.0.0.1',
database='education',
auth_plugin='mysql_native_password')
Then use the python global function input()
to
allow manual entry of new College
entities into the table from the terminal.
We will use that information to build the cursor
that will
send the data to the database.
college = input('Enter the college name: ')
students = input('Enter the student population: ')
cursor = cnx.cursor()
query = (f'INSERT INTO `Colleges` VALUES(NULL, "{college}, {students});')
cursor.execute(query)
This will enter the new data you specify into the database,
with NULLs for the properties not included.
Then it will execute the INSERT INTO
mutator query to
add this value into the Colleges
table.
Now let's verify it worked by reusing the code from before to
Read all the records from that table and close the connection.
#print all the rows
for row in cursor.fetchall():
print(row)
cursor.close()
cnx.close()
Connecting to a Database Using Secrets File in YAML
When connecting to a database or any server really, usually some kind of authentication is required. Before we would usually do this by putting the password into a python file as plaintext. This is really bad security practice. Better to have a credentials file that's ignored by git and other programs. Then reading into the client program that credentials file to connect. Let's do this with a YAML file.
host: 'localhost'
user: 'root'
passwd: 'mYpAsSw0rD'
db: 'education'
Then you can go into a python client program and use the yaml
module.
import yaml
import mysql.connector
db = yaml.safe_load(open('secrets.yml'))
config = {
'user': db['user'],
'password': db['passwd'],
'host': db['host'],
'database': db['db'],
'auth_plugin': 'mysql_native_password',
}
cnx = mysql.connector.connect(**config)
This will now use the connection string created from the secrets.yml
file
created above.
The config
object you'll note has all of
the named parameters connect()
expects exactly as they are named.
This means that when connect()
is called with config
,
using the destructuring dictionary keywords operator **config
then
will replace all of the named parameters of
the function with the config
object passed in.
You'll also note that yaml.safe_load()
is used to read the secrets.yml
file.
Then using the db['yaml_key']
syntax is used to assign config
values.