0

I'll acknowledge that this is a strange question before I ask it. I'm wondering if it's possible to replicate Flask / SQL Alchemy class methods using raw SQL instead of using the methods themselves?

Long story short, my teammates and I are taking a database design course, and we're now in the implementation phase where we are coding the app that is based on our DB schema design. We want to keep things simple, so we opted for using Flask in Python. We're following the Flask Mega Tutorial, which is a kickass-tic tutorial explaining how to build a basic site like we're doing. We've just completed Chapter 5: User Logins, and are moving on.

In the app/routes.py script, the tutorial does something to grab the user information. Here's the example login route for the example app:

from flask_login import current_user, login_user
from app.models import User

# ...

@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        return redirect(url_for('index'))
    return render_template('login.html', title='Sign In', form=form)

The line user = User.query.filter_by(username=form.username.data).first() is what I'm interested in. Basically, that line instantiates the User class, which is a database model from SQL Alchemy, and grabs information about the user from the email address they entered. Calling those methods generates a SQL statement like the following:

SELECT `User`.`userID` AS `User_userID`,
       `User`.user_email AS `User_user_email`,
       `User`.user_first_name AS `User_user_first_name`,
       `User`.user_last_name AS `User_user_last_name`,
       `User`.user_password AS `User_user_password`
FROM `User`
WHERE `User`.user_email = 'test@test.com'
LIMIT 1

And also some information about the user variable itself:

>>> print(type(user))
<class 'myapp.models.User'>

>>> pp(user.__dict__)
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x7f5a026a8438>,
 'userID': 1,
 'user_email': 'test@test.com',
 'user_first_name': 'SomeFirstName',
 'user_last_name': 'SomeLastName',
 'user_password': 'somepassword'}

On our project, we're not supposed to be using generated SQL statements like the one that comes from calling query.filter_by(username=form.username.data).first() on the instantiated User class; we should be writing the raw SQL ourselves, which normally doesn't make sense, but in our case it does.

Is this possible?

