In [1]:
from pathlib import Path

In [3]:
# Version Check
import sqlalchemy
print(sqlalchemy.__version__)


1.3.4

In [4]:
sqlite_engine_prefix_for_relative_paths = 'sqlite://'
sqlite_engine_prefix_for_absolute_paths = 'sqlite:///'

In [5]:
print(Path.cwd())
print(str(Path.cwd() / "example.db"))


/home/topolo/PropD/MLgrabbag/notebooks
/home/topolo/PropD/MLgrabbag/notebooks/example.db

In [6]:
# Create subdirectory if it doesn't exists
data_path = Path.cwd() / "data"
if not data_path.exists():
    data_path.mkdir(mode=0o777)
print(data_path.exists())
data_path.resolve()


True
Out[6]:
PosixPath('/home/topolo/PropD/MLgrabbag/notebooks/data')

In [7]:
from sqlalchemy import create_engine

In [8]:
create_engine_input = \
    sqlite_engine_prefix_for_absolute_paths + \
        str((data_path / "example.db").resolve())
print(create_engine_input)

# Works
#create_engine_input = \
#    sqlite_engine_prefix_for_relative_paths + \
#        "/example.db"

print(create_engine_input)


sqlite:////home/topolo/PropD/MLgrabbag/notebooks/data/example.db
sqlite:////home/topolo/PropD/MLgrabbag/notebooks/data/example.db

In [9]:
engine = \
    create_engine(create_engine_input,
                 echo=True)

The return value of create_engine() is an instance of Engine, and it represents the core interface to the database.

The first time a method like Engine.execute() or Engine.connect() is called, the Engine establishes a real DBAPI connection to the databse, which is ten used to emit the SQL.

  • When using the ORM, we typically don't use the Engine directly once created; instead it's used behind the scenes by the ORM.
  • lazy connecting, Engine, when first returned by create_engine(), hasn't actually tried to connect to the database yet; that happens only the first time it's asked to perform a task against the databse.
  • See also Database Urls examples of create_engine()

Declare a Mapping (between database tables and our own classes)

When using ORM, the configurational process starts by describing database tables we're dealing with, and define own classes that'll be mapped to those tables. These 2 tasks are usually performed together in modern SQLAlchemy, using a system known as Declarative, which allows us to create classes that include directives to describe the actual database table they'll be mapped to.

Classes mapped using the Declarative system are defined in terms of a base class which maintains a catalog of classes and tables relative to that base, known as the declarative base class. Our application will usually have just 1 instance of this base in a commonly imported module.

Create this base class using declarative_base():


In [10]:
from sqlalchemy.ext.declarative import declarative_base

In [11]:
Base = declarative_base()

cf. https://docs.sqlalchemy.org/en/13/orm/extensions/declarative/api.html

