class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
default = db.Column(db.Boolean, default=False, index=True)
permissions = db.Column(db.Integer)
class Devices(db.Model):
__tablename__ = 'devices'
id = db.Column(db.Integer, primary_key=True)
purpose = db.Column('purpose', db.String(64))
type = db.Column('type', db.String(64))
name = db.Column('name', db.String(64))
channel = db.Column('channel', db.Integer)
role_id = db.Column('role_id', db.Integer)
role_permissions = db.Column('role_permissions', db.Integer)
role = db.ForeignKeyConstraint(['role_id', 'role_permissions'], ['roles.id', 'roles.permissions'])
Then I would expect this to work:
dev = Devices(purpose="lights",type="tcp",name="zeus",channel=8)
role = Role.query.first()
dev.role = role
db.session.add(dev)
db.session.commit()
But once persisted, role_id and role_permissions get null value. Why? Whats the right way to do this??
You need to define a relationship in addition to the foreign key.
A foreign key is just a database-level constraint to ensure you cannot reference rows that don't exist (additionally, it helps SQLAlchemy setup a relationship without you specifying another time how the two tables are linked).
You want this in your model:
class Devices(db.Model):
__table_args__ = (db.ForeignKeyConstraint(['role_id', 'role_permissions'], ['roles.id', 'roles.permissions']),)
# ...
role = db.relationship('Role', backref=db.backref('devices'))
By doing so, device.role = some_role will properly populate the foreign keys, and in addition each Role instance will have a devices collection that gives you access to its associated devices.
The SQLAlchemy tutorial also has a section about relationships:
http://docs.sqlalchemy.org/en/rel_1_0/orm/tutorial.html#building-a-relationship
You can pretty much follow it; Flask-SQLAlchemy and plain SQLalchemy don't really differ - Flask-SQLAlchemy simply makes many things accessible via the db object to avoid importing them explicitly.
By the way, since Role.id is the primary key, you don't need to include role_permissions in the foreign key - you cannot have more than one role the same ID since the primary key is always unique. This makes your model even easier:
class Devices(db.Model):
# ...
role_id = db.Column('role_id', db.Integer, db.ForeignKey('roles.id'))
role = db.relationship('Role', backref=db.backref('devices'))
You can also get rid of the role_permissions column in your Devices model (which, by the way, should be named Device). If you need the permissions, simply get it from the role (if you usually need it, add lazy=False to the foreign key, then querying a device will always join the role table to avoid extra queries)
Related
I'm not sure I properly understand how to get the collection part of the one-to-many relationship.
class ProjectReport(db.Model):
__tablename__ = "project_reports"
id = db.Column(UUID, primary_key=True, default=uuid.uuid4)
project_id = db.Column(UUID, db.ForeignKey("projects.id"), nullable=False)
entries = db.relationship("ProducerEntry", backref="project_report", lazy="dynamic")
class ProducerEntry(Entry):
__tablename__ = "producer_entries"
__mapper_args__ = {"polymorphic_identity": "Entry"}
id = db.Column(UUID, db.ForeignKey("entries.id"), primary_key=True)
project_id = db.Column(UUID, db.ForeignKey("projects.id"), nullable=False)
project_report_id = db.Column(UUID, db.ForeignKey("project_reports.id"), nullable=True)
My problem is that I can't just access the entries field.
for entry in self.entries:
do_something(entry)
This returns NotImplementedError
I managed to get the data via hybrid property but that seems a bit of an overkill since already have the relationship, also it'd get a bit complex for further logic later on.
#hybrid_property
def entries(self):
return ProducerEntry.query.filter_by(project_report_id=self.id)
Ab additional information is that the ProjectReport is basically the common columns of the Entry and Project models, and the project_report_id is nullable, because the entries and projects are generated first and then I can generate the project reports from them. This is how I create the reports:
...
project_report = ProjectReport(date_order=entry.date_order, project_id=entry.project.id)
project_report.entries.append(entry)
...
As far as I know I don't have to add the project_report_id to the producer entry after this.
What am I missing here?
Well yeah, that relationship field returns a query, so I simply should have called:
self.entries.all()
Or anything else which is handling a query.
Using SQL Alchemy and declarative base to define two related tables
JiraBase = declarative_base(cls=_Shared)
class JiraSource(JiraBase):
id = Column(BigInteger, primary_key=True, autoincrement=True)
name = Column(String)
__tablename__ = 'jira_source'
class JiraProject(JiraBase):
id = Column(BigInteger, primary_key=True, autoincrement=True)
source = Column(BigInteger, ForeignKey('jira_source.id'), nullable=False)
project_id = Column(String, nullable=False)
project_key = Column(String, nullable=False)
name = Column(String)
__table_args__ = (Index('source', 'project_id', 'project_key', unique=True),)
__tablename__ = 'jira_project'
The idea behind the Index is that projects are uniquely identified by an id and key for a given JiraSource.
The tables are being dropped and created via
JiraBase.metadata.drop_all(bind=self.engine)
JiraBase.metadata.create_all(bind=self.engine)
This actually drops and creates tables fine; however, also throws an exception
sqlalchemy.exc.ProgrammingError: (psycopg2.errors.DuplicateTable) relation "source" already exists
[SQL: CREATE UNIQUE INDEX source ON jira_issue (issue_id)]
(Background on this error at: http://sqlalche.me/e/13/f405)
Thinking somehow because using both a ForeignKey and Index that both relate to source, is this a bad practice or something? When I comment out the Index entirely or drop the source field from it, the error goes away. Thinking that because I defined source as a foreign key that using it again in an index is bad or something?
Any idea why I'm seeing this error? For now am simply try/ignoring in code.
When im trying to delete category instance identified by 'id' with its category_image and files instances the way like this:
c = Category.query.get(id)
for ci in c.images:
db.session.delete(ci)
db.session.flush()
for ci in c.images:
db.session.delete(ci.file)
db.session.flush() # if i type here db.session.commit() all is fine
db.session.delete(c)
db.session.commit()
i'm getting a AssertionError: Dependency rule tried to blank-out primary key column 'category_image.id_category' on instance ''. But when i replace flush which is after deleting category_image.files with commit, then it works. I've notice it after i changed CategoryImage table to intermediary. Before changes it has it's own pk that wasn't combined and all was working properly. Here're my current models definitions.
class File(db.Model):
__tablename__ = 'file'
id_file = Column(Integer, Sequence('seq_id_file'), primary_key=True, nullable=False)
name = Column(Text, nullable=False)
path = Column(Text, nullable=False, unique=True)
protected = Column(Boolean, nullable=False, default=False)
class Category(db.Model):
__tablename__ = 'category'
id_category = Column(Integer, Sequence('seq_id_category'), primary_key=True, nullable=False)
name = Column(UnicodeText, nullable=False, unique=True)
images = relationship('CategoryImage', backref='images')
class CategoryImage(db.Model):
__tablename__ = 'category_image'
__table_args__ = (
PrimaryKeyConstraint('id_category', 'id_file', name='seq_id_category_image'),
)
id_category = Column(Integer, ForeignKey(Category.id_category), nullable=False)
id_file = Column(Integer, ForeignKey(File.id_file), nullable=False)
id_size_type = Column(Integer, nullable=)
file = relationship(File)
Now i'm trying to figure out what just happened. Correct me if i'm using things wrong.
I just noticed that i have to delete objects beeing in relation with intermediate model in the same order as it was declared in table_args, PrimaryKeyConstraint('id_category', 'id_file'). So when i perform it this way: session.delete(category_image), session.delete(category), session.delete(file) and commit it or flush everywhere before commit, then all works fine. If anyone spot something about it in alch docs let me know.
Here is what is happening. Once you call session.delete() on some object it is like having marked the object for deletion but not yet deleted from db. when you call the flush() after deleting (note: db still has the object as it is yet not committed) but session has marked the object as deleted. So the objects become inconsistent. In order to make the delete smooth you can always wrap your delete operations within a transaction and once they are deleted from the session you need to call the db.commit() once to make db session consistent with the db.
Hope it helps.
I have three [MySQL] tables: Person, Role, PersonRole
class Person(Base):
__tablename__ = 'people'
id = Column(INTEGER(unsigned=True), primary_key=True)
full_name = Column(String(120), nullable=False)
email = Column(String(128))
username = Column(String(50))
last_modified = Column(TIMESTAMP, nullable=False,
server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))
created = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'))
roles = relationship('Role',
secondary='person_role',
primaryjoin="and_(Person.id==PersonRole.person_id,"
"Role.active==True,"
"PersonRole.active==True)",
back_populates='people')
def __repr__(self):
"""String representation."""
return '''<Person(id='%s', full_name='%s')>''' % (
str(self.id), str(self.full_name)
)
class Role(Base):
__tablename__ = 'role'
id = Column(INTEGER(unsigned=True), Sequence('role_id_seq'), primary_key=True)
role_name = Column(String(16))
active = Column(Boolean, default=True)
last_modified = Column(TIMESTAMP, nullable=False,
server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))
created = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'))
users = relationship('Person',
secondary='person_role',
primaryjoin="and_(Role.id==PersonRole.role_id,"
"Role.active==True,"
"PersonRole.active==True)",
back_populates='roles')
def __repr__(self):
"""String representation."""
return '''<Role(role_id='%s', role_name='%s')>''' % (
str(self.role_id), str(self.role_name)
)
class PersonRole(Base):
__tablename__ = 'person_role'
ds_id = Column(INTEGER(unsigned=True),
ForeignKey('person.id'), primary_key=True)
role_id = Column(INTEGER(unsigned=True),
ForeignKey('role.id'), primary_key=True)
active = Column(Boolean, default=True)
last_modified = Column(TIMESTAMP, nullable=False,
server_default=text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))
created = Column(TIMESTAMP,
server_default=text('CURRENT_TIMESTAMP'))
Retrieving a Person like so:
...session setup as s...
person = s.query(Person).get(123)
and to list out their roles:
for role in person.roles:
print(role.id, role.role_name, role.active)
Now, in each table there is an active column. This is to keep track of an [shocker] active status as we dont want to remove data from the table, merely keep it's state. Now to the two issues which have kept me from using SQLAlchemy altogether and has me writing and executing SQL manually -
The active portion in the final loop though displays the active state of role.active and not that of the actual relationship, person_role.active.
Removing a role will remove the relationship row instead of performing the desired action, person_role.active = 0
Even if I deactivate the relationship, adding it again will set off the Duplicate Key error.
Is there a sane, valid way to go about accomplishing this without restructuring my data?
Edit for further clarification:
The two main tables in this case are person and person_role. Person is the main table which holds our users. PersonRole holds the roles a person actually has, in the form of person.id to role.id. The Role table is merely a lookup table for the Role definition (to get the names).
I suppose what I want is a way to intercept how the ORM actually adds/removes the data. Adding should [more-or-less] do an "upsert" and removing should basically run a update query like: update person_role set active = 0 where person_id = %s and role_id = %s.
I have read a lot of the docs but tend to get lost in the terminologies. :S
Not sure I completely understood your problems, but let me try to help you:
1) This is the one I am most confused with: person.roles maps to a collection of Role entities. Ain't that what you expect?
2, 3) You have set up a relationship between Person and Role, using PersonRole as secondary. Deactivating means setting PersonRole.status to inactive, not removing it. Trying to add it again will show a Duplicate Key error indeed!
I think you want to run a query to load a PersonRole entity by ds_id and role_id, update its status to active and persist changes. I understand that sometimes this may be tedious to perform, so maybe a immediate solution that would not require you to move data around would be to map Person and Role to PersonRole. So, in spite of having Person.roles you would have Person.personRoles, so you have access to PersonRole entities and may set its status.
Of course this is a very immediate solution. SQLAlchemy is extremely featured so maybe you can intercept the Person.roles removal and customize its behaviour to set PersonRole to inactive. You may want to read more about cascading on the docs as well.
Hope I was able to clarify things a little :)
I'm trying to insert the role data in this association table:
class Association(db.Model):
__tablename__ = 'associations'
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), primary_key=True)
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), primary_key=True)
role = db.Column(db.String)
user = db.relationship("User", back_populates="teams")
team = db.relationship("Team", back_populates="users")
It's linked to 2 other tables:
class User(db.Model, UserMixin):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String)
username = db.Column(db.String, nullable=False, unique=True, index=True)
password_hash = db.Column(db.String)
teams = db.relationship("Association", back_populates="user")
class Team(db.Model):
__tablename__ = 'teams'
id = db.Column(db.Integer, primary_key = True)
name = db.Column(db.String)
player = db.Column(db.String)
users = db.relationship("Association", back_populates="team")
However, when I try to insert the role in the association table:
t = Team.query.filter_by(id=team_name1_id).first()
a = Association(role=form.role.data)
a.user = User.query.filter_by(id=current_user.id).first()
t.users.append(a)
db.session.add(t)
db.session.commit()
I get this error:
IntegrityError: (raised as a result of Query-invoked autoflush;
consider using a session.no_autoflush block if this flush is occurring
prematurely) (sqlite3.IntegrityError) NOT NULL constraint failed:
association.team_id [SQL: u'INSERT INTO association (user_id, role)
VALUES (?, ?)'] [parameters: (1, u'point guard')]
Do you know how can I fix it?
The line that causes the autoflush is
t.users.append(a)
You've not defined your relationship loading strategies, so they default to "select". Since you're accessing the Team.users relationship attribute for the first time, it will emit a SELECT in order to fetch the related objects.
You've also made changes to the session indirectly in
a.user = User.query.filter_by(id=current_user.id).first()
which due to save-update cascade, on by default, places the new Association instance to the session as well.
In order to keep the DB's and session's state consistent SQLAlchemy has to flush your changes to the DB before the query is emitted, and so the half initialized Association is sent to the DB before being associated with the Team instance.
Possible solutions:
Temporarily disable the autoflush feature for that particular append because you know you're adding a new Association:
with db.session.no_autoflush:
t.users.append(a)
The SELECT will still be emitted, though.
Reverse the operation. Instead of appending the Association instance to the Team.users collection, assign the Team to the Association.team:
# Instead of t.users.append(a):
a.team = t # Mr.
A less obvious solution is to eager load the contents of the relationship when you fetch the Team instance, so that no SELECT is necessary:
t = db.session.query(Team).options(db.joinedload(Team.users)).first()
or not load them at all using the db.noload(Team.users) option.
Note that
db.session.add(t)
is redundant. The fetched Team instance is already in the session – otherwise the lazy load of Team.users could not have proceeded in the first place.