Interfacing with MySQL¶
We consider a graphical user interface to navigate the records in the database and then to modify the database.
A GUI to browse a MySQL table¶
Consider the database of Python scripts
of the previous lecture.
We made one table scripts
in the database
OurPyFiles
, storing data about our Python scripts.
For every Python script we have 4 fields:
its type, number, date, and file name.
An example of a header is below.
# L-26 MCS 275 Fri 10 Mar 2017 : guidb1.py
The corresponding data tuple is
('L', '26', '2017-03-10', 'guidb1.py')
The goal of the GUI is to get a better overview of the records in the table. We will sort the records in various ways.
The GUI will have the following widgets:
- A message field to write status information.
- Buttons to
- connect to the database,
- retrieve all records,
- see next 10 or previous 10 records.
- Radio buttons to determine
- which field to use for sorting,
- to sort in ascending or descending order.
- Listboxes to display the columns in the table.
The layout of the GUI is displayed in Fig. 73.
The code in __init__
to define the message label is
self.message = StringVar()
self.message.set("welcome to our database")
self.messageLabel = Label(wdw, \
textvariable = self.message)
self.messageLabel.grid(row=0, column=0, columnspan=4)
The labels for the three fields in the table are defined as
self.tt = Label(wdw, text='type')
self.tt.grid(row=2, column=0)
self.dd = Label(wdw, text='date')
self.dd.grid(row=2, column=1)
self.ff = Label(wdw, text='file')
self.ff.grid(row=2, column=2, columnspan=2)
And then we have the labels to sort:
self.sL = Label(wdw, text='sort by')
self.sL.grid(row=4, column=0)
The constructor contains a button to connect to the database:
self.bc = Button(wdw, text='connect', \
command=self.connect)
self.bc.grid(row=1, column=0)
self.cursor = 0
and the callback function associated to the button is
defined by the method connect
defined below.
def connect(self):
"""
Connects to the database OurPyFiles.
"""
try:
# db = MySQLdb.connect(db="OurPyFiles")
db = pymysql.connect(db="OurPyFiles")
self.message.set("connected to \"OurPyFiles\"")
self.cursor = db.cursor()
except:
self.message.set("failed to connect to \"OurPyFiles\"")
We can sort on type
, date
, or file
.
Which sort is selected by a Radiobutton,
stored in the attributed RadioSort
of the constructor.
self.RadioSort = IntVar()
self.st = Radiobutton(wdw, text='type', \
variable=self.RadioSort, value = 1)
self.st.grid(row=4, column=1)
self.sd = Radiobutton(wdw, text='date', \
variable=self.RadioSort, value = 2)
self.sd.grid(row=4, column=2)
self.sf = Radiobutton(wdw, text='file', \
variable=self.RadioSort, value = 3)
self.sf.grid(row=4, column=3)
self.RadioSort.set(1)
The default sort is set to type
.
The user has choice between ascending or descending order,
which is defined by another Radiobutton,
with code in the __init__
:
self.RadioOrder = IntVar()
self.asc = Radiobutton(wdw, text='ascending order', \
variable = self.RadioOrder, value = 1)
self.asc.grid(row=5, column=0, columnspan=2)
self.dsc = Radiobutton(wdw, text='descending order', \
variable = self.RadioOrder, value = 2)
self.dsc.grid(row=5, column=2, columnspan=2)
self.RadioOrder.set(1)
The default order is set to ascending
.
Depending on the choices selected by the user,
the formulation of the query is defined in the method query
.
def query(self):
"""
Returns the query to select all records,
taking input from the radio buttons.
"""
q = 'select * from scripts'
ord = ''
if self.RadioOrder.get() == 1:
ord = 'asc'
elif self.RadioOrder.get() == 2:
ord = 'desc'
if self.RadioSort.get() == 1:
q = q + ' order by t, n ' + ord
elif self.RadioSort.get() == 2:
q = q + ' order by d ' + ord
elif self.RadioSort.get() == 2:
q = q + ' order by f ' + ord
return q
The callback command for the second button,
to retrieve records is defined in the method retrieve
.
def retrieve(self):
"""
Retrieves all records from the scripts table.
"""
if self.cursor == 0:
self.message.set("please connect first")
else:
q = self.query()
lc = self.cursor.execute(q)
m = 'retrieved %d records' % int(lc)
self.message.set(m)
R = self.cursor.fetchall()
self.clear()
for i in range(len(R)):
if i >= 10:
break
self.insert(R[i])
self.records = R
self.pos = 10
All records in the table are stored in the data attribute records
.
The self.pos
holds the current position in the of records.
To display the data, we use Listbox
widgets.
We will use three listboxes (defined in __init__
):
for type and number, e.g.:
L-26
self.Lt = Listbox(wdw, width=4, height=10) self.Lt.grid(row=3, column=0)
for the date, e.g.:
2017-03-10
self.Ld = Listbox(wdw, width=10, height=10) self.Ld.grid(row=3, column=1)
for the file name, e.g.:
guidb1.py
self.Ln = Listbox(wdw, width=15, height=10) self.Ln.grid(row=3, column=2, columnspan=2)
The basic methods to use the listboxes are defined below.
def clear(self):
"""
Clears all listboxes.
"""
self.Lt.delete(0, END)
self.Ld.delete(0, END)
self.Ln.delete(0, END)
def insert(self, item):
"""
Inserts one record item to the listboxes.
"""
t = item[0] + '-' + str(int(item[1]))
self.Lt.insert(END, t)
self.Ld.insert(END, str(item[2]))
self.Ln.insert(END, item[3])
Because our listboxes are only 10 in height,
we cannot display everything all at once.
Two navigation buttons are defined in __init__
:
to show the next 10 records:
self.bn = Button(wdw, text='next 10', command=self.next10) self.bn.grid(row=1, column=2)
to show the previous 10 records:
self.bp = Button(wdw, text='previous 10', command=self.prev10) self.bp.grid(row=1, column=3)
Storing the current position in the retrieved records
is done in the data attribute pos
initialized to zero.
self.pos = 0
The display of the next 10 records is defined by
the method next10
listed below.
def next10(self):
"""
Displays the next 10 records in listboxes.
"""
if self.records == 0:
self.message.set("no records to show")
else:
self.clear()
for i in range(self.pos, self.pos+10):
if i >= len(self.records):
break
self.insert(self.records[i])
self.pos = self.pos + 10
if self.pos >= len(self.records):
self.pos = len(self.records) - 1
Displaying the previous 10 records is similar,
but notice the difference in adjusting the
data attributed pos
to set the current position
in the list of records.
def prev10(self):
"""
Displays the previous 10 records in listboxes.
"""
if self.records == 0:
self.message.set("no records to show")
else:
self.clear()
self.pos = self.pos - 20
if self.pos < 0:
self.pos = 0
for i in range(self.pos, self.pos+10):
if i >= len(self.records):
break
self.insert(self.records[i])
self.pos = self.pos + 10
Normalization¶
To make the database more useful, we have to split
the table scripts
in two.
The four fields were copied from the headers. There are two drawbacks to working with one table:
- only the file name is unique for every record and not so convenient to use as key; and
- as most lectures featured more than one script, there is a lot of redundant data in the table.
We normalize, using scripts to create two new tables: one with file names and one with type and dates. Because it is tedious to do manually with the mysql monitor, we develop a script for the normalization.
The top down design is realized by main()
,
defined below.
def main():
"""
Splits scripts in two tables.
"""
ourdb = pymysql.connect(db="OurPyFiles")
crs = ourdb.cursor()
qry = 'select * from scripts order by d'
nbr = crs.execute(qry)
print('found %d records' % int(nbr))
ans = input('Make the tables ? (y/n) ')
if ans == 'y':
make_tables(crs)
data = crs.fetchall()
(types, files) = split_records(data)
print('T = ', types)
print('F = ', files)
if ans == 'y':
ans = input('Do the inserts ? (y/n) ')
insert_type_date(crs, types, ans=='y')
insert_file_data(crs, files, ans=='y')
date_files(crs)
ourdb.commit()
else:
print('showing the insert queries')
insert_type_date(crs, types)
insert_file_data(crs, files)
The making the tables is defined by make_tables
below.
def make_tables(crs):
"""
Executes the MySQL commands to create
the tables typedate and filedata.
The input parameter crs is the cursor.
"""
make_type_date(crs)
make_file_data(crs)
The structure of the table typedate
is encoded
in the function below.
def make_type_date(crs):
"""
The table typedate stores type and date,
e.g.: L-26 and 2017-03-10, repesented by
four fields: id, type, number, and date.
The input parameter crs is the cursor.
"""
try:
qry = 'create table typedate ' + \
'( i INT, t CHAR(1), n INT, d DATE )'
crs.execute(qry)
print(qry + ' succeeded')
except:
print(qry + ' went wrong...')
The structure of the table filedata
is defined
in the function below.
def make_file_data(crs):
"""
The table filedata stores file names,
represented by three fields: id, name,
and id of the corresponding entry in
the typedate table, crs is the cursor.
"""
try:
qry = 'create table filedata ' + \
'( i INT, f CHAR(20), t INT )'
crs.execute(qry)
print(qry + ' succeeded')
except:
print(qry + ' went wrong...')
In moving the data into the tables,
we first split the records with a Python script.
The relevant function is split_records()
,
listed below.
def split_records(R):
"""
Returns two lists: records that go
in typedate and in filedata.
"""
L = []; T = []; F = []; cnt = -1
for i in range(len(R)):
d = R[i]
p = (d[0], d[1], d[2])
if p in L:
ind = L.index(p)
else:
cnt = cnt + 1
L.append((d[0], d[1], d[2]))
T.append((cnt, d[0], d[1], d[2]))
ind = cnt
F.append((i, d[3], ind))
return (T, F)
The query to insert the type and date
in the table typedate
is encoded
in the function listed below.
def insert_type_date(crs, T, doit=False):
"""
Given the cursor and a list of values for
the table typedate, all records are added.
"""
for i in range(len(T)):
p = T[i]
qry = 'insert into typedate values ' + \
'(\"%s\", \"%s\", \"%s\", \"%s\") ' \
% (str(p[0]), str(p[1]), str(p[2]), \
str(p[3]))
if doit:
crs.execute(qry)
else:
print('query = ', qry)
The query to insert the names of the files
into the table filedata
is encoded in
the function listed below.
def insert_file_data(crs, F, doit=False):
"""
Given the cursor and a list of values for
the table filedata, all records are added.
"""
for i in range(len(F)):
n = F[i]
qry = 'insert into filedata values ' + \
'(\"%s\", \"%s\", \"%s\") ' \
% (str(n[0]), n[1], str(n[2]))
if doit:
crs.execute(qry)
else:
print('query = ', qry)
The check to see whether we have not lost any data
is to run a query which show all original records
in the table scripts
, but now using the tables
filedata
and typedate
.
This check is defined in the function below.
def date_files(crs):
"""
As a final check, selects file name
and corresonding date from the newly
created tables, crs is the cursor.
"""
qry = 'select f,d from filedata, typedate' \
+ ' where filedata.t = typedate.i'
crs.execute(qry)
res = crs.fetchall()
print(res)
print('#records :', len(res))
Retrieving Specific Records¶
After the normalization, every script has a unique key.
This key is in the i
field of the table filedata
.
Accessing the data via the key allows us to
select records directly; and to
insert, delete, and update records.
The query to retrieve a specific record is encoded in the following function.
def query(self, key):
"""
Returns the query for all the information
of the script with the given key.
"""
q = 'select typedate.t, n, d, f ' + \
'from typedate, filedata ' + \
'where filedata.t = typedate.i ' + \
'and filedata.i = %d' % key
return q
To insert a new record in the table, we have to assign a key. Therefore we count the number of files we have already stored in the table. The query for this count is encapsulated in the function below.
def count_files(self):
"""
Returns the number of files.
"""
qry = 'select count(*) from filedata'
ret = self.cursor.execute(qry)
nbr = self.cursor.fetchone()
return int(nbr[0])
The GUI to enter the data for a new record is illustrated in Fig. 75.
With the message label we confirm the data and then ask for the user to confirm, as shown in Fig. 76.
Then with a message label, the insertion is confirmed, as shown in Fig. 77.
The insertion of the data for a new record in the database is initiated by the method below.
def insert_data(self):
"""
Inserts a new record into the database.
"""
t = self.tp.get()
d = self.dt.get()
f = self.nm.get()
r = (t, d, f)
if not self.startinsert:
m = 'inserting %s,' % str(r)
m = m + 'press to confirm'
self.message.set(m)
self.startinsert = True
else:
self.insert_record(r)
m = 'inserted %s' % str(r)
self.startinsert = False
The method insert_record
, used above
in the method insert_data
, is defined below.
def insert_record(self, data):
"""
Prepares data to insert a record.
"""
L = data[0].split('-')
(t, n) = (L[0], L[1])
(d, f) = (data[1], data[2])
nf = self.count_files()
nt = self.count_types()
v = '(' + '\"' + str(nf) + '\"' + \
',' + '\"' + f + '\"' + ',' + \
'\"' + str(nt) + '\"' + ')'
self.insert_file_values(v)
m = 'inserted %s' % v
self.message.set(m)
To insert the values, the MySQL command
insert into <table> values <tuple>
is applied in the method below.
def insert_file_values(self, vals):
"""
Inserts values in filedata table.
"""
qry = 'insert into filedata values '
qry = qry + vals
self.cursor.execute(qry)
Visit http://www.mysqltutorial.org/
for more about mysql.
Exercises¶
- Create the equivalent to
guidb1.py
, our first GUI, writing a CGI script. - The method in our second GUI to insert is not yet complete.
Also provide functions to add the corresponding values
in the table
typedate
. - Provide functions to delete records.
- Provide functions to update records.