sqlalchemy.ext.declarative.declarative_base(bind=None, metadata=None, mapper=None, cls=<class 'object'>, name='Base', constructor=<function_declarative_constructor>, class_registry=None, metaclass=<class 'sqlalchemy.ext.declarative.api.DeclarativeMeta'>

new base class will be given a metaclass that produces appropriate Table objects and makes the appropriate mapper() calls based on information provided declaratively in the class and any subclasses of the class.

Parameters:

  • bind - optional Connectable, will be assigned the bind attribute on MetaData instance.
  • metadata - optional MetaData instance. All Table objects implicitly declared by subclasses of base will share this MetaData.

  • class_registry - optional dictionary that'll serve as the registry of class names->mapped classes when string names are used to identify classes inside of relationship(), and others.

Now that we have a "base", define any number of mapped classes in terms of it.

start with table called "users" <-> class User map to this table "users"


In [12]:
from sqlalchemy import Column, Integer, String

In [13]:
class User(Base):
    # class using Declarative at minimum needs __tablename__ atttribute
    __tablename__ = 'users'

    # class using Declarative needs at least 1 Column which is part of a 
    # primary key.
    #
    # SQLAlchemy never makes any assumptions by itself about table to which
    # a class refers, including that it has no built-in conventions for
    # names, datatypes, or constraints.
    
    id = Column(Integer, primary_key=True)
    name = Column(String)
    fullname = Column(String)
    nickname = Column(String)
    
    def __repr__(self):
        return "<User(name='%s', fullname='%s', nickname='%s')>" % (
            self.name, self.fullname, self.nickname)

Create your own automated conventions using helper functions and mixin classes, described in Mixin and Custom Base Classes

When class constructed, Declarative replaces all Column objects with special Python accessors known as descriptors, process known as instrumentation.

  • The "instrumented" mapped class will provide us with the means to refer to our table in a SQL context as well as to persist and load values of columns from the database.

Create a Schema

With User class constructed via Declarative system, we have defined information about our table, known as table metadata.

Object used by SQLAlchemy to represent this information for a specific table is called Table object, and here Declarative has made 1 for us. We see this by inspecting __table__ attribute:


In [14]:
User.__table__


Out[14]:
Table('users', MetaData(bind=None), Column('id', Integer(), table=<users>, primary_key=True, nullable=False), Column('name', String(), table=<users>), Column('fullname', String(), table=<users>), Column('nickname', String(), table=<users>), schema=None)

When we declared our class, Declarative also created Table object according to our specifications, and associated it with class by constructing Mapper object.

Classical Mappings, any plain Python class can be mapped to any Table using mapper() function, described in Classical Mappings

Table object is a member of larger collection known as MetaData. When using Declarative, this object is available using .metadata attribute.


In [15]:
Base.metadata


Out[15]:
MetaData(bind=None)

In [17]:
User.metadata


Out[17]:
MetaData(bind=None)

In [16]:
# MetaData.create_all() checks first presence of tables, in actual 
# CREATE TABLE statement
# Note, if the database hadn't been created yet, this will literally create the database in the file system.
Base.metadata.create_all(engine)


2019-06-17 01:38:38,956 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
2019-06-17 01:38:38,957 INFO sqlalchemy.engine.base.Engine ()
2019-06-17 01:38:38,959 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
2019-06-17 01:38:38,960 INFO sqlalchemy.engine.base.Engine ()
2019-06-17 01:38:38,962 INFO sqlalchemy.engine.base.Engine PRAGMA table_info("users")
2019-06-17 01:38:38,962 INFO sqlalchemy.engine.base.Engine ()
2019-06-17 01:38:38,964 INFO sqlalchemy.engine.base.Engine 
CREATE TABLE users (
	id INTEGER NOT NULL, 
	name VARCHAR, 
	fullname VARCHAR, 
	nickname VARCHAR, 
	PRIMARY KEY (id)
)


2019-06-17 01:38:38,965 INFO sqlalchemy.engine.base.Engine ()
2019-06-17 01:38:38,983 INFO sqlalchemy.engine.base.Engine COMMIT

Minimal Table Descriptions vs. Full Descriptions

VARCHAR columns were generated without length on SQLite and PostgreSQL, this is a valid datatype, but not on others.

Length may be provided to String type:

Column(String(50))

The length field on String as well as similar precision/scale fields available on Integer, Numeric, etc. aren't referenced by SQLAlchemy other than when creating tables.

Additionally, Firebird and Oracle require sequences to generate primary key identifiers, and SQLAlchemy doesn't generate or assume these without being instructed.

If otherwise, use Sequence construct

from sqlalchemy import Sequence
Column(Integer, sequence('user_id_seq'), primary_key=True)

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, Sequence('user_id_seq'), primary_key=True)
    name = Column(string(50))
    fullname = Column(String(50))
    nickname = Column(String(50))

    def __repr__(self):
        return "<Username>='%s', fullname='%s', nickname='%s')>" % (
            self.name, self.fullname, self.nickname)

Create Instance of the Mapped Class


In [18]:
ed_user = User(name='ed', fullname='Ed Jones', nickname='edsnickname')
print(ed_user.name)


ed

In [19]:
ed_user.nickname


Out[19]:
'edsnickname'

In [20]:
str(ed_user.id)


Out[20]:
'None'

