Python SQL Alchemy and Working with existing database

Python SQL Alchemy and Working with existing database

Work with an existing database

Suppose we have built the following database and table,

from sqlalchemy import create_engine, MetaData, Table, Integer
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session

engine = create_engine("sqlite:///mydatabase.db")
metadata_obj = MetaData()

Table('user_order',
    metadata_obj,
    Column('id', Integer, primary_key=True)
     )

metadata_obj.create_all(engine)

engine.dispose()

Basic Use

The simplest usage is to autoload_with and engine.

engine = create_engine("sqlite:///mydatabase.db")
session = Session(engine)

Base = automap_base()
Base.prepare(autoload_with=engine)

Order = Base.classes.user_order
session.query(Order).all()

Through MetaData

Pass a pre-declared MetaData object to automap_base(), the metadata itself can be reflected from an engine, and/or defined by Table Objects Ourselves.

from sqlalchemy import create_engine, MetaData, Table, Integer, Column
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session

# this is an example of reflecting metadata from engine
engine = create_engine("sqlite:///mydatabase.db")

metadata_obj = MetaData()
metadata_obj.reflect(engine, only=['user_order'])

Base = automap_base(metadata=metadata_obj)
Base.prepare()

Order = Base.classes.user_order
session = Session(engine)

session.query(Order).all()
from sqlalchemy import create_engine, MetaData, Table, Integer, Column
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session

# this is an example of explicitly defining the table to metadata
engine = create_engine("sqlite:///mydatabase.db")

metadata_obj = MetaData()

Table('user_order',
    metadata_obj,
    Column('id', Integer, primary_key=True)
)

Base = automap_base(metadata=metadata_obj)
Base.prepare()

Order = Base.classes.user_order
session = Session(engine)

session.query(Order).all()

Explicitly Specify the Classes

The automap_base() is not necessary anymore in this use case, even declarative_base should work as well.

from sqlalchemy import create_engine, Integer, Column
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session

engine = create_engine("sqlite:///mydatabase.db")
session = Session(engine)

Base = automap_base()
class Order(Base):
    __tablename__ = 'user_order'
    id = Column(Integer, primary_key=True)

Base.prepare()

Common Failures

One failure happens when we try to build mapped classes from existing tables that DO NOT have PRIMARY KEY (or simply cannot be auto mapped). In this case, even though MetaData is able to reflect the table schema, Base cannot be prepared to build corresponding mapped classes because primary key is required.

This failure is very hard to be identified, because It seems there is even no error message.

To deal with this, we have to explicitly define the mapped class ourself (which is documented above), and make a column the primary key. While, this is too cumbersome if we are working with a large table.

Therefore, here we show how to leverage the auto reflected columns as much as possible, make copies and overwrite a column to be the primary key.

A table written by dataframe does not have a primary key, let’s use this as an example.

import pandas as pd
from sqlalchemy import create_engine, MetaData
from copy import deepcopy
engine = create_engine("sqlite:///mydatabase.db")

df = pd.DataFrame(
    {
        'id': [1, 2, 3, 4, 5],
        'name': ['alice', 'bob', 'carol', 'david', 'emily']
    }
)

df.to_sql('users', con=engine, if_exists='replace', index=False)

The reflection still works, but the mapped class does not show in Base.classes.

metadata_obj = MetaData()
metadata_obj.reflect(engine, only=['users'])

# the users table is already in the metadata
metadata_obj.tables

# build mapped classes from the metadata that are reflected from the engine
Base = automap_base(metadata=metadata_obj)
Base.prepare()
# Base.classes.users ! this will fail... users class is not yet ready

# Of course you can declare everthing from scratch and forget about the automap
Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String)

session = Session(engine)
session.query(User).all()

The approach we use is to take out the columns from the table, make id a primary key, then write the new table to the metadata_obj.

new_columns = []
reflected_table = metadata_obj.tables['users']
for old_column in reflected_table.columns.keys():
    # Column.copy() method is deprecated
    copy = deepcopy(reflected_table.columns[old_column])
    # a drawback of deepcopy is that the table information is also copied
    # we need a clean column with no table built
    copy.table = None
    if old_column == 'id':
        copy.primary_key = True
    new_columns.append(copy)

# recreate the table with new columns and bind with metadata
metadata_obj.clear()
Table("users", metadata_obj, *new_columns)

# automap with new metadata
Base = automap_base(metadata=metadata_obj)
Base.prepare()

# now the mapped classes are constructed correctly.
User = Base.classes.users
session.query(User).all()
Yiming Zhang
Yiming Zhang
Quantitative Researcher Associate, JP Morgan