2024-11-07 Building a CRUD Webapp with PostgresSQL and MySQL

In the CRUD webapp example I built previously, SQLAlchemy is used to interact with a SQLite database. Here I want to see how to make the app work with both PostgreSQL and MySQL without too much trouble.

PostgreSQL

Searching the web using "SQLAlchemy" and "PostgreSQL", we will find a lot of information. Since I want to have a quick testing with the database, I'll run a PostgreSQL database as a docker container.

$ docker run --name noter_psdb -e POSTGRES_PASSWORD=mypass -p 5432:5432 -d postgres

Unable to find image 'postgres:latest' locally
latest: Pulling from library/postgres
...

$ docker ps
CONTAINER ID   IMAGE                             COMMAND                  CREATED         STATUS                PORTS                                                                                  NAMES
15af98338033   postgres                          "docker-entrypoint.s…"   7 seconds ago   Up 4 seconds          0.0.0.0:5432->5432/tcp, :::5432->5432/tcp  

We named the container noter_psdb, so let's shell into the container:

$ docker exec -it noter_psdb bash
root@15af98338033:/# 

Then we can use psql to interact with the database:

root@15af98338033:/# su - postgres
postgres@15af98338033:~$ psql
psql (17.0 (Debian 17.0-1.pgdg120+1))
Type "help" for help.

postgres=# \conninfo
You are connected to database "postgres" as user "postgres" via socket in "/var/run/postgresql" at port "5432".
postgres=# \q
root@15af98338033:/#
^D

Based on the information so far, we can proceed by updating our existing program accordingly. For example, our app.py is changed to:

...
app = Flask(__name__)
app.config['SECRET_KEY'] = '123454321'
app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://postgresql:mypass@192.168.x.x/noterdb"

db.init_app(app)
...

The format for the connection string is based on SQLAlchemy's official site. The code above indicates that the docker host is at 192.168.x.x`, and we will connect to the PostgreSQL server using default port 5432, with the database instance noterdb.

If we attempt to initialize the database tables like before using

python app.py

there will be some errors to fix. First we need to install psycopg2 :

pip install psycopg2

We also need to create a new database noterdb in PostgreSQL:

$ docker exec -it noter_psdb bash
root@15af98338033:/# su - postgres
postgres@15af98338033:~$ createdb noterdb
postgres@15af98338033:~$ ^D

Now we can run python app.py again, and launch the server:

python app.py
flask run --debug

See if you can create new notes now:

MySQL

We may guess that in this case we can simply change the connection string to mysql://root:mypass@192.168.x.x/noterdb") in app.py and follow a similar procedure. That's right. First create a MySQL container:

docker run --name noter_mydb -e MYSQL_ROOT_PASSWORD=mypass -p 3306:3306 -d mysql

We can shell into the container too:

$ docker exec -it noter_mydb bash
bash-5.1# mysql -h localhost -u root -p
Enter password: ******
Welcome to the MySQL monitor.  Commands end with ; or \g.
...

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> ^D

Again change app.py to:

...
app = Flask(__name__)
app.config['SECRET_KEY'] = '123454321'
app.config["SQLALCHEMY_DATABASE_URI"] = "mysql://root:mypass@192.168.x.x/noterdb"

db.init_app(app)
...

Of course trying to initialize the database by running python app.py at this stage will result in errors. So let's fix them. Since my local machine is a Mac, according to here I need to:

brew install mysql-client pkg-config
export PKG_CONFIG_PATH="$(brew --prefix)/opt/mysql-client/lib/pkgconfig"
pip install mysqlclient

You need to follow the instructions based on your OS.

To create the noterdb database in MySQL, shell into the container:

$ docker exec -it noter_mydb bash
bash-5.1# mysql -h localhost -u root -p
...
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> CREATE DATABASE noterdb;
Query OK, 1 row affected (0.16 sec)

mysql> ^D

But when running python app.py, we get one last error:

...
raise exc.CompileError(
sqlalchemy.exc.CompileError: (in table 'note', column 'text'): VARCHAR requires a length on dialect mysql

It seems in our code it is not a good idea to use String() for text with unknown size. So I change the models.py file to use Text instead :

from flask_sqlalchemy import SQLAlchemy

from sqlalchemy import String, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

class Base(DeclarativeBase):
  pass

db = SQLAlchemy(model_class=Base)

class Note(db.Model):
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(100))
    text: Mapped[str] = mapped_column(Text)

With the change, the app works again.

Remarks

Note that my goal is to quickly check if it is straightforward to adjust the webapp for different databases using SQLAlchemy. It is confirmed. But in real-world situation when dealing with databases one should be careful about how to setup the user accounts, passwords, among other things. Furthermore, it is certain that the data models will continue to be modified during development (or between different releases), and the existing data in the database need to be "migrated" carefully. This is an important topic when working with databases.