Even though we didn't specify it in ctor, id attribute produces value None when we access it (as opposed to Python's raising AttributeError for undefined attribute). SQLAlchemy's instrumentation normally produces this default value for column-mapped attributes when 1st accessed.

Creating a Session

Start talking to database; ORM's "handle" to db is the Session. When we first set up the application, at same level as our create_engine(), we define Session class which will serve as factor for new Session objects:


In [21]:
from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)

In the case where your application doesn't have an Engine when you define your module-level objects, just set it up like this:

Session = sessionmaker()

Later, when you create your engine with create_engine(), connect it to Session using configure():

Session.configure(bind=engine)

When do I construct a Session when do I commit it, and when do I close it?

When you need to have a conversation with the database, instantiate a Session; Session associated with our SQLite-enabled Engine, but hasn't opened any connections yet.


In [22]:
session = Session()

Adding and Updating Objects

To persist our User object, add() it to our Session:


In [23]:
ed_user = User(name='ed', fullname='Ed Jones', nickname='edsnickname')
session.add(ed_user)

At this point, instance is pending; no SQL has yet been issued and object isn't represented yet by row in the database.

  • The Session will issue SQL to persist Ed Jones as soon as needed, process known as flush
  • If we query database for Ed Jones, all pending information will be first be flushed, and query immediately issued.

    e.g. below, we create a new Query object which loads instances of Users. We "filter by" name attribute of ed


In [24]:
our_user = session.query(User).filter_by(name='ed').first()


2019-06-17 01:39:30,808 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2019-06-17 01:39:30,811 INFO sqlalchemy.engine.base.Engine INSERT INTO users (name, fullname, nickname) VALUES (?, ?, ?)
2019-06-17 01:39:30,812 INFO sqlalchemy.engine.base.Engine ('ed', 'Ed Jones', 'edsnickname')
2019-06-17 01:39:30,816 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname 
FROM users 
WHERE users.name = ?
 LIMIT ? OFFSET ?
2019-06-17 01:39:30,817 INFO sqlalchemy.engine.base.Engine ('ed', 1, 0)

In [25]:
our_user


Out[25]:
<User(name='ed', fullname='Ed Jones', nickname='edsnickname')>

In [26]:
# Session identified that row returned is the same row as 1 already
# represented within its internal map of objects
ed_user is our_user


Out[26]:
True

ORM concept of identity map ensures that all operations upon a particular row within a Session operate upon same set of data.


In [27]:
# Add more `User` objects at once using `add_all()`
session.add_all([
    User(name='wendy', fullname='Wendy Williams', nickname='windy'),
    User(name='mary', fullname='Mary Contrary', nickname='mary'),
    User(name='fred', fullname='Fred Flintstone', nickname='freddy')
])

In [28]:
# change Ed's nickname
ed_user.nickname = 'eddie'

In [29]:
# Session is paying attention, e.g.
session.dirty


Out[29]:
IdentitySet([<User(name='ed', fullname='Ed Jones', nickname='eddie')>])

In [30]:
# Sessino knows 3 new User objects are pending
session.new


Out[30]:
IdentitySet([<User(name='wendy', fullname='Wendy Williams', nickname='windy')>, <User(name='mary', fullname='Mary Contrary', nickname='mary')>, <User(name='fred', fullname='Fred Flintstone', nickname='freddy')>])

We tell Session we'd like to issue all remaining changes to the database and commit the transaction, which has been in progress throughout.

  • do this via commit()
  • e.g. Session emits UPDATE statement for the nickname change on "ed", as well as INSERT statements for 3 new User objects added

In [31]:
session.commit()


