├── .gitignore ├── README.md ├── app.py ├── requirements.txt └── templates ├── dynamic.html └── movie.html /.gitignore: -------------------------------------------------------------------------------- 1 | movies.db 2 | __pycache__ 3 | *.pyc 4 | .vscode 5 | video 6 | models.mov 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask | Flask-SQLAlchemy Model Relationships 2 | 3 | A simple example demonstrating the progression of adding relationships to SQLAlchemy models in a Flask project. 4 | 5 | [Documentation for SQLAlchemy relationships](http://flask-sqlalchemy.pocoo.org/2.3/models/). 6 | 7 | Relationship Coordinality Samples: 8 | 9 | * One-to-One 10 | * One-to-Many 11 | * Many-to-Many 12 | 13 | ## One-to-One 14 | 15 | One-to-One relationships are defined with a `db.relationship()` function. This is not a `Column`, but a function that manages the relationship between models to return the Python objects when the property is accessed. 16 | 17 | The related object must have a `Column` for the `ForeignKey()`. 18 | 19 | The `backref` attribute is providing a name for an attribute to attach to the related model to allow access both directions. 20 | 21 | ```python 22 | # Access 23 | director.guild # returns object 24 | guild.director # returns object 25 | 26 | # Update Related Objects 27 | director.guild = Guild(...) # sets the related object to the new one 28 | guild.director = Director(...) # sets the related object to the new one 29 | db.session.commit() # persists changed models to the database 30 | ``` 31 | 32 | The only difference between a One-to-One and One-to-Many relationship is the use of the keyword argument `uselist` set to `False`. 33 | 34 | ```python 35 | class Director(db.Model): 36 | id = db.Column(db.Integer, primary_key=True) 37 | # ... 38 | guild = db.relationship( 39 | "GuildMembership", backref="director", lazy="select", uselist=False 40 | ) 41 | 42 | class GuildMembership(db.Model): 43 | id = db.Column(db.Integer, primary_key=True) 44 | # ... 45 | direcotr_id = db.Column(db.Integer, db.ForeignKey("director.id")) 46 | ``` 47 | 48 | ## One-to-Many 49 | 50 | One-to-Many relationships are defined with a `db.relationship()` function. This is not a `Column`, but a function that manages the relationship between models to return the Python objects when the property is accessed. 51 | 52 | The related object must have a `Column` for the `ForeignKey()`. 53 | 54 | ```python 55 | class Movie(db.Model): 56 | id = db.Column(db.Integer, primary_key=True) 57 | # ... 58 | director_id = db.Column(db.Integer, db.ForeignKey("director.id")) 59 | # ... 60 | 61 | class Director(db.Model): 62 | id = db.Column(db.Integer, primary_key=True) 63 | # ... 64 | movies = db.relationship( 65 | "Movie", backref="director", lazy="joined"), lazy="select" 66 | ) 67 | # ... 68 | ``` 69 | 70 | A One-to-Many relationship allows you to manipulate the related objects as native Python objects. 71 | 72 | ```python 73 | # Access 74 | director.movies # returns a list of objects 75 | for m in director.movies: 76 | print(m.title) 77 | 78 | movie.director # returns object 79 | movie.director.guild.name # traverses related objects to access nested data 80 | 81 | # Update Related Objects 82 | m = Movie(...) 83 | d = Director(...) 84 | 85 | db.session.add(m) 86 | db.session.add(d) 87 | 88 | director.movies.append(m) # adds related object to the list of movies 89 | movie.director = d # sets the related object to the new one 90 | db.session.commit() # persists changed models to the database 91 | ``` 92 | 93 | ## Many-to-Many 94 | 95 | Many-to-Many relationships are defined with a `db.relationship()` function. This is not a `Column`, but a function that manages the relationship between models to return the Python objects when the property is accessed. 96 | 97 | The related object does *not* have a `Column` for the `ForeignKey()`. 98 | 99 | Instead, a join table is needed to maintain the relationship by defining a `Table`. 100 | 101 | ```python 102 | actors = db.Table( 103 | "actors", 104 | db.Column("actor_id", db.Integer, db.ForeignKey("actor.id")), 105 | db.Column("movie_id", db.Integer, db.ForeignKey("movie.id")), 106 | ) 107 | 108 | class Movie(db.Model): 109 | id = db.Column(db.Integer, primary_key=True) 110 | # ... 111 | actors = db.relationship("Actor", secondary=actors, backref="movies", lazy="select") 112 | # ... 113 | 114 | class Actor(db.Model): 115 | id = db.Column(db.Integer, primary_key=True) 116 | # ... 117 | ``` 118 | 119 | A Many-to-Many relationship allows you to manipulate the related objects as native Python objects. 120 | 121 | ```python 122 | # Access 123 | actor.movies # returns a list of objects 124 | for m in actor.movies: 125 | print(m.title) 126 | 127 | movie.actors # returns a list of objects 128 | for a in movie.actors: 129 | print(a.first_name) 130 | 131 | # Update Related Objects 132 | m = Movie(...) 133 | a = Actor(...) 134 | 135 | db.session.add(m) 136 | db.session.add(a) 137 | 138 | actor.movies.append(m) # adds related object to the list of movies 139 | movie.actors.append(d) # adds related object to the list of actors 140 | db.session.commit() # persists changed models to the database 141 | ``` 142 | 143 | ## Running the Code 144 | 145 | Using Python 3.7+, run `pip3 install -r requirements.txt` to install the dependencies. 146 | 147 | Set the environment variable `FLASK_APP=app.py`. 148 | 149 | Commands: 150 | 151 | * `flask run` 152 | * `flask initdb` 153 | * `flask bootstrap` 154 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, jsonify, request, abort 2 | from flask_sqlalchemy import SQLAlchemy 3 | from datetime import datetime 4 | import os, json 5 | 6 | app = Flask(__name__) 7 | app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + os.path.join( 8 | app.root_path, "movies.db" 9 | ) 10 | # Suppress deprecation warning 11 | app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False 12 | db = SQLAlchemy(app) 13 | 14 | 15 | actors = db.Table( 16 | "actors", 17 | db.Column("actor_id", db.Integer, db.ForeignKey("actor.id")), 18 | db.Column("movie_id", db.Integer, db.ForeignKey("movie.id")), 19 | ) 20 | 21 | 22 | class Movie(db.Model): 23 | id = db.Column(db.Integer, primary_key=True) 24 | title = db.Column(db.String(255)) 25 | director_id = db.Column(db.Integer, db.ForeignKey("director.id")) 26 | release_date = db.Column(db.DateTime) 27 | actors = db.relationship("Actor", secondary=actors, backref="movies", lazy="select") 28 | 29 | def release_year(self): 30 | return self.release_date.strftime("%Y") 31 | 32 | def to_json(self): 33 | 34 | return { 35 | "id": self.id, 36 | "title": self.title, 37 | "director": self.director.last_name if self.director else None, 38 | "director_id": self.director.id if self.director else None, 39 | "release_date": self.release_date if self.release_date else None, 40 | "actors": [ {"id": a.id, "name": a.last_name} for a in self.actors] if self.actors else None 41 | } 42 | 43 | 44 | class Director(db.Model): 45 | id = db.Column(db.Integer, primary_key=True) 46 | first_name = db.Column(db.String(255)) 47 | last_name = db.Column(db.String(255)) 48 | movies = db.relationship( 49 | "Movie", backref=db.backref("director", lazy="joined"), lazy="select" 50 | ) 51 | guild = db.relationship( 52 | "GuildMembership", backref="director", lazy="select", uselist=False 53 | ) 54 | 55 | 56 | # m = Movie(...) 57 | # m.director.first_name 58 | 59 | 60 | class Actor(db.Model): 61 | id = db.Column(db.Integer, primary_key=True) 62 | first_name = db.Column(db.String(255)) 63 | last_name = db.Column(db.String(255)) 64 | guild = db.relationship( 65 | "GuildMembership", backref="actor", lazy="select", uselist=False 66 | ) 67 | 68 | 69 | class GuildMembership(db.Model): 70 | id = db.Column(db.Integer, primary_key=True) 71 | guild = db.Column(db.String(255)) 72 | direcotr_id = db.Column(db.Integer, db.ForeignKey("director.id")) 73 | actor_id = db.Column(db.Integer, db.ForeignKey("actor.id")) 74 | 75 | 76 | @app.route("/") 77 | def hello(): 78 | movie = db.session.query(Movie).first() 79 | return render_template("movie.html", movie=movie) 80 | 81 | 82 | @app.route("/dyn/") 83 | def dyn(): 84 | return render_template("dynamic.html") 85 | 86 | 87 | @app.route("/api/movies/", methods=['GET', 'POST']) 88 | def movies_endpoint(): 89 | if request.method == 'POST': 90 | m = Movie(title=request.form['title']) 91 | db.session.add(m) 92 | db.session.commit() 93 | return jsonify(m.to_json()), 201 94 | else: 95 | movies = db.session.query(Movie).all() 96 | return jsonify([m.to_json() for m in movies]) 97 | 98 | 99 | @app.route("/api/movies/") 100 | def movie_endpoint(m_id=None): 101 | if m_id: 102 | m = db.session.query(Movie).get(m_id) 103 | return jsonify(m.to_json()) 104 | else: 105 | return abort(404) 106 | 107 | 108 | @app.cli.command("initdb") 109 | def reset_db(): 110 | """Drops and Creates fresh database""" 111 | db.drop_all() 112 | db.create_all() 113 | 114 | print("Initialized default DB") 115 | 116 | 117 | @app.cli.command("bootstrap") 118 | def bootstrap_data(): 119 | """Populates database with data""" 120 | db.drop_all() 121 | db.create_all() 122 | 123 | m = Movie( 124 | title="Evil Dead", release_date=datetime.strptime("Oct 15 1981", "%b %d %Y") 125 | ) 126 | 127 | m2 = Movie( 128 | title="Darkman", release_date=datetime.strptime("Aug 24 1990", "%b %d %Y") 129 | ) 130 | 131 | m3 = Movie( 132 | title="The Quick and the Dead", 133 | release_date=datetime.strptime("Feb 10 1995", "%b %d %Y"), 134 | ) 135 | 136 | m4 = Movie( 137 | title="The Gift", release_date=datetime.strptime("Jan 19 2001", "%b %d %Y") 138 | ) 139 | 140 | m5 = Movie( 141 | title="Army of Darkness", 142 | release_date=datetime.strptime("Feb 19 1993", "%b %d %Y"), 143 | ) 144 | 145 | db.session.add(m) 146 | db.session.add(m2) 147 | db.session.add(m3) 148 | db.session.add(m4) 149 | db.session.add(m5) 150 | 151 | d = Director( 152 | first_name="Sam", last_name="Raimi", guild=GuildMembership(guild="Raimi DGA") 153 | ) 154 | m.director = d 155 | m2.director = d 156 | m3.director = d 157 | m4.director = d 158 | m5.director = d 159 | db.session.add(d) 160 | 161 | bruce = Actor( 162 | first_name="Bruce", 163 | last_name="Campbell", 164 | guild=GuildMembership(guild="Campbell SAG"), 165 | ) 166 | ellen = Actor( 167 | first_name="Ellen", 168 | last_name="Sandweiss", 169 | guild=GuildMembership(guild="Sandweiss SAG"), 170 | ) 171 | hal = Actor( 172 | first_name="Hal", 173 | last_name="Delrich", 174 | guild=GuildMembership(guild="Delrich SAG"), 175 | ) 176 | betsy = Actor( 177 | first_name="Betsy", last_name="Baker", guild=GuildMembership(guild="Baker SAG") 178 | ) 179 | sarah = Actor( 180 | first_name="Sarah", last_name="York", guild=GuildMembership(guild="York SAG") 181 | ) 182 | 183 | # darkman actors 184 | liam = Actor( 185 | first_name="Liam", last_name="Neeson", guild=GuildMembership(guild="Neeson SAG") 186 | ) 187 | frances = Actor( 188 | first_name="Frances", 189 | last_name="McDormand", 190 | guild=GuildMembership(guild="McDormand SAG"), 191 | ) 192 | 193 | # Quick and the Dead Actors 194 | sharon = Actor( 195 | first_name="Sharon", last_name="Stone", guild=GuildMembership(guild="Stone Sag") 196 | ) 197 | gene = Actor( 198 | first_name="Gene", 199 | last_name="Hackman", 200 | guild=GuildMembership(guild="Hackman Sag"), 201 | ) 202 | 203 | # The Gift Actors 204 | cate = Actor( 205 | first_name="Cate", 206 | last_name="Blanchett", 207 | guild=GuildMembership(guild="Blanchett Sag"), 208 | ) 209 | keanu = Actor( 210 | first_name="Keanu", 211 | last_name="Reeves", 212 | guild=GuildMembership(guild="Reeves Sag"), 213 | ) 214 | 215 | db.session.add(bruce) 216 | db.session.add(ellen) 217 | db.session.add(hal) 218 | db.session.add(betsy) 219 | db.session.add(sarah) 220 | db.session.add(liam) 221 | db.session.add(frances) 222 | db.session.add(sharon) 223 | db.session.add(gene) 224 | db.session.add(cate) 225 | db.session.add(keanu) 226 | 227 | m.actors.extend((bruce, ellen, hal, betsy, sarah)) 228 | m2.actors.extend((bruce, liam, frances)) 229 | m3.actors.extend((bruce, sharon, gene)) 230 | m4.actors.extend((bruce, cate, keanu)) 231 | m5.actors.append(bruce) 232 | 233 | db.session.commit() 234 | 235 | print("Added development dataset") 236 | 237 | 238 | if __name__ == "__main__": 239 | app.run() 240 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.0.2 2 | Flask-SQLAlchemy==2.3.2 3 | -------------------------------------------------------------------------------- /templates/dynamic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Dynamic Movie Page 8 | 9 | 10 | 11 |

Movie

12 | 13 | 14 |
15 | 16 |
17 | 18 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /templates/movie.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ movie.title }} 6 | 7 | 8 |

{{ movie.title }}

9 |
    10 |
  • {{ movie.director.first_name}} {{ movie.director.last_name }} 11 |
      12 | {% for m in movie.director.movies %} 13 | {% for a in m.actors %} 14 |
    • {{a.last_name}}: {{ a.guild.guild }}
    • 15 | {% endfor %} 16 | {% endfor %} 17 |
    18 |
  • 19 | {% for a in movie.actors %} 20 |
  • {{ a.first_name }} {{ a.last_name }} 21 |
      22 | {% for m in a.movies %} 23 |
    • {{m.director.last_name}}: {{ m.director.guild.guild }}
    • 24 | {% endfor %} 25 |
    26 |
  • 27 | {% endfor %} 28 |
29 | 30 | 31 | --------------------------------------------------------------------------------