---
# System prepended metadata

title: Relationships

---

# Relationships

---

### Outline
1. Review types of relationships
2. When you should use an association table in SQLAlchemy
3. How to use a association table (CRUD)
4. Some other relationship options

---

## Types of relationships

---

### What type of relationship would you have between...
- `students` and `cohorts`
- `posts` and `comments`
- `users` and `user_profiles`
- `guests` and `events`
- `books` and `authors`


---

### One-to-one
- not expecially common
- typically useful when you have
    - a collection of columns that would have null values in most rows
    - that you won't access most of the time
- e.g. `users` and `user_profiles`


---


### One-to-many/Many-to-one
- these are most common
- the "many" side has the foreign key
- e.g.
    - `employees` and `departments`
    - `players` and `teams`


---

### Many-to-many

- no direct relationship between the two tables
- an intermediate table contains both the foreign keys
- e.g. 
    - `posts` and `tags`
    - `students` and `classes`
    - self joins too—`users` with `users` in a follows table

---


### Many-to-many

- on the database level, there is no distinction between a many-to-many relationship and just two one-to-many relationships
- the difference emerges in the models/in the ORM

---

### Association tables

- in SQLAlchemy, you only use an association table (as opposed to a model) when there is no other information on that table—just the two primary keys
- it also doesn't let you represent duplicates
- join table uses `db.Table`


---

### Association tables

```python=
pet_owners = db.Table(
    "pet_owners",
    db.Column(
        "pet_id", 
        db.Integer, 
        db.ForeignKey("pets.id"), 
        primary_key=True
    ),
    db.Column(
        "owner_id", 
        db.Integer, 
        db.ForeignKey("owners.id"),
        primary_key=True)
    )
```


---

### Relationships with association tables
- on the Owner model
```python=
pets = db.relationship(
        "Pet", 
        secondary=pet_owners, 
        back_populates="owners"
    )
```
- on the Pet model
```python=
owners = db.relationship(
        "Owner", 
        secondary=pet_owners, 
        back_populates="pets"
    )
```

---

### Use model class or association table?

- `guests` and `events`
- `pets` and `owners`
- `orders` and `products`
- `characters` and `items` 


---

## Join table CRUD

---

### Create entries in a join table
- get or create the two entities you want to associate
- use `append` to add one entity to the relevent list
```python=
# get my pet object from the database
juice = Pets.query.filter_by(name="Juice").first()
# create an owner object
juliet = Owner(name="Juliet")
# create join table entry for juliet and juice
juliet.pets.append(juice)
db.session.add(juliet)
db.session.commit()
```


---

### Create entries in a join table (using only ids)

- if you already have the ids of the objects, you would usually just query the database for the objects (using `.get()`) and then insert using the previous, however, if you really needed to, you could insert the values directly into the join table, then add and commit
- the syntax is not as memorable
```python=
db.session.connection().execute(
    pet_owners.insert().values(owner_id=5, pet_id=2)
)
```

---

### Read entries in a join table
- the `db.relationship` will allow you to access the collection of the associated objects as a property
```python=
pets = juliet.pets

# use a comprehension to get the information
# that you actually want
print([pet.name for pet in pets])
```

---

### Update entries in a join table
- you don't!
- there's nothing to update—either the two entries are joined in the table, or they aren't
- there's no other information to change


---

### Delete entries in a join table
- get or create the two objects that you no longer want to be associated
- use `remove` to delete the association
```python=
not_juliet.pets.remove(juice) 
db.session.commit()
```


---

## Additional options


---


### Cascade delete
- add `passive_deletes=True` to the `db.relationship` on the parent model
- add `ondelete="CASCADE"` to the `db.ForeignKey` column


---

### Cascade delete

```python=
class Circle(db.Model):
    __tablename__ = 'circles'
    id = db.Column(db.Integer, primary_key=True)
    students = db.relationship(
        "Student", back_populates="circle", passive_deletes=True,
    )
class Student(db.Model):
    __tablename__ = 'students'
    id = db.Column(db.Integer, primary_key=True)
    circle_id = db.Column(
        db.Integer, 
        db.ForeignKey('circles.id', ondelete="CASCADE")
    )
    circle = db.relationship("Circle", back_populates="students")
```

---


### Lazy and eager loading

- by default, a `db.relationship` will use lazy loading (`lazy='select'`)
- for eager loading, you could use `lazy='joined'`
- sometimes you may want to use `lazy='dynamic'` so that you can perform additional filtering
- you can also configure the loading for an individual query
- more options: [documentation](https://docs.sqlalchemy.org/en/13/orm/relationship_api.html#sqlalchemy.orm.relationship.params.lazy)
- but remember: make it work before you make it fast