# Flask-SQLAlchemy Quick Reference Here are some examples of useful patterns for models and queries with Flask-SQLAlchemy. ## Model Associations ### One-to-One relationship between Student and Scholarship One-to-one relationships are not especially common, and they are implemented very similarly to one-to-many relationships. The key difference is just that on the parent data table, you would set use the `uselist=False` argument to indicate that a single corresponding entry on the child table is expected (so a list is not necessary). #### On the student model ```python= class Student(db.Model): __tablename__ = "students" id = db.Column(db.Integer, primary_key=True) # other columns scholarship = db.relationship( "Scholarship", uselist=False, back_populates="student" ) ``` #### On the scholarship model ```python= class Scholarship(db.Model): __tablename__ = "scholarships" id = db.Column(db.Integer, primary_key=True) # other columns student_id = db.Column(db.Integer, db.ForeignKey("students.id")) student = db.relationship("Student", back_populates="scholarship") ``` ### One-to-Many relationship between Student and Class #### On the student model ```python= class Student(db.Model): __tablename__ = "students" id = db.Column(db.Integer, primary_key=True) # other columns classes = db.relationship("Class", back_populates="student") ``` #### On the class model ```python= class Class(db.Model): __tablename__ = "classes" id = db.Column(db.Integer, primary_key=True) # other columns student_id = db.Column(db.Integer, db.ForeignKey("students.id")) student = db.relationship("Student", back_populates="classes") ``` ### Many to Many between Student and Lesson through student_lessons helper table #### Helper table for joining The helper table doesn't need to be defined as a class since we will never need to access the entries on it directly. Its only purpose is to connect our other two models. ```python= student_lessons = db.Table( "student_lessons", db.Column( "student_id", db.Integer, db.ForeignKey("students.id"), primary_key=True ), db.Column( "lesson_id", db.Integer, db.ForeignKey("lessons.id"), primary_key=True ) ) ``` #### On the lesson model ```python= class Lesson(db.Model): __tablename__ = "lessons" id = db.Column(db.Integer, primary_key=True) # other columns students = db.relationship( "Student", secondary=student_lessons, back_populates="lessons" ) ``` #### On the student model ```python= class Student(db.Model): __tablename__ = "students" id = db.Column(db.Integer, primary_key=True) # other columns lessons = db.relationship( "Lesson", secondary=student_lessons, back_populates="students" ) ``` - note: for all of these relationships, you can use `backref` instead of `back_populates`. `backref` only has to exist on one of the two models, and it will create the relationship on its counterpart. [more info on backref vs back_populates](https://stackoverflow.com/a/59920780) ### Many-to-Many relationship between Users and Users through follows helper table Sometimes you may have a table that is joined to itself—you can still use a `db.relationship` to make it easier to access the related entries in both directions. For instance, you might have a User model, and you want to be able to access the collection of users that a given user follows as well as the user's followers. #### On helper table ```python= follows = db.Table( "follows", db.Column("follower_id", db.Integer, db.ForeignKey("users.id")), db.Column("followed_id", db.Integer, db.ForeignKey("users.id")) ) ``` #### On the User model ```python= class User(db.Model): __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) # columns followers = db.relationship( "User", secondary=follows, primaryjoin=(follows.c.follower_id == id), secondaryjoin=(follows.c.followed_id == id), backref=db.backref("follows", lazy="dynamic"), lazy="dynamic" ) # this relationship allows you to access both the collection of users # that follow a given user (with user.followers), and the collection # of users that a user follows (with user.follows) ``` ## Query Format ### For a collection of entities ```python= # get all entities — this will return a list of Student objects # the .all() method turns the result of the query from a generator # into a list students = Student.query.all() ``` ### For one entity #### by ID ```python= # get the student with the ID 2 student = Student.query.get(2) ``` #### based on another value ```python= # using filter_by jeff = Student.query.filter_by(name="Jeff").first() # using filter (these two will yield the same result) jeff = Student.query.filter(Student.name == "Jeff").first() # note: using the method `first` will get the first # entity that meets the query, or if there are none # it will return `None`. if you replaced `first` with `one` # it will raise an exception if there is more than # one result (or if there are none at all) ``` ### Simple query examples (with `filter`, `order_by`, `limit`) #### get the students whose usernames contain "sh" ```python= Student.query.filter(Student.username.ilike("%sh%")) ``` ##### get the three students whose names come last in the alphabet ```python= last_students = Student.query.order_by( Student.name.desc() ).limit(3) # case insensitive version - include import statement at top of file from sqlalchemy import func last_students = Student.query.order_by( func.lower(Student.name).desc() ).limit(3) ``` ##### get students whose enrollment status is `True` ```python= Student.query.filter(Student.enrolled.is_(True)) ``` ##### student whose enrollment status is `True` and whose usernames contain sh ```python= # by chaining filters filtered_students = Student.query.filter( Student.username.ilike("%ju%") ).filter( Student.enrolled.is_(True) ).all() # Student.query.filter( Student.username.ilike("%ju%"), Student.enrolled.is_(True) ).all() ``` ### More complicated query examples (`group_by`, `join`, `subquery`) #### Simple joining If you have included the appropriate `db.relationship` on the models, explicitly joining two models in your query may not be necessary. When you define a `db.relationship` between two models, it means that the associated entry/entries from the other table are included as a property on any instance of that model. For example, one could get a list of all the students from a specific cohort with `cohort.students`—no need to query the students table and filter by the cohort ID. ```python= # get all the students from a specific cohort cohort_a = Cohort.query.get(1) students_from_a = cohort.students # a list of Student objects # or the reverse—get the cohort object associated with a # specific student student_four = Student.query.get(4) cohort_of_student_four = student_four.cohort # a single Cohort object ``` #### Complicated queries ##### get students from all cohorts that started in August ```python= august_students = Student.query.join(Cohort).filter( Cohort.start_month == ("August") ) ``` ##### get cohorts who have more than 50 students ```python= from sqlalchemy import func # this subquery gets the number of students in each cohort student_counts = db.session.query( func.count(Student.id).label("num_students"), Student.cohort_id ).group_by(Student.cohort_id).subquery() # by joining the previous subquery, we can selectively filter # our cohorts based on size large_cohorts = Cohort.query.join( student_counts, Cohort.id == student_counts.c.cohort_id ).filter(student_counts.c.num_students > 50).all() ``` ##### get the average scores of the currently enrolled students ```python= from sqlalchemy.sql import func student_avgs = db.session.query( Student, func.avg(Score.percent_correct).label("average_score") ).join(Score).group_by(Student.id) # be aware that this returns a collection of tuples, not Student objects ``` ## Creating and updating data Note on seeding: Unlike the workflow for seeding that we used with Sequelize, seeding the database in our SQLAlchemy projects looks just like adding entries to our database using the ORM. In the python group project skeleton, we're using a custom command line function for seeding (those `flask seed all`/`flask seed undo` commands are not built into Flask anywhere—Flask just allows you to define your own functions that run on the command line). Your seed files can add data to the database just like on your routes. So the examples below could be used in seeders or in routes—it looks the same. #### adding new data to database ```python= # create a new entry august = Cohort(start_month="August") # setting up one-to-many associations # # now lets create some students who belong to specific cohorts # option 1: if you already have the id student1 = Student(name="Laramie", cohort_id=3, enrolled=True) # option 2: use the Cohort object to establish the relationship student2 = Student(name="Marni", cohort=august, enrolled=True) # setting up many-to-many relationships # # students can belong to many teams, teams can have many students team1 = Team(color="blue", name="cool kids") # you can add a student to a team by appending to the Student object student1.teams.append(team1) # or by appending to the Team object team1.students.append(student2) # finally, add all your new entries and commit db.session.add(august) db.session.add(student1) db.session.add(student2) db.session.add(team1) db.session.commit() ``` #### updating existing data ```python= # select your entry from the database - for example, by ID score = Score.query.get(12) # change the relevant information on that item score.percent_correct = 75 # # add and commit db.session.add(score) db.session.commit() ``` ## Sending data as JSON ### Dictionaries Recall that, in JavaScript, we typically use objects for two purposes: as a collection of key/value pairs, and also as a collection of methods and properties. In Python, key/value pairs in dictionaries are _not_ the same as attributes on objects. Dictionaries consist of key/value pairs. Accessing the value associated with a particular key uses bracket notation (`dictionary["my_key"] = some_value`). Keys can be any _hashable_ type (i.e. immutable types: strings, booleans integers, and tuples). Accessing the methods and properties on objects is different. You can access these properties and methods using dot notation (`my_object.some_property = data`). When you get entries from your database using SQLAlchemy, these objects are instances of your Model classes, not dictionaries. Python dictionaries can easily be serialized into JSON format (as can strings, numbers, booleans, `None`, lists, and tuples). Objects from your Model classes cannot. If you want to send an entity from your database to the frontend of your application, you will have to convert it to a dictionary. ### `to_dict()` methods You may find it helpful to define methods on your database model classes for converting objects from that class into dictionaries. Each class would need a unique `to_dict` method which captures the relevent properties. You could even create multiple dictionary conversion methods for the same class that return different information (for example, separate methods for a summary versus a detail view). That said, you are not required to write these methods—you could instead access the values of interest and create a dictionary directly on the route where you are returning the data. However, since you will often need to send the same data from these classes in multiple routes. In that case, you can make your code more _DRY_ (Don't Repeat Yourself) by making reusable `to_dict` methods. ```python= class Student(db.Model): __tablename__ = "students" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), nullable=False) caffeinated = db.Column(db.Boolean, default=True) cohort_id = db.Column(db.Integer, db.ForeignKey("cohorts.id")) cohort = db.relationship("Cohort", back_populates="students") def to_dict(self): return { "id": self.id, "name": self.name, "caffeinated": self.caffeinated, "cohort_id": self.cohort_id } class Cohort(db.Model): __tablename__ = "cohorts" id = db.Column(db.Integer, primary_key=True) start_month = db.Column(db.String(15)) nullable=False) ptm = db.Column(db.String(50), nullable=False) ptm_tagline = db.Column(db.String(1000), nullable=False) students = db.relationship("Student", back_populates="cohort") def to_dict(self): return { "id": self.id, "start_month": self.start_month, "ptm": self.ptm, "ptm_tagline": self.ptm_tagline, } ``` Here are some example routes using the above methods. ```python= @app.route("/students") def get_students(): students = Student.query.all() return {"students": [student.to_dict() for user in users]} @app.route("/students/<int:id>") def get_user_by_id(id): student = Student.query.get(1) return student.to_dict() @app.route("/cohorts/<int:id>") def get_cohort(id): cohort = Cohort.query.get(id) return cohort.to_dict() ```