diff --git a/README.md b/README.md index d002bcc7..a871eb4c 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,17 @@ ## Learning Goals -- Use SQLAlchemy to join tables with one-to-one, one-to-many, and - many-to-many relationships. +- Use SQLAlchemy to join tables with one-to-one, one-to-many, and many-to-many + relationships. -*** +--- ## Introduction In the previous lesson, we saw how to create a **one-to-many** association -between two models using SQLAlchemy by following certain naming conventions, -use of the `relationship()` and `backref()` methods, and using the right -foreign key on our tables when generating the migrations. +between two models using SQLAlchemy by following certain naming conventions, use +of the `relationship()` and `backref()` methods, and using the right foreign key +on our tables when generating the migrations. In the SQL section, we learned about one other kind of relationship: the **many-to-many**, also known as the **has many through**, relationship. For @@ -34,14 +34,15 @@ these relationships: - A user **has many** reviews. - A user **has many** games, **through** reviews. -Once we're done setting up the database tables, here's what the ERD will look like: +Once we're done setting up the database tables, here's what the ERD will look +like: ![Game Reviews ERD](https://curriculum-content.s3.amazonaws.com/phase-3/active-record-associations-many-to-many/games-reviews-users-erd.png) -To get started, run `pipenv install; pipenv shell`, then follow along with -the code. +To get started, run `pipenv install; pipenv shell`, then follow along with the +code. -*** +--- ## Creating a User Model @@ -85,14 +86,14 @@ values: itself. Since the database is kept in one central location, assigning it the work of creating default values means that we don't have to worry about the quality of our developers' or users' computers. -- `onupdate` means exactly what it says: when the record is updated, the - column value is set. +- `onupdate` means exactly what it says: when the record is updated, the column + value is set. - We saw `func` briefly in the previous module; it allows us to use SQL operations instead of their Python counterparts. This benefits us for the same reasons as `server_default`. `func.now()` is equivalent to the current time. -> **Note: If you add these timestamps to your `Game` and `Review` models (not -> a bad idea), you will have to clear the data in your database before running +> **Note: If you add these timestamps to your `Game` and `Review` models (not a +> bad idea), you will have to clear the data in your database before running > your new migration. This is because SQLite can't populate existing records > with dynamic default values, like `func.now()`.** @@ -129,9 +130,9 @@ class Review(Base): ``` -Now run `alembic revision --autogenerate -m'Add User model'` from the -`lib` directory to make our migration. If all goes well, run -`alembic upgrade head` to push your migrations to the database. +Now run `alembic revision --autogenerate -m'Add User model'` from the `lib` +directory to make our migration. If all goes well, run `alembic upgrade head` to +push your migrations to the database. ```console $ alembic upgrade head @@ -146,7 +147,7 @@ Run the first seed file as well to populate the `games` and `reviews` tables: $ python seed.py ``` -*** +--- ## Creating a Many-to-Many Relationship @@ -171,8 +172,8 @@ approach, `Table` objects. ### Many-to-Many with an Association Object An association object is really just another model, so we can create a -**`GameUser`** model using our `Base` object and simply build relationships -in either direction, as seen below: +**`GameUser`** model using our `Base` object and simply build relationships in +either direction, as seen below: ```py # example only @@ -193,9 +194,9 @@ class GameUser(Base): ``` -An association object can use either its own primary key or a combination of -the two joined tables' primary keys as a unique identifier. Here, we use the -simpler strategy and create an `id` column. +An association object can use either its own primary key or a combination of the +two joined tables' primary keys as a unique identifier. Here, we use the simpler +strategy and create an `id` column. Next, we use the `relationship()` method to connect to both the `Game` and `User` models. Here, we opt for the `back_populates` argument in place of @@ -215,9 +216,9 @@ preferred approach to define many-to-many relationships using SQLAlchemy is with ### Many-to-Many with `Table` Objects `Table` objects are instances of the `sqlalchemy.Table` class. They function -more or less the same as data models, with the exception of being a little -more compact. This syntax visually de-emphasizes association tables in your -models and is the preferred approach for simple many-to-many relationships in +more or less the same as data models, with the exception of being a little more +compact. This syntax visually de-emphasizes association tables in your models +and is the preferred approach for simple many-to-many relationships in SQLAlchemy. Let's build the same association table as above with our new syntax: @@ -266,8 +267,8 @@ A few more notes on this approach: best way to leave readable code behind for other developers. Run `alembic revision --autogenerate -m'Add game_user Association Table'`, then -`alembic upgrade head`. You can use the script in `lib/seed_2.py` to generate new -data and interact with your database through the Python shell. To create +`alembic upgrade head`. You can use the script in `lib/seed_2.py` to generate +new data and interact with your database through the Python shell. To create relationships between `Game` records and `User` records, run the second seed file with `python seed_2.py`. @@ -390,14 +391,14 @@ building a many-to-many relationship between games and users. Run `pytest -x` from the `lib/` directory to see if your models are working as expected- and don't forget to use Alembic to create your database first! -*** +--- ## Conclusion -The power of SQLAlchemy all boils down to understanding database -relationships and making use of the correct classes and methods. By leveraging -"convention over configuration", we're able to quickly set up complex -associations between multiple models with just a few lines of code. +The power of SQLAlchemy all boils down to understanding database relationships +and making use of the correct classes and methods. By leveraging "convention +over configuration", we're able to quickly set up complex associations between +multiple models with just a few lines of code. The **one-to-many** and **many-to-many** relationships are the most common when working with relational databases. You can apply the same concepts and code we @@ -412,11 +413,11 @@ Actor -< Character >- Movie The code required to set up these relationships would look very similar to the code we wrote in this lesson. -By understanding the conventions SQLAlchemy expects you to follow, and how -the underlying database relationships work, you have the ability to model all -kinds of complex, real-world concepts in your code! +By understanding the conventions SQLAlchemy expects you to follow, and how the +underlying database relationships work, you have the ability to model all kinds +of complex, real-world concepts in your code! -*** +--- ## Solution Code @@ -424,13 +425,16 @@ kinds of complex, real-world concepts in your code! # many_to_many/models.py from sqlalchemy import create_engine, func -from sqlalchemy import ForeignKey, Table, Column, Integer, String, DateTime +from sqlalchemy import ForeignKey, Table, Column, Integer, String, DateTime, MetaData from sqlalchemy.orm import relationship, backref from sqlalchemy.ext.declarative import declarative_base -engine = create_engine('sqlite:///many_to_many.db') +convention = { + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", +} +metadata = MetaData(naming_convention=convention) -Base = declarative_base() +Base = declarative_base(metadata=metadata) game_user = Table( 'game_users', @@ -440,6 +444,22 @@ game_user = Table( extend_existing=True, ) +class User(Base): + __tablename__ = 'users' + + id = Column(Integer(), primary_key=True) + name = Column(String()) + created_at = Column(DateTime(), server_default=func.now()) + updated_at = Column(DateTime(), onupdate=func.now()) + + reviews = relationship('Review', backref=backref('user')) + games = relationship('Game', secondary=game_user, back_populates='users') + + # don't forget your __repr__()! + def __repr__(self): + return f'User(id={self.id}, ' + \ + f'name={self.name})' + class Game(Base): __tablename__ = 'games' @@ -449,41 +469,24 @@ class Game(Base): platform = Column(String()) price = Column(Integer()) + reviews = relationship('Review', backref=backref('game')) users = relationship('User', secondary=game_user, back_populates='games') - reviews = relationship('Review', backref=backref('game'), cascade='all, delete-orphan') def __repr__(self): return f'Game(id={self.id}, ' + \ f'title={self.title}, ' + \ f'platform={self.platform})' -class User(Base): - __tablename__ = 'users' - - id = Column(Integer(), primary_key=True) - name = Column(String()) - created_at = Column(DateTime(), server_default=func.now()) - updated_at = Column(DateTime(), onupdate=func.now()) - - games = relationship('Game', secondary=game_user, back_populates='users') - reviews = relationship('Review', backref=backref('user'), cascade='all, delete-orphan') - - def __repr__(self): - return f'User(id={self.id}, ' + \ - f'name={self.name})' - class Review(Base): __tablename__ = 'reviews' id = Column(Integer(), primary_key=True) - score = Column(Integer()) comment = Column(String()) game_id = Column(Integer(), ForeignKey('games.id')) user_id = Column(Integer(), ForeignKey('users.id')) - def __repr__(self): return f'Review(id={self.id}, ' + \ f'score={self.score}, ' + \ @@ -491,7 +494,7 @@ class Review(Base): ``` -*** +--- ## Resources