2019-06-17 01:39:39,256 INFO sqlalchemy.engine.base.Engine UPDATE users SET nickname=? WHERE users.id = ?
2019-06-17 01:39:39,258 INFO sqlalchemy.engine.base.Engine ('eddie', 1)
2019-06-17 01:39:39,260 INFO sqlalchemy.engine.base.Engine INSERT INTO users (name, fullname, nickname) VALUES (?, ?, ?)
2019-06-17 01:39:39,260 INFO sqlalchemy.engine.base.Engine ('wendy', 'Wendy Williams', 'windy')
2019-06-17 01:39:39,262 INFO sqlalchemy.engine.base.Engine INSERT INTO users (name, fullname, nickname) VALUES (?, ?, ?)
2019-06-17 01:39:39,263 INFO sqlalchemy.engine.base.Engine ('mary', 'Mary Contrary', 'mary')
2019-06-17 01:39:39,264 INFO sqlalchemy.engine.base.Engine INSERT INTO users (name, fullname, nickname) VALUES (?, ?, ?)
2019-06-17 01:39:39,264 INFO sqlalchemy.engine.base.Engine ('fred', 'Fred Flintstone', 'freddy')
2019-06-17 01:39:39,266 INFO sqlalchemy.engine.base.Engine COMMIT

In [32]:
ed_user.id # Now is 1


2019-06-17 01:39:41,061 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2019-06-17 01:39:41,062 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname 
FROM users 
WHERE users.id = ?
2019-06-17 01:39:41,063 INFO sqlalchemy.engine.base.Engine (1,)
Out[32]:
1

In [33]:
ed_user


Out[33]:
<User(name='ed', fullname='Ed Jones', nickname='eddie')>

Session Object States

transient - User object moved from being outside the Session, pending - inside the Session without primary key, to persistent - actually being inserted

read Quickie Intro to Object States

Rolling Back


In [34]:
ed_user.name = 'Edwardo'

In [35]:
fake_user = User(name='fakeuser', fullname='Invalid', nickname='12345')
session.add(fake_user)

In [36]:
# Querying session, they're flushed into current transaction
session.query(User).filter(User.name.in_(['Edwardo', 'fakeuser'])).all()


2019-06-17 01:39:48,157 INFO sqlalchemy.engine.base.Engine UPDATE users SET name=? WHERE users.id = ?
2019-06-17 01:39:48,158 INFO sqlalchemy.engine.base.Engine ('Edwardo', 1)
2019-06-17 01:39:48,159 INFO sqlalchemy.engine.base.Engine INSERT INTO users (name, fullname, nickname) VALUES (?, ?, ?)
2019-06-17 01:39:48,160 INFO sqlalchemy.engine.base.Engine ('fakeuser', 'Invalid', '12345')
2019-06-17 01:39:48,163 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname 
FROM users 
WHERE users.name IN (?, ?)
2019-06-17 01:39:48,164 INFO sqlalchemy.engine.base.Engine ('Edwardo', 'fakeuser')
Out[36]:
[<User(name='Edwardo', fullname='Ed Jones', nickname='eddie')>,
 <User(name='fakeuser', fullname='Invalid', nickname='12345')>]

Rolling back, we can see that ed_user's name is back to ed, and fake_user has been kicked out of the session


In [37]:
session.rollback()


2019-06-17 01:39:50,879 INFO sqlalchemy.engine.base.Engine ROLLBACK

In [38]:
ed_user.name


2019-06-17 01:39:51,552 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2019-06-17 01:39:51,554 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname 
FROM users 
WHERE users.id = ?
2019-06-17 01:39:51,555 INFO sqlalchemy.engine.base.Engine (1,)
Out[38]:
'ed'

In [39]:
fake_user in session


Out[39]:
False

In [40]:
session.query(User).filter(User.name.in_(['ed', 'fakeuser'])).all()


2019-06-17 01:39:55,815 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname 
FROM users 
WHERE users.name IN (?, ?)
2019-06-17 01:39:55,816 INFO sqlalchemy.engine.base.Engine ('ed', 'fakeuser')
Out[40]:
[<User(name='ed', fullname='Ed Jones', nickname='eddie')>]

Querying


In [41]:
for instance in session.query(User).order_by(User.id):
    print(instance.name, instance.fullname)


2019-06-17 01:39:57,129 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname 
FROM users ORDER BY users.id
2019-06-17 01:39:57,131 INFO sqlalchemy.engine.base.Engine ()
ed Ed Jones
wendy Wendy Williams
mary Mary Contrary
fred Fred Flintstone

