Skip to content

Commit 1f401d0

Browse files
authored
Create sql-alchemy.md
1 parent 57a037b commit 1f401d0

File tree

1 file changed

+171
-0
lines changed

1 file changed

+171
-0
lines changed

python/sql-alchemy.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Databases with SQLAlchemy
2+
3+
This is an example of how to work with databases (in this example we use a SQLite database) using the SQLAlchemy Python package. The example hows how to set up a database for tracking financial transactions in several accounts (account movements).
4+
5+
## Creating models
6+
7+
In the following example we want to create:
8+
9+
- A `User` model.
10+
- Each `User` may have various `Account`s.
11+
- The `Account` is associated to a single user.
12+
- Each `Account` has many `Movement`s. These represent the transactions of currency in and out of each account.
13+
- Each `Movement` belongs to a single `Account`.
14+
- Each `Movement` may be categorized with a `Category` and a `Subcategory`.
15+
- The `Subcategory` cannot repeat for the same `Category`.
16+
17+
```python
18+
from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, ForeignKey, UniqueConstraint
19+
from sqlalchemy.orm import declarative_base, sessionmaker, relationship
20+
21+
22+
Base = declarative_base()
23+
24+
25+
class User(Base):
26+
__tablename__ = "users"
27+
id = Column(Integer, primary_key=True)
28+
name = Column(String, nullable=False)
29+
30+
accounts = relationship("Account", back_populates="user", cascade="all, delete-orphan")
31+
32+
33+
class Account(Base):
34+
__tablename__ = "accounts"
35+
id = Column(Integer, primary_key=True)
36+
name = Column(String, nullable=False)
37+
38+
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
39+
user = relationship("User", back_populates="accounts")
40+
41+
movements = relationship("Movement", back_populates="account", cascade="all, delete-orphan")
42+
43+
44+
class Movement(Base):
45+
__tablename__ = "movements"
46+
id = Column(Integer, primary_key=True)
47+
date = Column(DateTime, nullable=False)
48+
description = Column(String)
49+
debit = Column(Float)
50+
credit = Column(Float)
51+
52+
account_id = Column(Integer, ForeignKey('accounts.id'), nullable=False)
53+
account = relationship("Account", back_populates="movements")
54+
55+
category_id = Column(Integer, ForeignKey('categories.id'), nullable=True)
56+
category = relationship("Category", back_populates="movements")
57+
58+
subcategory_id = Column(Integer, ForeignKey('subcategories.id'), nullable=True)
59+
subcategory = relationship("Subcategory", back_populates="movements")
60+
61+
shared = Column(Boolean, default=False, nullable=False)
62+
63+
64+
class Category(Base):
65+
__tablename__ = "categories"
66+
id = Column(Integer, primary_key=True)
67+
name = Column(String, nullable=False, unique=True)
68+
69+
subcategories = relationship("Subcategory", back_populates="category", cascade="all, delete-orphan")
70+
movements = relationship("Movement", back_populates="category")
71+
72+
73+
class Subcategory(Base):
74+
__tablename__ = "subcategories"
75+
id = Column(Integer, primary_key=True)
76+
name = Column(String, nullable=False)
77+
78+
category_id = Column(Integer, ForeignKey("categories.id"), nullable=False)
79+
category = relationship("Category", back_populates="subcategories")
80+
81+
movements = relationship("Movement", back_populates="subcategory")
82+
83+
# Make so a subcategory cannot repeat within a category but can across categories
84+
__table_args__ = (
85+
UniqueConstraint('name', 'category_id', name='uix_name_category'),
86+
)
87+
```
88+
89+
90+
## Connecting to the database
91+
92+
Let's connect to the database now. In the following, we if the database already exists and if it doesn't, we create it.
93+
94+
```python
95+
from pathlib import Path
96+
from sqlalchemy.orm import sessionmaker
97+
98+
99+
db_path = Path('database.db')
100+
101+
first_time = not db_path.exists() # False if DB exist, otherwise True (first time connecting)
102+
103+
engine = create_engine(f"sqlite:///{db_path}", echo=False)
104+
105+
# If this is the first time connecting create all tables
106+
if first_time:
107+
Base.metadata.create_all(engine)
108+
109+
Session = sessionmaker(bind=engine)
110+
111+
session = Session()
112+
```
113+
114+
115+
## Adding data to the database
116+
117+
Adding a single `User`. The code avoids adding a user if another exists with the same name.
118+
119+
```python
120+
USERNAME = 'John'
121+
122+
user = session.query(User).filter_by(name=USERNAME).first() # returns None if no user with name John exists.
123+
124+
# Add user if it doesn't exist
125+
if not user:
126+
user = User(name=USERNAME)
127+
session.add(user)
128+
print(f"Created User <{user.name}>.")
129+
```
130+
131+
Adding rows from a Pandas dataframe. We assume `Account`s, `Category`s, and `Subcategory`s have already been added.
132+
133+
```python
134+
# We assume we have a dataframe called df
135+
136+
for i, row in df.iterrows():
137+
# Find account
138+
account = session.query(Account).filter_by(name=row['Account']).first()
139+
140+
# Find category and subcategory
141+
if pd.isna(row["Category"]): # We want NaN values in the dataframe to translate to NULL values in the database
142+
category = None
143+
subcategory = None
144+
145+
else:
146+
category = session.query(Category).filter_by(name=row["Category"]).first()
147+
148+
if pd.isna(row["Subcategory"]):
149+
subcategory = None
150+
else:
151+
subcategory = session.query(Subcategory).filter_by(name=row["Subcategory"], category=category).first()
152+
153+
# Create movement
154+
movement = Movement(
155+
date=row['Date'],
156+
description=row['Description'],
157+
debit=row['Debit'],
158+
credit=row['Credit'],
159+
account=account,
160+
category=category,
161+
subcategory=subcategory
162+
)
163+
session.add(movement)
164+
165+
# Commit all new items to the database. Including the user from above,
166+
session.commit()
167+
```
168+
169+
## References
170+
171+
- [SQLAlchemy Documentation](https://docs.sqlalchemy.org/en/20/)

0 commit comments

Comments
 (0)