sqlmodel/docs/tutorial/relationship-attributes/back-populates.md
Sebastián Ramírez 6d1d86ab85 📝 Add docs
2021-08-24 15:02:48 +02:00

12 KiB

Relationship back_populates

Now you know how to use the relationship attributes to manipulate connected data in the database! 🎉

Let's now take a small step back and review how we defined those Relationship() attributes again, let's clarify that back_populates argument. 🤓

Relationship with back_populates

So, what is that back_populates argument in each Relationship()?

The value is a string with the name of the attribute in the other model class.

That tells SQLModel that if something changes in this model, it should change that attribute in the other model, and it will work even before committing with the session (that would force a refresh of the data).

Let's understand that better with an example.

An Incomplete Relationship

Let's see how that works by writing an incomplete version first, without back_populates:

{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:1-21]!}

# Code below omitted 👇
👀 Full file preview
{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!}

Read Data Objects

Now, we will get the Spider-Boy hero and, independently, the Preventers team using two selects.

As you already know how this works, I won't separate that in a select statement, results, etc. Let's use the shorter form in a single call:

# Code above omitted 👆

{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:105-113]!}

# Code below omitted 👇
👀 Full file preview
{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!}

!!! tip When writing your own code, this is probably the style you will use most often, as it's shorter, more convenient, and you still get all the power of autocompletion and inline errors.

Print the Data

Now, let's print the current Spider-Boy, the current Preventers team, and particularly, the current Preventers list of heroes:

# Code above omitted 👆

{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:105-117]!}

# Code below omitted 👇
👀 Full file preview
{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!}

Up to this point, it's all good. 😊

In particular, the result of printing preventers_team.heroes is:

Preventers Team Heroes: [
        Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2),
        Hero(name='Spider-Boy', age=None, id=3, secret_name='Pedro Parqueador', team_id=2),
        Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2),
        Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2),
        Hero(name='Captain North America', age=93, id=8, secret_name='Esteban Rogelios', team_id=2)
]

Notice that we have Spider-Boy there.

Update Objects Before Committing

Now let's update Spider-Boy, removing him from the team by setting hero_spider_boy.team = None and then let's print this object again:

# Code above omitted 👆

{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:105-106]!}

        # Code here omitted 👈

{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:119-123]!}

# Code below omitted 👇
👀 Full file preview
{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!}

The first important thing is, we haven't commited the hero yet, so accessing the list of heroes would not trigger an automatic refresh.

But in our code, in this exact point in time, we already said that Spider-Boy is no longer part of the Preventers. 🔥

!!! tip We could revert that later by not committing the session, but that's not what we are interested in here.

Here, at this point in the code, in memory, the code expects Preventers to not include Spider-Boy.

The output of printing hero_spider_boy without team is:

Spider-Boy without team: name='Spider-Boy' age=None id=3 secret_name='Pedro Parqueador' team_id=2 team=None

Cool, the team is set to None, the team_id attribute still has the team ID until we save it. But that's okay as we are now working mainly with the relationship attributes and the objects.

But now, what happens when we print the preventers_team.heroes?

Preventers Team Heroes again: [
        Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), 
        Hero(name='Spider-Boy', age=None, id=3, secret_name='Pedro Parqueador', team_id=2, team=None), 
        Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), 
        Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2), 
        Hero(name='Captain North America', age=93, id=8, secret_name='Esteban Rogelios', team_id=2)
]

Oh, no! 😱 Spider-Boy is still listed there!

Commit and Print

Now, if we commit it and print again:

# Code above omitted 👆

{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:105-106]!}

        # Code here omitted 👈

{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py[ln:125-132]!}

# Code below omitted 👇
👀 Full file preview
{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial001.py!}

When we access preventers_team.heroes after the commit, that triggers a refresh, so we get the latest list, without Spider-Boy, so that's fine again:

INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age, hero.team_id AS hero_team_id 
FROM hero 
WHERE ? = hero.team_id
2021-08-13 11:15:24,658 INFO sqlalchemy.engine.Engine [cached since 0.1924s ago] (2,)

Preventers Team Heroes after commit: [
        Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), 
        Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), 
        Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2), 
        Hero(name='Captain North America', age=93, id=8, secret_name='Esteban Rogelios', team_id=2)
]

There's no Spider-Boy after committing, so that's good. 😊

But we still have that inconsistency in that previous point above.

If we use the objects before committing, we could end up having errors. 😔

Let's fix that. 🤓

Fix It Using back_populates

That's what back_populates is for.

Let's add it back:

{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:1-21]!}

# Code below omitted 👇
👀 Full file preview
{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!}

And we can keep the rest of the code the same:

# Code above omitted 👆

{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:105-106]!}

        # Code here omitted 👈

{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:119-123]!}

# Code below omitted 👇
👀 Full file preview
{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!}

!!! tip This is the same section where we updated hero_spider_boy.team to None but we haven't committed that change yet.

The same section that caused a problem before.

Review the Result

This time, SQLModel (actually SQLAlchemy) will be able to notice the change, and automatically update the list of heroes in the team, even before we commit.

That second print would output:

Preventers Team Heroes again: [
        Hero(name='Rusty-Man', age=48, id=2, secret_name='Tommy Sharp', team_id=2), 
        Hero(name='Tarantula', age=32, id=6, secret_name='Natalia Roman-on', team_id=2), 
        Hero(name='Dr. Weird', age=36, id=7, secret_name='Steve Weird', team_id=2), 
        Hero(name='Captain North America', age=93, id=8, secret_name='Esteban Rogelios', team_id=2)
]

Notice that now Spider-Boy is not there, we fixed it with back_populates! 🎉

The Value of back_populates

Now that you know why back_populates is there, let's review the exact value again.

It's quite simple code, it's just a string, but it might be confusing to think exactly what string should go there:

{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:1-21]!}

# Code below omitted 👇
👀 Full file preview
{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!}

The string in back_populates is the name of the attribute in the other model, that will reference the current model.

So, in the class Team, we have an attribute heroes and we declare it with Relationship(back_populates="team").

# Code above omitted 👆

{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:6-11]!}

# Code below omitted 👇
👀 Full file preview
{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!}

The string in back_populates="team" refers to the attribute team in the class Hero (the other class).

And, in the class Hero, we declare an attribute team, and we declare it with Relationship(back_populates="heroes").

So, the string "heroes" refers to the attribute heroes in the class Team.

# Code above omitted 👆

{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py[ln:14-21]!}

# Code below omitted 👇
👀 Full file preview
{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial002.py!}

!!! tip Each relationship attribute points to the other one, in the other model, using back_populates.

Although it's simple code, it can be confusing to think about 😵, because the same line has concepts related to both models in multiple places:

  • Just by being in the current model, the line has something to do with the current model.
  • The name of the attribute is about the other model.
  • The type annotation is about the other model.
  • And the back_populates refers to an attribute in the other model, that points to the current model.

A Mental Trick to Remember back_populates

A mental trick you can use to remember is that the string in back_populates is always about the current model class you are editing. 🤓

So, if you are in the class Hero, the value of back_populates for any relationship attribute connecting to any other table (to any other model, it could be Team, Weapon, Powers, etc) will still always refer to this same class.

So, back_populates would most probably be something like "hero" or "heroes".

# Code above omitted 👆

{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py[ln:29-41]!}

# Code below omitted 👇
👀 Full file preview
{!./docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py!}