Flask/SQLAlchemy - error creating models with relationships - python

I created a table users with this mysql statement in the shell:
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(40) NOT NULL,
`email` varchar(120) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB;
and a table posts with this statement:
CREATE TABLE `posts` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`text` varchar(140) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `fk_user_id` (`user_id`),
CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;
I want to use these tables in my Flask app. I created a foreign key connecting posts to users. It's supposed to be a one-to-many relationship with the user having many posts.
In my python code I have the following models:
class Users(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(40), unique=True)
email = db.Column(db.String(120), unique=True)
posts = db.relationship('Posts', backref='user', lazy='dynamic')
def __init__(self, username, email):
self.username = username
self.email = email
def __repr__(self):
return '<User {}>'.format(self.username)
class Posts(db.Model):
id = db.Column(db.Integer, primary_key=True)
text = db.Column(db.String(140))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
When I try running my Flask app however, I keep getting the error:
sqlalchemy.exc.InvalidRequestError: One or more mappers failed to
initialize - can't proceed with initialization of other mappers.
Original exception was: Could not determine join condition between
parent/child tables on relationship Users.posts - there are no foreign
keys linking these tables.
But I created the foreign key connecting posts to users in the mysql statement. When I take out the line posts = db.relationship('Posts', backref='user', lazy='dynamic'), everything works just fine, so the issue lies in that piece of code. What part of my setup is wrong?