Scott Crooks
  • 1,523
  • 4
  • 24
  • 39
  • 1
    *but in our case it does.*: **Why** does it make sense? Yes, you can execute raw queries. You can then create your own instance of `User` from the data. But **why would you want to do that**? – Martijn Pieters Oct 23 '18 at 11:46
  • Note that `User.query.filter_by(username=form.username.data).first()` issues a very, very simple `SELECT` query with a `LIMIT 1`. Using a manual 'handcrafted' query is not going to improve on that query. Everything SQLAlchemy does is introspectable and adjustable, you can build that same query with the [core API](https://docs.sqlalchemy.org/en/latest/core/index.html) or just pass raw SQL to the database connection. But that a) increases development cost, and b) opens you up to security problems if you don't handle untrusted data carefully. – Martijn Pieters Oct 23 '18 at 11:49
  • @MartijnPieters Ha, I told you it was strange :D. Normally you wouldn't do that, and I know this. I'm worried that the graders for the project will look harshly on us using class methods that auto generate SQL queries, no matter how simple. – Scott Crooks Oct 23 '18 at 11:50
  • So, TLDR: All the methods, such as `filter_by`, are well documented, and can be replicated by using the SQLAlchemy core API or raw SQL strings. *You just really don't want to do that*. – Martijn Pieters Oct 23 '18 at 11:50
  • @MartijnPieters You're totally correct, and normally this would be a very foolish thing to do. Since this is for a DB design class, I'm simply covering my bases in case we're accused of using tools that autogenerate SQL for us. – Scott Crooks Oct 23 '18 at 11:51
  • 1
    Then go talk to your professor or TA to see what the requirements really are. If you are supposed to write your own SQL, then *don't use SQLAlchemy to begin with*. – Martijn Pieters Oct 23 '18 at 11:53
  • If you need to write a login system for a web site for a db class, then don't use Flask-Login, and just write queries directly and don't yet use an ORM. Otherwise, go the whole nine yards and trust SQLAlchemy. It can be trusted to do its job. – Martijn Pieters Oct 23 '18 at 11:54
  • As an aside, if you really *really* **really** want to take this route (you should not), then sure, you sort of can; see [`Query.from_statement()`](https://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query.from_statement), but I doubt that is really what you meant, or are meant to do. – Ilja Everilä Oct 23 '18 at 11:54
  • @MartijnPieters Well, I wasn't married to the idea of using SQLAlchemy, but like I said, I was following the tutorial, and the ways that they use to implement things like logging in assume you're using SQLAlchemy. I'm very much a beginner with Flask (obviously), and it was easier to start with that, and get _some_ functionality built in at first. – Scott Crooks Oct 23 '18 at 11:56
  • @IljaEverilä Thank you! – Scott Crooks Oct 23 '18 at 11:56
  • 1
    @ScottCrooks: then perhaps you need to look for a different tutorial. The Mega tutorial is teaching best practices but if those don't fit your class requirements, find something else. You are asking if a tool specifically designed to handle SQL for you can be bypassed to not handle SQL for you. Yes, it can, but then why use the tool to begin with? – Martijn Pieters Oct 23 '18 at 11:57
  • "But then why use the tool to begin with?" Because in the end, we're not interested in the website itself; it needs to work, that's all. Maybe I should re-phrase my question: If I'm using DB-API drivers directly, what's the best way to integrate that with libraries like `flask_login` so you get the best of both worlds? Namely, being able to run raw SQL outside of SQLAlchemy, while also using being able to use methods like `login_user`, and ensuring that the objects you're returning with the raw SQL are in a format suited for `flask_login` ? – Scott Crooks Oct 23 '18 at 12:03
  • @ScottCrooks: Flask-Login is not tied to an ORM at all. Just implement the methods it needs and pass around instances. How those instances are loaded, Flask-Login doesn't care about. – Martijn Pieters Oct 23 '18 at 12:22

1 Answers1

8

First of all: Talk to your professor or TA. You will save yourself time by not making assumptions about something so major. If the goal of the class is to think about database schema design then using an ORM is probably fine. If you need to write your own SQL, then don't use an ORM to begin with.

To answer the technical question: yes, you can use SQLAlchemy purely as a database connection pool, as a tool to create valid SQL statements from Python objects, and as a full-fledged ORM, and every gradation in between.

For example, using the ORM layer, you can tell a Query object to not generate the SQL for you but instead take text. This is covered in the SQLAlchemy ORM tutorial under the Using Textual SQL section:

Literal strings can be used flexibly with Query, by specifying their use with the text() construct, which is accepted by most applicable methods

For your login example, querying for just the password could look like this:

user = User.query.from_statement(
    db.text("SELECT * FROM User where user_email=:email LIMIT 1")
).params(email=form.username.data).first()

if user is None or user.check_password(form.password.data):
    # ...

You could also read up on the SQL Expression API (the core API of the SQLAlchemy library) to build queries using Python code; the relationship between Python objects and resulting query is pretty much one on one; you generally would first produce a model of your tables and then build your SQL from there, or you can use literals:

s = select([
    literal_column("User.password", String)
]).where(
    literal_column("User.user_email") == form.username.data
).select_from(table("User")).limit(1)

and execute such objects with the Session.execute() method

results = db.session.execute(s)

If you wanted to really shoot yourself in the foot, you can pass strings to db.session.execute() directly too:

results = db.session.execute("""
    SELECT user_password FROM User where user_email=:email LIMIT 1
    """, {'email': form.username.data})

Just know that Session.execute() returns a ResultProxy() instance, not ORM instances.

Also, know that Flask-Login doesn't require you to use an ORM. As the project documentation states:

However, it does not:

  • Impose a particular database or other storage method on you. You are entirely in charge of how the user is loaded.

So you could just create a subclass of UserMixin that you instantiate each time you queried the database, manually.

class User(flask_login.UserMixin):
    def __init__(self, id):    # add more attributes as needed
        self.id = id

@login_manager.user_loader
def load_user(user_id):
    # perhaps query the database to confirm the user id exists and
    # load more info, but all you basically need is:
    return User(user_id)

# on login, use

user = User(id_of_user_just_logged_in)
login_user(user)

That's it. The extension wants to see instances that implement 4 basic methods, and the UserMixin class provides all of those and you only need to provide the id attribute. How you validate user ids and handle login is up to you.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Thank you, this actually does answer my question. I didn't mean to offend anyone with my shitty request, I know it's weird (please forgive me), but I'm just needing to potentially find a workaround. For the record, I have asked my TAs for clarification on this :D – Scott Crooks Oct 23 '18 at 12:22
  • You didn't offend, don't worry. But you are really making it harder on yourself by using an ORM if you need to write your own queries. – Martijn Pieters Oct 23 '18 at 12:24
  • @ScottCrooks: I added a short section on how to use Flask-Login. It's *really really simple*. – Martijn Pieters Oct 23 '18 at 12:31
  • @IljaEverilä: ick, yes, that's totally wrong. That's the result of me being interrupted somewhere and coming back with incomplete state. I'll fix that. – Martijn Pieters Oct 23 '18 at 13:38