In [43]:
for name, fullname in session.query(User.name, User.fullname):
    print(name, fullname)


2019-06-17 01:40:00,545 INFO sqlalchemy.engine.base.Engine SELECT users.name AS users_name, users.fullname AS users_fullname 
FROM users
2019-06-17 01:40:00,546 INFO sqlalchemy.engine.base.Engine ()
ed Ed Jones
wendy Wendy Williams
mary Mary Contrary
fred Fred Flintstone

In [42]:
for row in session.query(User, User.name).all():
    print(row.User, row.name)


2019-06-17 01:39:58,601 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname 
FROM users
2019-06-17 01:39:58,602 INFO sqlalchemy.engine.base.Engine ()
<User(name='ed', fullname='Ed Jones', nickname='eddie')> ed
<User(name='wendy', fullname='Wendy Williams', nickname='windy')> wendy
<User(name='mary', fullname='Mary Contrary', nickname='mary')> mary
<User(name='fred', fullname='Fred Flintstone', nickname='freddy')> fred

In [44]:
for row in session.query(User.name.label('name_label')).all():
    print(row.name_label)


2019-06-17 01:40:02,769 INFO sqlalchemy.engine.base.Engine SELECT users.name AS name_label 
FROM users
2019-06-17 01:40:02,771 INFO sqlalchemy.engine.base.Engine ()
ed
wendy
mary
fred

The name given to a full entity such as User, assuming that multiple entities are present in the call to query(), can be controlled using aliased()


In [45]:
from sqlalchemy.orm import aliased
user_alias = aliased(User, name='user_alias')

In [46]:
for row in session.query(user_alias, user_alias.name).all():
    print(row.user_alias)


2019-06-17 01:40:05,812 INFO sqlalchemy.engine.base.Engine SELECT user_alias.id AS user_alias_id, user_alias.name AS user_alias_name, user_alias.fullname AS user_alias_fullname, user_alias.nickname AS user_alias_nickname 
FROM users AS user_alias
2019-06-17 01:40:05,814 INFO sqlalchemy.engine.base.Engine ()
<User(name='ed', fullname='Ed Jones', nickname='eddie')>
<User(name='wendy', fullname='Wendy Williams', nickname='windy')>
<User(name='mary', fullname='Mary Contrary', nickname='mary')>
<User(name='fred', fullname='Fred Flintstone', nickname='freddy')>

Basic operations with Query (i.e. actual SQL commands) include LIMIT and OFFSET, most conveniently using Python array slices, in conjunction with ORDER BY:


In [50]:
for u in session.query(User).order_by(User.id)[1:3]:
    print(u)


2019-06-17 01:42:19,850 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname 
FROM users ORDER BY users.id
 LIMIT ? OFFSET ?
2019-06-17 01:42:19,851 INFO sqlalchemy.engine.base.Engine (2, 1)
<User(name='wendy', fullname='Wendy Williams', nickname='windy')>
<User(name='mary', fullname='Mary Contrary', nickname='mary')>

In [51]:
# filtering results
for name, in session.query(User.name).filter_by(fullname='Ed Jones'):
    print(name)


2019-06-17 02:52:26,631 INFO sqlalchemy.engine.base.Engine SELECT users.name AS users_name 
FROM users 
WHERE users.fullname = ?
2019-06-17 02:52:26,632 INFO sqlalchemy.engine.base.Engine ('Ed Jones',)
ed

or filter(), which uses more flexible SQL expression language constructs. These allow you to use regular Python operators with the class-level attributes on your mapped class:


In [52]:
for name, in session.query(User.name).filter(User.fullname=='Ed Jones'):
    print(name)


2019-06-17 02:53:56,242 INFO sqlalchemy.engine.base.Engine SELECT users.name AS users_name 
FROM users 
WHERE users.fullname = ?
2019-06-17 02:53:56,243 INFO sqlalchemy.engine.base.Engine ('Ed Jones',)
ed

In [53]:
# further criteria may be added
for user in session.query(User).filter(User.name=='ed').filter(User.fullname=='Ed Jones'):
    print(user)