Not sure what's wrong with the initial creation of the tables, but i would use sqlalchemy's create_all. See here and in the docs. This way you know any issue is not a matter of different definitions of the initial SQL creation query vs the models' definitions. Stick with SQLAlchemy all the way.
Your model definitions seem OK, except for the backref. I compared it with my models definitions, and my backrefs are always the table names. You have 'user' as a backref, which doesn't really appear anywhere else in the code, so i'm assuming that's where things go wrong.
To forcibly define table names add tablename to the class definition; e.g.
class User(db.model):
__tablename__ = 'Users'
id = db.Column(..
.. more fields ..
posts = db.relationship('Post', backref='Users', lazy='dynamic')
I use the convention of the classes in singular, and table names in plural, which makes it easier to see how things are defined, what a backref points to, etc. For example, note that the above relationship points to the class Post, while the backref to the table Users. Now there's no ambivalence in the definitions.

Related

flask-sqlalchemy : how to manage SQL Views?

I am discovering Flask-SqlAlchemy, and currently facing an issue with the views : how can I properly manage them in the model of SqlAlchemy? Can I differenciate them in the code ?
For example if I have these two SQL Table :
CREATE TABLE Account (
[username] VARCHAR(13) NOT NULL,
[email] VARCHAR(255) NULL,
CONSTRAINT [PK_Account] PRIMARY KEY CLUSTERED ([username] ASC))
CREATE TABLE Address (
[username] VARCHAR(13) NOT NULL,
[address] CHAR(58) NOT NULL,
CONSTRAINT [PK_Address] PRIMARY KEY CLUSTERED ([username] ASC, [address] ASC))
and the view
CREATE VIEW [AccountView] AS
SELECT [Account].[username]
,[Address].[address]
FROM [Account]
INNER JOIN [Address] ON [Account].[username] = [Address].[username]
In python I have build the two tables like :
class Account(db.Model):
username = db.Column(db.String(13), primary_key=True)
email = db.Column(db.String(255), nullable=True)
class Address(db.Model):
username = db.Column(db.String(13), primary_key=True)
address = db.Column(db.String(255), primary_key=True)
what is the best solution to create the view model? I have try :
class AccountView(db.Model):
username = db.Column(db.String(13), primary_key=True)
address = db.Column(db.String(255), primary_key=True)
But here I don't see how SqlAlchemy can know it refer to a view, and so it is readonly and also should be update if I update Account or Address table.
Any advice here? I am working the right way?

How can I insert multiple time a many-to-many relation with flask-sqlalchemy?

I am using flask-sqlalchemy in order to create a RestAPI. The idea is to store playlists with one or several medias inside it. To link these two, I am using a many-to-many relationship.
Sometimes, a same media will be inserted twice inside a playlist but it fails because the relationship table already has the entry.
class Media(db.Model):
__tablename__ = 'media'
id = db.Column(db.Integer, primary_key=True)
medias = db.Table('media_playlist',
db.Column('media_id', db.Integer, db.ForeignKey('media.id'), primary_key=True),
db.Column('playlist_id', db.Integer, db.ForeignKey('playlist.id'), primary_key=True),
)
class Playlist(db.Model):
__tablename__ = 'playlist'
id = db.Column(db.Integer, primary_key=True)
medias = db.relationship('Media',
secondary=medias,
lazy='subquery',
backref=db.backref('playlists', lazy=True))
The linked error :
UNIQUE constraint failed: media_playlist.media_id, media_playlist.playlist_id
How would you do to store several times the same link ?
Thank you,
Rom
Why are you setting primary_key=True for foreign key columns? SQLAlchemy
Since you are making a column foreign key it means it is going to store multiple times the same value, but when you make it primary key it means it needs to be unique and now you have this error fails UNIQUE constraint. If you want it to be indexed, you do not have to do anything as DB is indexing foreign keys by default.

Flask-admin: How to change foreign key and update related records

I have two tables with a foreign key relationship and I am using flask-admin as admin GUI.
In some cases I need to modify the foreign-key. In this case all existing Records of the related table should be updated (with new foreign key). How would you implement this functionality?
I tried the on_model_change function of flask-admin. Although I run into the problem, that I only get the updated key from the function(?) -> (I need the old key to find the existing assessment-records and update them). Additionally I'm not able to commit the new key unless the Assessment records have been updated (foreign key constraint would not be met).
Can I achieve this functionality within the flask-admin GUI?
class Assessment(db.Model):
__tablename__ = "assesment_table"
id = db.Column(db.Integer, primary_key=True)
psr_id = db.Column(db.String(40), db.ForeignKey("psr_object_table.psr_id"))
class Unit(db.Model):
__tablename__ = "units_table"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
psr_id = db.Column(db.String(40), unique=True)
#needed for Flask-Admin view
class UnitView(ModelView):
def on_model_change(self, form, model, is_created):
#get old_key
#query and update db with new foreign key
#commit everything
Any help is highly appreciated. Thank you
Use database side cascades:
class Assessment(db.Model):
...
psr_id = db.Column(db.String(40), db.ForeignKey("psr_object_table.psr_id",
onupdate="CASCADE"))

Flask-Migration with one-to-many relationship

I have a working blog system. I want to add it to comment system. I completed migrations with post model with id, title and body.
Now I add comments and create new model named Comment. When I run migrations:
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.env] No changes in schema detected.
from run import db
class Post(db.Model):
__tablename__ = 'blog.post'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String, nullable=False)
body = db.Column(db.Text, nullable=False)
comments = db.relationship('Comment', backref='blog.post')
from run import db
class Comment(db.Model):
__tablename__ = 'blog.comment'
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text, nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey('blog.post.id'), nullable=False)
I dont know what is wrong with my code. I get relationship from documentation and edit it. There aren't any comment table in db before.
EDIT 1:
I call comment inside run like below:
from model.comment import Comment
After that I can create migration but migration got error like below:
sqlalchemy.exc.InternalError: (pymysql.err.InternalError) (1005, 'Can\'t create table blog_db.blog.comment (errno: 150 "Foreign key constraint is incorrectly formed")') [SQL: '\nCREATE TABLE blog.comment (\n\tid INTEGER NOT NULL AUTO_INCREMENT, \n\tname VARCHAR(255) NOT NULL, \n\tbody TEXT NOT NULL, \n\tcreated DATETIME DEFAULT now(), \n\tstatus INTEGER NOT NULL, \n\tpost_id INTEGER NOT NULL, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(post_id) REFERENCES blog.post (id)\n)\n\n'] (Background on this error at: http://sqlalche.me/e/2j85)
The error Foreign key constraint is incorrectly formed happens because the primary key and the foreign key have different types.
In the Post model you defined the id column as:
id = db.Column(db.Integer, primary_key=True)
But the post_id foreign key that you added in the Comment model is defined differently:
post_id = db.Column(db.Integer, db.ForeignKey('blog.post.id'), nullable=False)
I think if you remove the nullable clause from the foreign key you'll get the migration accepted.

How can "on delete restrict" be used in a model?

I have figured out how to use "on delete cascade", but am unclear on how to do "on delete restrict" constraints. What I would like to achieve is to not be able to delete a parent that has a child or children records.
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
fullname = db.Column(db.String)
password = db.Column(db.String)
posts = db.relationship("Post", backref='user', cascade="all, delete, delete-orphan")
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String, nullable=False)
description = db.Column(db.String, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id', onupdate="CASCADE", ondelete="CASCADE"))
CREATE TABLE posts (
id INTEGER NOT NULL,
title VARCHAR NOT NULL,
description VARCHAR NOT NULL,
user_id INTEGER,
PRIMARY KEY (id),
FOREIGN KEY(user_id) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE TABLE users (
id INTEGER NOT NULL,
name VARCHAR,
fullname VARCHAR,
password VARCHAR,
PRIMARY KEY (id)
);
Replacing "delete" with "restrict" just allows me to delete the parents and retain the orphaned rows.
How do I properly specify the "restrict" behavior?
SQLite does not support foreign key constraints by default. They must be enabled at compile time and enabled at run time, otherwise they are silently ignored.
You can check if foreign keys are enabled by running pragma foreign_keys in a sqlite shell. If it returns 1, they are enabled. If it returns 0, they are disabled. If it does not return anything, they are not supported and sqlite must be recompiled to support them.
If foreign keys are disabled, you can instruct SQLAlchemy to enable them when creating connections.
from sqlalchemy import event
from sqlalchemy.engine import Engine
from sqlite3 import Connection as SQLite3Connection
#event.listens_for(Engine, "connect")
def _set_sqlite_pragma(dbapi_connection, connection_record):
if isinstance(dbapi_connection, SQLite3Connection):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON;")
cursor.close()
source: https://stackoverflow.com/a/15542046/400617
SQL foreign key cascades are different than SQLAlchemy's relationship cascades (scroll down that second link to see a detailed comparison of the two). SQLAlchemy has no "restrict" cascade. You should specify that on the foreign key. Be sure to recreate/migrate the database if you change a foreign key that already exists.
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey(User.id, ondelete='RESTRICT'), nullable=False)

Categories

Resources