SQLAlchemy Subclass/Inheritance Relationships - python

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.

Related

Displaying joined fields in Flask-Admin ModelView

I have a Flask app using Flask-SQLAlchemy with some simple relational data mapping, e.g. between Orders and OrderItems belonging to those orders.
In my Flask-Admin backend I would like to show some of the order attributes in the list of OrderItems — as opposed to having the entire order object. E.g. make the "Order.email" listed (can be read-only) in the OrderItems' rows.
I've looked into the inline_models attribute of the ModelView, but this seems to be more feared towards actually editing the relational object — I just want to display (and sort/search by) some value of the "parent".
Is there a way to achieve this?
You can easily include fields via a foreign key relationship by including them in column_list value - documentation. Consider the two simplified models, note the company back reference in the Address model:
class Company(db.Model):
__tablename__ = 'companies'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(255), nullable=False, unique=True, index=True)
website = db.Column(db.Unicode(255), nullable=True)
notes = db.Column(db.UnicodeText())
#hybrid_property
def address_count(self):
return len(self.addresses)
#address_count.expression
def address_count(cls):
return select([func.count(Address.id)]).where(Address.company_id == cls.id).label("address_count")
def __str__(self):
return self.name
class Address(db.Model):
__tablename__ = 'addresses'
id = db.Column(db.Integer, primary_key=True)
address1 = db.Column(db.Unicode(255), nullable=False)
town = db.Column(db.Unicode(255), index=True, nullable=False)
county = db.Column(db.Unicode(255))
country = db.Column(db.Unicode(255))
post_code = db.Column(db.Unicode(10))
company_id = db.Column(db.Integer, db.ForeignKey('companies.id'), index=True)
company = db.relationship(Company, backref=db.backref('addresses', uselist=True, lazy='select', cascade='delete-orphan,all'))
def __str__(self):
return ', '.join(filter(None, [self.address1, self.town, self.county, self.post_code, self.country]))
In the Address view you can access a "parent" company using dotted notation. For example:
class AddressView(ModelAdmin):
column_list = (
'company.name',
'company.website',
'address1',
'address2'
)

SQLAlchemy how to create a relationship or mapping of a table's "type" row to the table model or mixin

I need to set the entity_type_id as a column value when I persist a row to a generic table of various entity_types. I should be able to load the entity_type_id for every specific instance at instantiation time because it is accessible via a simple select statement. I'd like to have that id automatically retrieved/set at the class (or instance) level without executing a query and/or manually setting to every time I persist a row of an "entity_type".
I tried an entity_type_id #property on the mixin that returns the id of the entity_type using the object_session but for reasons I don't fully understand the orm still inserts null as the entity_type_id value when I commit/flush the session. (my guess is having the "property" itself isn't the same thing as setting the attribute value on the instance and/or causing an issue because the column name from the base class has the same name)
Here's a slimmed down version of the relevant models in my schema:
class EntityType(Base):
__tablename__ = 'entity_type'
id = Column(UUID(as_uuid=True), primary_key=True, server_default=FetchedValue())
table_name = Column(String, nullable=False)
ui_label = Column(Text, unique=True, nullable=False)
entry_key = Column(Text, unique=True, nullable=False)
Base class model:
class TrackedEntity(Base):
#declared_attr
def __tablename__(cls):
return convert(cls.__name__)
__table_args__ = (
UniqueConstraint('entity_type_id', 'label'),
)
id = Column(UUID(as_uuid=True), primary_key=True, server_default=FetchedValue())
entity_type_id = Column('entity_type_id', ForeignKey('entity_type.id'))
label = Column('label', String, nullable=False)
entity_type = relationship('EntityType')
polymorphic_discriminator = column_property(select([EntityType.table_name]).where(EntityType.id == entity_type_id).as_scalar())
#declared_attr
def entity_type_label(cls):
return association_proxy('entity_type', 'label')
#declared_attr
def __mapper_args__(cls):
if cls.__name__ == 'TrackedEntity':
return {
"polymorphic_on": cls.polymorphic_discriminator,
"polymorphic_identity": cls.__tablename__
}
else:
return {"polymorphic_identity": cls.__tablename__}
Children class mixin:
class TrackedEntityMixin(object):
# noinspection PyMethodParameters
#declared_attr
def id(cls) -> Column:
return Column(ForeignKey('tracked_entity.id'), primary_key=True)
#gets me the id but isn't very helpful like this, still needs to be manually set like child.entity_type_id = child._entity_type_id
#property
def _entity_type_id(self):
return object_session(self). \
scalar(
select([EntityType.id]).
where(EntityType.table_name == self.__tablename__)
)
A child class model:
class DesignedMolecule(TrackedEntityMixin, TrackedEntity):
extra = Column('extra', String)
parents = relationship('TrackedEntity', secondary='mix_dm_parent_entity')

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.

Saving duplicate entries in a Flask SQLAlchemy association table

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?

SQLAlchemy Using relationship()

I am using SQLAlchemy here, trying to make a couple tables and link them and am having problems implementing this.
class Team(Base):
__tablename__ = "teams"
id = Column(Integer, primary_key=True)
espn_team_id = Column(Integer, unique=True, nullable=False)
games = relationship("Game", order_by="Game.date")
def __init__(self, name):
self.name = name
self.espn_team_id = espn_team_id
self.games = games
class Game(Base):
__tablename__ = "games"
id = Column(Integer, primary_key=True)
espn_game_id=Column(Integer, unique=True, nullable=False)
date = Column(Date)
h_espn_id = Column(Integer, ForeignKey('teams.espn_team_id'))
a_espn_id = Column(Integer, ForeignKey('teams.espn_team_id'))
I have this in one file which I use to create the tables. Then in another file I use the insert() function to put values into both tables. I think if I have a team with espn_team_id 360, and then I put in multiple games into the game table which have either h_espn_id=360, or a_espn_id=360, i should be able to do:
a = Table("teams", metadata, autoload=True)
a = session.query(a).filter(a.c.espn_team_id==360).first().games
and it should give me a list of all games team with ID 360 has played. But instead I get this error
AttributeError: 'NamedTuple' object has no attribute 'games'
What am I misunderstanding about SQLAlchemy or relational databases here?
Firstly, you don't have to create another Table object, as it is available as Team.__table__. Anyway, you can just query the mapped class, e.g.
query = Session.query(Team).filter(Team.espn_team_id == 360)
team360 = query.one()
games = team360.games
Refer to the documentation for methods .one(), .first(), and .all(): http://docs.sqlalchemy.org/en/latest/orm/query.html
Here is the solution I found, took way too long to understand this...
class Team(Base):
__tablename__ = "teams"
id = Column(Integer, primary_key=True)
name = Column(String)
espn_team_id = Column(Integer, unique=True, nullable=False)
h_games = relationship(
"Game",
primaryjoin="Game.h_espn_id==Team.espn_team_id",
order_by="Game.date")
a_games = relationship(
"Game",
primaryjoin="Game.a_espn_id==Team.espn_team_id",
order_by="Game.date")
#hybrid_property
def games(self):
return self.h_games+self.a_games
def __init__(self, name):
self.name = name
self.espn_team_id = espn_team_id
self.h_games = h_games
self.a_games = a_games
self.games = games

Categories