2019-06-17 02:59:25,232 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname 
FROM users 
WHERE users.name = ? AND users.fullname = ?
2019-06-17 02:59:25,234 INFO sqlalchemy.engine.base.Engine ('ed', 'Ed Jones')
<User(name='ed', fullname='Ed Jones', nickname='eddie')>

Common Filter Operators


In [58]:
# equals
list(session.query(User).filter(User.name == 'ed'))


2019-06-17 03:00:47,587 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname 
FROM users 
WHERE users.name = ?
2019-06-17 03:00:47,588 INFO sqlalchemy.engine.base.Engine ('ed',)
Out[58]:
[<User(name='ed', fullname='Ed Jones', nickname='eddie')>]

In [59]:
# not equals
list(session.query(User).filter(User.name != 'ed'))


2019-06-17 03:01:05,506 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname 
FROM users 
WHERE users.name != ?
2019-06-17 03:01:05,508 INFO sqlalchemy.engine.base.Engine ('ed',)
Out[59]:
[<User(name='wendy', fullname='Wendy Williams', nickname='windy')>,
 <User(name='mary', fullname='Mary Contrary', nickname='mary')>,
 <User(name='fred', fullname='Fred Flintstone', nickname='freddy')>]

In [61]:
# LIKE
list(session.query(User).filter(User.name.like('%ed%')))


2019-06-17 03:01:43,750 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname 
FROM users 
WHERE users.name LIKE ?
2019-06-17 03:01:43,751 INFO sqlalchemy.engine.base.Engine ('%ed%',)
Out[61]:
[<User(name='ed', fullname='Ed Jones', nickname='eddie')>,
 <User(name='fred', fullname='Fred Flintstone', nickname='freddy')>]

In [62]:
# ILIKE (case-insensitive LIKE)
list(session.query(User).filter(User.name.ilike('%ed')))


2019-06-17 03:02:19,626 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname 
FROM users 
WHERE lower(users.name) LIKE lower(?)
2019-06-17 03:02:19,627 INFO sqlalchemy.engine.base.Engine ('%ed',)
Out[62]:
[<User(name='ed', fullname='Ed Jones', nickname='eddie')>,
 <User(name='fred', fullname='Fred Flintstone', nickname='freddy')>]

In [63]:
# IN
list(session.query(User).filter(User.name.in_(['ed', 'wendy', 'jack'])))


2019-06-17 03:02:57,559 INFO sqlalchemy.engine.base.Engine SELECT users.id AS users_id, users.name AS users_name, users.fullname AS users_fullname, users.nickname AS users_nickname 
FROM users 
WHERE users.name IN (?, ?, ?)
2019-06-17 03:02:57,560 INFO sqlalchemy.engine.base.Engine ('ed', 'wendy', 'jack')
Out[63]:
[<User(name='ed', fullname='Ed Jones', nickname='eddie')>,
 <User(name='wendy', fullname='Wendy Williams', nickname='windy')>]

In [ ]:

Building a Relationship


In [47]:
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship

In [48]:
class Address(Base):
    __tablename__ = 'addresses'
    id = Column(Integer, primary_key=True)
    email_address = Column(String, nullable=False)

    # ForeignKey here expresses that values in the addresses.user_id
    # column should be constrained to those values in the users.id column,
    # i.e. its primary key.
    user_id = Column(Integer, ForeignKey('users.id'))
    
    user = relationship("User", back_populates="addresses")
    
    def __repr__(self):
        return "<Address(email_address='%s')>" % self.email_address

In [49]:
# relationship tells the ORM that Address class itself should be linked to
# User class, using the attribute Address.user
User.addresses = relationship(
    "Address", order_by=Address.id, back_populates="user")

relationship() uses foreign key relationships between the 2 tables to determine the nature of the linkage, determining that Address.user will be many to one.

relationship.back_populates is assigned to refer to the complementary attribute names; by doing so, each relationship() can make intelligent decision about same relaitonship as expressed in reverse; Address.user refers to User instance, and on other side User.addresses refers to list of Address instances.

More on back_populates, a newer version of back_ref, in Linking Relationships with Backref


In [ ]: