Saving duplicate entries in a Flask SQLAlchemy association table - python

I am designing a schema to audit which user has which monitors.
For a given audit, we have users. And each user can have zero or many monitors.
Also, a user can have many of the same monitor.
Here is my User class:
class User(db.Model):
id = db.Column(db.Integer, db.Sequence('user_id_seq'),
autoincrement=True, primary_key=True)
login = db.Column(db.String(140), unique=True)
def __init__(self, login):
self.login = login
def __repr__(self):
return '<User %r>' % self.login
Here is my Audit class:
class Audit(db.Model):
id = db.Column(db.Integer, db.Sequence('audit_id_seq'),
autoincrement=True, primary_key=True)
start_date = db.Column(db.DateTime)
end_date = db.Column(db.DateTime)
def __init__(self):
self.start_date = datetime.now()
def __repr__(self):
return '<Audit %r>' % self.id
Here is my Monitor class:
class Monitor(db.Model):
id = db.Column(db.Integer, db.Sequence('monitor_id_seq'),
autoincrement=True, primary_key=True)
description = db.Column(db.String(140), unique=True)
def __init__(self, description):
self.description = description
def __repr__(self):
return '<Monitor %r>' % self.description
Here is my UserAudit class:
class UserAudit(db.Model):
id = db.Column(db.Integer, db.Sequence('user_audit_id_seq'),
autoincrement=True, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
user = db.relationship('user',
backref=db.backref('user_audits', lazy='dynamic'))
audit_id = db.Column(db.Integer, db.ForeignKey('audit.id'))
audit = db.relationship('Audit')
monitors = db.relationship('Monitor',
secondary=userAuditMonitor.association_table,
backref='user_audit_monitors')
def __init__(self, user, audit):
self.user = user
self.audit = audit
def __repr__(self):
return '<UserAudit %r>' % self.id
And finally, here is my UserAuditMonitor class which glues the whole thing together:
class UserAuditMonitor():
association_table = Table('user_audit_monitor', db.Model.metadata,
Column('user_audit_id', db.Integer, db.ForeignKey('user_audit.id')),
Column('monitor_id', db.Integer, db.ForeignKey('monitor.id'))
)
The above association table is super useful as I can simply use the .append() method and add more monitors to a UserAudit sqlalchemy object.
Example:
>>> u = UserAudit.query.get(1)
>>> monitors = [Monitor.query.get(3), Monitor.query.get(4)]
>>> u.append(monitors)
>>> print u.monitors
[<Monitor u'HP'>, <Monitor u'Dell'>]
>>> db.session.commit()
>>> print u.monitors
[<Monitor u'HP'>, <Monitor u'Dell'>]
However, if I try and append more than one of the same monitors to a UserAudit object, only one monitor gets stored.
Example:
>>> u = UserAudit.query.get(2)
>>> monitors = [Monitor.query.get(3), Monitor.query.get(3)]
>>> u.append(monitors)
>>> print u.monitors
[<Monitor u'HP'>, <Monitor u'HP'>]
>>> db.session.commit()
>>> print u.monitors
[<Monitor u'HP'>] # ONLY ONE MONITOR GOT SAVED HERE!!!
How do I configure the UserAuditMonitor class to save duplicates?
Thanks!

My guess is that UserAuditMonitor doesn't have an explicitly defined primary key, so SQLA is using user_audit_id x monitor_id as the key and de-duplicates the relationship. Perhaps try adding an autoincrement primary key to the association_table?

Related

Flask-Admin: Change sort order of inline_models?

In Flask-Admin, is there any way to control the order of the list generated by inline_models? It seems to be coming out in database order, i.e. ordered by the ID primary key.
That is, if I have an Author that has_many Books, and my AuthorModelView class has inline_models = (Books,), the books are always ordered by book_id. Passing column_default_sort to the inline model, to try to sort by (say) title or date_purchased, has no effect. Is there any way to handle this?
Specify the order_by parameter when specifying the relationships, see docs. See note at the end if you want to sort by a specific field at runtime.
Example of model declarations for Author -> Books. Here we are ordering on the book title field ascending - order_by='Book.title.asc()' :
class Author(db.Model):
__tablename__ = 'authors'
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.Text(length=255), nullable=False)
last_name = db.Column(db.Text(length=255), nullable=False)
books = db.relationship("Book", order_by='Book.title.asc()', cascade="all,delete-orphan", backref=db.backref('author'))
def __str__(self):
return f"ID: {self.id}; First Name: {self.first_name}; Last Name: {self.last_name}"
class Book(db.Model):
__tablename__ = 'books'
id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey('authors.id'), nullable=False, index=True)
title = db.Column(db.Text(length=255), nullable=False)
def __str__(self):
return f"ID: {self.id}; Title: {self.title}; Author ID: {self.author_id}"
Single file full example:
from faker import Faker
import click
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin
from flask_admin.contrib import sqla
db = SQLAlchemy()
class Author(db.Model):
__tablename__ = 'authors'
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.Text(length=255), nullable=False)
last_name = db.Column(db.Text(length=255), nullable=False)
books = db.relationship("Book", order_by='Book.title.asc()', cascade="all,delete-orphan", backref=db.backref('author'))
def __str__(self):
return f"ID: {self.id}; First Name: {self.first_name}; Last Name: {self.last_name}"
class Book(db.Model):
__tablename__ = 'books'
id = db.Column(db.Integer, primary_key=True)
author_id = db.Column(db.Integer, db.ForeignKey('authors.id'), nullable=False, index=True)
title = db.Column(db.Text(length=255), nullable=False)
def __str__(self):
return f"ID: {self.id}; Title: {self.title}; Author ID: {self.author_id}"
app = Flask(__name__)
app.config['SECRET_KEY'] = '123456790'
app.config['SQLALCHEMY_ECHO'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///sample.sqlite'
db.init_app(app)
#app.cli.command('create-database', short_help='Create Authors database')
#click.option('--count', default=100, help='Number of authors (default 100)')
def create_database(count):
"""
Create database with "count" authors
"""
db.drop_all()
db.create_all()
_faker = Faker()
for _ in range(0, count):
_author = Author(
first_name=_faker.first_name(),
last_name=_faker.last_name(),
)
db.session.add(_author)
for _ in range(0, _faker.pyint(1, 20)):
_book = Book(
title=_faker.sentence(),
author=_author
)
db.session.add(_book)
db.session.commit()
class AuthorView(sqla.ModelView):
# default sort: last_name ascending
column_default_sort = ('last_name', False)
inline_models = (Book,)
# Flask views
#app.route('/')
def index():
return 'Click me to get to Admin!'
admin = Admin(app, template_mode="bootstrap3")
admin.add_view(AuthorView(Author, db.session))
if __name__ == '__main__':
app.run()
Run the following command to initialize an SQLite DB.
flask create-database --count 100
If you want to change the sort field at runtime override the view's get_one() method and use Python to sort the instrumented list directly. For example, sorting by ISBN field instead of title:
class Author2View(sqla.ModelView):
def get_one(self, id):
_author = super().get_one(id)
_author.books = sorted(_author.books, key=lambda book: book.isbn)
return _author
# default sort: last_name ascending
column_default_sort = ('last_name', False)
inline_models = (Book,)
admin.add_view(Author2View(Author, db.session, name="Author 2", endpoint='author-2'))

When do relationships / backrefs become usable

I'm having a difficult time understanding how relationships / backrefs work.
I seem to be missing the point regarding how to make them 'live' so I keep getting errors like:
'NoneType' object has no attribute 'decid'.
These tables are 1 to 1 forming a heirachy.
I have an SQLite db and the following classes defined.
class Person(DECLARATIVE_BASE):
__tablename__ = 'person'
__table_args__ = ({'sqlite_autoincrement': True})
idperson = Column(INTEGER, autoincrement=True,
primary_key=True, nullable=False)
lastname = Column(VARCHAR(45), index=True, nullable=False)
firstname = Column(VARCHAR(45), index=True, nullable=False)
def __repr__(self):
return self.__str__()
def __str__(self):
return "<Person(%(idperson)s)>" % self.__dict__
class Schoolmember(DECLARATIVE_BASE):
__tablename__ = 'schoolmember'
person_id = Column(INTEGER, ForeignKey("person.idperson"),
index=True, primary_key=True, nullable=False)
decid = Column(VARCHAR(45), unique=True, nullable=False)
type = Column(VARCHAR(20), nullable=False)
person = relationship("Person", foreign_keys=[person_id],
backref=backref("schoolmember", uselist=False))
def __repr__(self):
return self.__str__()
def __str__(self):
return "<Schoolmember(%(person_id)s)>" % self.__dict__
class Student(DECLARATIVE_BASE):
__tablename__ = 'student'
person_id = Column(INTEGER, ForeignKey("schoolmember.person_id"),
autoincrement=False, primary_key=True, nullable=False)
studentnum = Column(VARCHAR(30), unique=True, nullable=False)
year = Column(INTEGER, nullable=False)
graduated = Column(BOOLEAN, default=0, nullable=False)
schoolmember = relationship("Schoolmember", foreign_keys=[person_id],
backref=backref("student", uselist=False))
def __repr__(self):
return self.__str__()
def __str__(self):
return "<Student(%(person_id)s)>" % self.__dict__
I don't understand why here I can't access schoolmember from Student.
I was expecting declaritive to cascade up the relationships.
newstu = Student()
newstu.studentnum = '3456'
newstu.schoolmember.decid = 'fred.frog' # Error, 'NoneType' object
The following works, but only by stomping over the relationships defined in the class?
Do I need to do it this way?
s = Schoolmember(decid = 'fred.frog')
newstu = Student(schoolmember=s, studentnum='3456')
I don't 'get' what's is going on. I'm trying to understand the principals involved so I don't get bamboozled by the next problem
The reason your first example doesn't work is because when you initialize student there is no schoolmember associated with it. SQLAlchemy doesn't automatically generate this for you. If you wanted to, every time you create a Student object for it to automatically create a new schoolmember, you could do that inside of an __init__. In addition, if you wanted it to work you could do something like:
student = Student()
schoolmember = Schoolmember()
student.studentnum = 3456
student.schoolmember = schoolmember
student.schoolmember.decid = 'fred.frog'
An __init__ method could help also, if this is behavior you want every time.
def __init__(self, studentnum=None, year=None, graduated=None, schoolmember=None):
# If no schooolmember was provided, automatically generate one.
self.schoolmember = schoolmember or Schoolmember()
Hope this helps.

Flask-SQLAlchemy filter_by and follow multiple backrefs

I would like to extend the schema from the Flask-SQLAlchemy Quickstart Simple Relationships example and add a class called Editor.
My schema now looks like this:
from datetime import datetime
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(80))
body = db.Column(db.Text)
pub_date = db.Column(db.DateTime)
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
category = db.relationship('Category',
backref=db.backref('posts', lazy='dynamic'))
def __init__(self, title, body, category, pub_date=None):
self.title = title
self.body = body
if pub_date is None:
pub_date = datetime.utcnow()
self.pub_date = pub_date
self.category = category
def __repr__(self):
return '<Post %r>' % self.title
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
editor_id = db.Column(db.Integer, db.ForeignKey('editor.id'))
editor = db.relationship('Editor',
backref=db.backref('categories', lazy='dynamic'))
def __init__(self, name, editor):
self.name = name
self.editor = editor
def __repr__(self):
return '<Category %r>' % self.name
class Editor(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
def __init__(self, name):
self.name = name
def __repr__(self):
return '<Editor %r>' % self.name
The queries from the example on the quickstart, in addition to the query for categories by editor, all work fine:
>>> py = Category('Python')
>>> p = Post('Hello Python!', 'Python is pretty cool', py)
>>> e = Editor('dude')
>>> db.session.add(py)
>>> db.session.add(p)
>>> db.session.add(e)
>>> Post.query.filter_by(category=py).all()
[<Post 'Hello Python!'>]
>>> Category.query.filter_by(editor=e).all()
[<Category 'Python'>]
However, I would also like to find all posts from a specific editor. I tried using this query:
>>> Post.query.filter_by(category.editor=e).all()
But I got this error:
SyntaxError: keyword can't be an expression
How do I get posts by editor?
You have to join the Post and Category first, and i made some small corrections on your tests:
db.create_all()
e = Editor('Dude')
py = Category('Python', e)
p = Post('Hello Python!', 'Python is pretty cool', py)
db.session.add(py)
db.session.add(p)
db.session.add(e)
print Post.query.filter_by(category=py).all()
print Category.query.filter_by(editor=e).all()
print Post.query.join(Category).filter_by(editor=e).all()

One-to-many Flask | SQLAlchemy

I am trying to create a one-to-many relationship using Flask and SQLAlchemy.
I want the one-to-many relationship to be as so:
"For any single movie, there can be multiple characters"
Here it what I have so far, but it is saving in my DB as one-to-one right now. (One movie to one character, saving multiple times in DB for multiple characters)
class Movie(db.Model):
__tablename__ = "movies"
id = db.Column('movies_id', db.Integer, primary_key=True)
movie_type = db.Column('movie_type', db.Text())
def __init__(self, movie_type):
self.movie_type = movie_type
def __repr__(self):
return '<Movie %r>' % self.id
class Character(db.Model):
__tablename__ = "characters"
id = db.Column('character_id', db.Integer, primary_key=True)
character_description = db.Column('character_description', db.Text())
movie_id = db.Column(db.Integer, db.ForeignKey('movies.movie_id'))
movie = db.relationship('Movie', backref='characters', lazy='dynamic')
def __init__(self, character_description, movie):
self.character_description = character_description
self.movie = movie
def __repr__(self):
return '<Character %r>' % self.id
I am saving into the DB like this:
movie = models.movie(movie_type)
character = models.Character(character_description, movie)
db.session.add(movie)
db.session.add(character)
db.session.commit()
The end goal is to be able to look up what movie a character is in. If you could also help me out with that query, that would be great!
Thanks ahead of time.
Well, I think you miss the characters relations in the movie + the insert was not totaly right.
There is also little details that you have to be carefull. Why id of movie is movieS_id and id of character is character_id ?
Also, the name of the column is the same as the name of the variable if not specified.
For example you can do that:
character_description = db.Column(db.Text())
Anyway, without changing this details, you can try this:
class Movie(db.Model):
__tablename__ = "movies"
id = db.Column('movies_id', db.Integer, primary_key=True)
movie_type = db.Column('movie_type', db.Text())
characters = db.relationship("Character", backref="movie", lazy='dynamic')
def __init__(self, movie_type):
self.movie_type = movie_type
def __repr__(self):
return '<Movie %r>' % self.id
class Character(db.Model):
__tablename__ = "characters"
id = db.Column('character_id', db.Integer, primary_key=True)
character_description = db.Column('character_description', db.Text())
movie_id = db.Column(db.Integer, db.ForeignKey('movies.movies_id'))
movie = db.relationship('Movie')
def __init__(self, character_description, movie):
self.character_description = character_description
self.movie = movie
def __repr__(self):
return '<Character %r>' % self.id
Inserting
c = Character(character_description='c')
c2 = Character(character_description='c2')
m = Movie(movie_type ='action')
# link characters to movie
m.characters.append(c)
m.characters.append(c2)
# or
m.characters.extend([c,c2])
db.session.add(m)
# add characters
db.session.add(c)
db.session.add(c2)
# or
db.session.add_all([c,c2])
# commit
db.session.commit()

SQLAlchemy Subclass/Inheritance Relationships

class Geolocation(db.Model):
__tablename__ = "geolocation"
id = db.Column(db.Integer, primary_key=True)
latitude = db.Column(db.Float)
longitude = db.Column(db.Float)
elevation = db.Column(db.Float) # Meters
# Relationships
pin = db.relationship('Pin', uselist=False, backref="geolocation")
def __init__(self, latitude, longitude, elevation):
self.latitude = latitude
self.longitude = longitude
self.elevation = elevation
def __repr__(self):
return '<Geolocation %s, %s>' % (self.latitude, self.longitude)
class Pin(db.Model):
__tablename__ = "pin"
id = db.Column(db.Integer, primary_key=True)
geolocation_id = db.Column(db.Integer, db.ForeignKey('geolocation.id')) # True one to one relationship (Implicit child)
def __init__(self, geolocation_id):
self.geolocation_id = geolocation_id
def __repr__(self):
return '<Pin Object %s>' % id(self) # Instance id merely useful to differentiate instances.
class User(Pin):
#id = db.Column(db.Integer, primary_key=True)
pin_id = db.Column(db.Integer, db.ForeignKey('pin.id'), primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(120), nullable=False)
salt = db.Column(db.String(120), nullable=False)
# Relationships
#posts = db.relationship('Post', backref=db.backref('user'), lazy='dynamic') #One User to many Postings.
def __init__(self, username, password_hash, salt, geolocation_id):
super(Pin, self).__init__(self, geolocation_id)
self.username = username
self.password_hash = password_hash
self.salt = salt
def __repr__(self):
return '<User %r>' % self.username
I'm confused about how to set up id's and relationships with subclasses in SQLAlchemy (I happen to be using Flask-SQLAlchemy). My general design is to have the superclass Pin be a high level representation of anything that has a geolocation (i.e. a User, a Place, etc.).
There is a one to one relationship between a Pin and Geolocation object so a Geolocation does not contain the location of two Users (or a User and a Place) simultaneously for example. Now I want to subclass Pin to create the User class. A User object should have a name, password_hash, salt and I also want to be able to lookup the Geolocation of the User via userObj.geolocation. However, I later want to make a class Place which also subclasses Pin and I should be able to lookup the geolocation of a Place via placeObj.geolocation . Given a geolocation object, I should be able to use geolocationObj.pin to lookup the User/Place/etc. that the geolocation object corresponds to. The whole reason I introduced the superclass Pin was to ensure that there was a pure one to one relationship between Pin and Geolocation objects rather than having a Geolocation be associated with either a User or a Person which would require the Geolocation table to have user_id and place_id columns, one of which would always be null.
I was expecting every User to automatically have a .geolocation property, via the parent Pin class, which referred to a Geolocation but it seems like SQLAlchemy does not do this. How can I make subclassing relationships work to accomplish my goal of having User and Place and potentially other classes subclass Pin, have each of those classes have a geolocation property via Pin, and have a one to one relationship between a Pin and a Geolocation?
The solution I came up with. This serves as a full example of subclassing in SQLAlchemy in the declarative style and using Join inheritance.
class Geolocation(Base):
__tablename__ = "geolocation"
id = Column(Integer, primary_key=True)
latitude = Column(Float)
longitude = Column(Float)
elevation = Column(Float) # Meters
# Relationships
person = relationship('Pin', uselist=False, backref="geolocation")
def __init__(self, latitude, longitude, elevation):
self.latitude = latitude
self.longitude = longitude
self.elevation = elevation
def __repr__(self):
return '<Geolocation %s, %s>' % (self.latitude, self.longitude)
class Pin(Base):
__tablename__ = 'pin'
id = Column(Integer, primary_key=True)
geolocation_id = Column(Integer, ForeignKey('geolocation.id'), unique=True, nullable=False) # True one to one relationship (Implicit child)
type = Column('type', String(50)) # discriminator
__mapper_args__ = {'polymorphic_on': type}
def __init__(self, geolocation_id):
self.geolocation_id = geolocation_id
class User(Pin):
__tablename__ = 'user'
id = Column(Integer, ForeignKey('pin.id'), primary_key=True)
__mapper_args__ = {'polymorphic_identity': 'user',
'inherit_condition': (id == Pin.id)}
user_id = Column(Integer, autoincrement=True, primary_key=True, unique=True)
username = Column(String(80), unique=True)
password_hash = Column(String(120))
salt = Column(String(120))
posts = relationship('Posting', primaryjoin="(User.user_id==Posting.user_id)", backref=backref('user'), lazy='dynamic') #One User to many Postings.
def __init__(self, username, password_hash, salt, geo_id):
super(User, self).__init__(geo_id)
self.username = username
self.password_hash = password_hash
self.salt = salt
def __repr__(self):
return '<User %s>' % (self.username)
class Posting(Pin):
__tablename__ = 'posting'
id = Column(Integer, ForeignKey('pin.id'), primary_key=True)
__mapper_args__ = {'polymorphic_identity': 'posting',
'inherit_condition': (id == Pin.id)}
posting_id = Column(Integer, autoincrement=True, primary_key=True, unique=True)
creation_time = Column(DateTime)
expiration_time = Column(DateTime)
user_id = Column(Integer, ForeignKey('user.user_id')) # One User to many Postings
def __init__(self, creation_time, expiration_time, user_id, geo_id):
super(Posting, self).__init__(geo_id)
# For now, require creation time to be passed in. May make this default to current time.
self.creation_time = creation_time
self.expiration_time = expiration_time
self.user_id = user_id
def __repr__(self):
#TODO come up with a better representation
return '<Post %s>' % (self.creation_time)
Here's the documentation for mapping inheritance hierarchies and for doing it declaratively in SQLAlchemy.
I believe you'll want the joined table inheritance flavour, meaning that every class in your parent class chain has its own table with the columns unique to it. Basically, you need to add a discriminator column to the pin table to denote the subclass type for each Pin, and some double underscore properties to your classes to describe the inheritance configuration to SQLAlchemy.

Categories