I'm setting up multiple foreign keys but it shows error
from flask_sqlalchemy import SQLAlchemy
sql= SQLAlchemy(app)
app.secret_key = os.urandom(24)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql:///test'
app.config['SECERET_KEY'] = 'RANDAM'
class Users(sql.Model):
__tablename__='users'
id = sql.Column(sql.Integer, primary_key=True, nullable=False,
default='')
name = sql.Column(sql.String(25),nullable=False, default='')
username = sql.Column(sql.String(25),nullable=False, default='')
email = sql.Column(sql.String(25), primary_key=True,nullable=False,
default='')
password = sql.Column(sql.String(100),nullable=False, default='')
reg_time = sql.func.current_timestamp()
online = sql.Column(sql.String(1),nullable=False, default='')
class friendgroup(sql.Model):
__tablename__='friendgroup'
group_id = sql.Column(sql.Integer,sql.ForeignKey('users.id'),
primary_key=True,nullable=False, default='')
fg_name = sql.Column(sql.String(25), primary_key=True, nullable=False,
default='')
owner_email = sql.Column(sql.String(25),sql.ForeignKey('users.email'),
primary_key=True, nullable=False,default='')
description = sql.Column(sql.String(25), nullable=False, default='')
class belong(sql.Model):
__tablename__ = 'belong'
group_id=sql.Column(sql.Integer,sql.ForeignKey('friendgroup.group_id')
,primary_key=True,nullable=False,default='')
email = sql.Column(sql.String(25),sql.ForeignKey('users.email'),
nullable=False, default='', primary_key=True)
owner_email = sql.Column(sql.String(25),
sql.ForeignKey('friendgroup.owner_email'),nullable=False,
default='', primary_key=True)
fg_name = sql.Column(sql.String(25),
sql.ForeignKey('friendgroup.fg_name'),nullable=False, default='',
primary_key=True)
sql.create_all()
app.run(debug=True)
I expect all these will be entered into the database but it shows error:
sqlalchemy.exc.OperationalError:
(MySQLdb._exceptions.OperationalError) (1005, 'Can\'t create table
test.friendgroup (errno: 150 "Foreign key constraint is
incorrectly formed")')
When I run SHOW ENGINE INNODB STATUS, it shows this error under the LATEST FOREIGN KEY ERROR section:
Cannot find an index in the referenced table where the referenced
columns appear as the first columns, or column types in the table and
the referenced table do not match for constraint. Note that the
internal storage type of ENUM and SET changed in tables created with
>= InnoDB-4.1.12, and such columns in old tables cannot be referenced by such columns in new tables.
We can disregard the stuff about ENUM and SET, and the type definitions between the two are exact, so that just leaves the issue about the index.
If you add index=True to the column definitions of Users.email and friendgroup.fg_name it provides an index that can be used for mysql to generate the foreign keys on and your tables should create.
However, I would advise a rethink of your primary keys your tables. You are defining each of those tables with an autoincrement primary key field, yet include other fields in a composite primary key. You don't need to do that. If you remove the non-autoincrement columns from the primary key and add a unique constraint on them instead, all your problems go away.
Additionally, by including the field friendgroup.owner_email in either the primary key or a unique constraint means that each user can only own a single friend group. Is that what you want? Similarly, having owner_email and fg_name in the primary key of the belong table would mean that only one user can belong to any group owned by a particular user.
If it helps, this is how I would put those models together. It resolves your issues with the foreign keys and uses relationship attributes instead of duplicating data like email addresses and group names across tables.
class Users(sql.Model):
__tablename__ = "users"
id = sql.Column(sql.Integer, primary_key=True)
name = sql.Column(sql.String(25), nullable=False)
username = sql.Column(sql.String(25), nullable=False)
email = sql.Column(sql.String(25), unique=True, nullable=False)
password = sql.Column(sql.String(100), nullable=False)
reg_time = sql.Column(
sql.TIMESTAMP, server_default=sql.func.current_timestamp()
)
online = sql.Column(sql.String(1), nullable=False, default="")
class friendgroup(sql.Model):
__tablename__ = "friendgroup"
group_id = sql.Column(
sql.Integer, sql.ForeignKey("users.id"), primary_key=True
)
fg_name = sql.Column(sql.String(25), unique=True, nullable=False)
owner_id = sql.Column(
sql.Integer, sql.ForeignKey("users.id"), nullable=False
)
description = sql.Column(sql.String(25), nullable=False, default="")
owner = sql.relationship("User")
class belong(sql.Model):
__tablename__ = "belong"
group_id = sql.Column(
sql.Integer, sql.ForeignKey("friendgroup.group_id"), primary_key=True
)
user_id = sql.Column(
sql.Integer, sql.ForeignKey("users.id"), primary_key=True
)
user = sql.relationship("User")
group = sql.relationship("friendgroup")
Related
I have these two objects:
class Account(Base):
__tablename__ = "accounts"
uuid = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"))
name = Column(String(255), nullable=False)
description = Column(String(255), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
profiles = relationship("Profile")
class Profile(Base): # pragma: integration
__tablename__ = "profiles"
uuid = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"))
name = Column(String(255), nullable=False)
description = Column(String(255), nullable=True)
account_uuid = Column(UUID(as_uuid=True), ForeignKey(Account.uuid), nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
An account can have multilple profiles and a profile cannot exist without it belonging to an account.
Now lets suppose I have an account Account A and that account has Profile 1 and I want to change description in Account A.
I'm fetching and modifying Account A like this:
account = SESSION.query(Account)\
.select_from(Account)\
.filter_by(uuid='uuid-here')\
.options(noload('*'))\
.all()[0]
account.description = "New description"
SESSION.merge(account)
SESSION.commit()
but I get this error:
{IntegrityError}(psycopg2.errors.NotNullViolation) null value in column "account_uuid" of relation "profiles" violates not-null constraint
DETAIL: Failing row contains (2023-01-25 10:28:04.067598+00, 2023-01-25 10:44:44.45617+00, c2e25467-b058-469f-99bb-28debd6289cd, Profile 1, This is a profile for testing, null).
[SQL: UPDATE profiles SET updated_at=now(), account_uuid=%(account_uuid)s WHERE profiles.uuid = %(profiles_uuid)s]
[parameters: {'account_uuid': None, 'profiles_uuid': UUID('c2e25467-b058-469f-99bb-28debd6289cd')}]
(Background on this error at: https://sqlalche.me/e/14/gkpj)
I don't understand why this error is comming up, I am not touching the profiles table at all (I purposefully didn't even let it be loaded with noload('*')) and I haven't changed Account A's uuid either.
Why is it trying to update profiles?
EDIT:
I can solve this by changing the code to:
account = SESSION.query(Account)\
.select_from(Account)\
.filter_by(uuid='uuid-here')\
.options(noload('*'))\
.all()[0]
account.description = "New description"
SESSION.expunge_all()
SESSION.add(account)
SESSION.merge(account)
SESSION.commit()
but that doesn't feel right
Context: I'm making an auctioning website for which I am using Flask-SQLAlchemy. My tables will need to have a many-to-many relationship (as one artpiece can have many user bids and a user can bid on many artpieces)
My question is: it is possible to add another column to my joining table to contain the id of the user bidding, the id of artpiece that they are bidding on and also how much they bid? Also if yes, how would I include this bid in the table when I add a record to said table?
bid_table = db.Table("bid_table",
db.Column("user_id", db.Integer, db.ForeignKey("user.user_id")),
db.Column("item_id", db.Integer, db.ForeignKey("artpiece.item_id"))
)
class User(db.Model):
user_id = db.Column(db.Integer, unique=True, primary_key=True, nullable=False)
username = db.Column(db.Integer, unique=True, nullable=False)
email = db.Column(db.String(50), unique =True, nullable=False)
password = db.Column(db.String(60), nullable=False)
creation_date = db.Column(db.DateTime, default=str(datetime.datetime.now()))
bids = db.relationship("Artpiece", secondary=bid_table, backref=db.backref("bids", lazy="dynamic"))
class Artpiece(db.Model):
item_id = db.Column(db.Integer, unique=True, primary_key=True, nullable=False)
artist = db.Column(db.String(40), nullable=False)
buyer = db.Column(db.String(40), nullable=False)
end_date = db.Column(db.String(40))
highest_bid = db.Column(db.String(40))
It is possible to do this with SQL Alchemy, but it's very cumbersome in my opinion.
SQLAlchemy uses a concept called an Association Proxy to turn a normal table into an association table. This table can have whatever data fields you want on it, but you have to manually tell SQLAlchemy which columns are foreign keys to the other two tables in question.
This is a good example from the documentation.
In your case, the UserKeyword table is the association proxy table that you want to build for your user/bid scenario.
The special_key column is the arbitrary data you would store like the bid amount.
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import backref, declarative_base, relationship
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String(64))
# association proxy of "user_keywords" collection
# to "keyword" attribute
keywords = association_proxy('user_keywords', 'keyword')
def __init__(self, name):
self.name = name
class UserKeyword(Base):
__tablename__ = 'user_keyword'
user_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
keyword_id = Column(Integer, ForeignKey('keyword.id'), primary_key=True)
special_key = Column(String(50))
# bidirectional attribute/collection of "user"/"user_keywords"
user = relationship(User,
backref=backref("user_keywords",
cascade="all, delete-orphan")
)
# reference to the "Keyword" object
keyword = relationship("Keyword")
def __init__(self, keyword=None, user=None, special_key=None):
self.user = user
self.keyword = keyword
self.special_key = special_key
class Keyword(Base):
__tablename__ = 'keyword'
id = Column(Integer, primary_key=True)
keyword = Column('keyword', String(64))
def __init__(self, keyword):
self.keyword = keyword
def __repr__(self):
return 'Keyword(%s)' % repr(self.keyword)
Check out the full documentation for instructions on how to access and create this kind of model.
Having used this in a real project, it's not particularly fun and if you can avoid it, I would recommend it.
https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
Im writing an inventory app with flask+sqlalchemy, long history made short i have a table called Bill with id, bill_id and bill_date, and the inventory table with id, bill_number, loctation. Then using a queryselectfield the user select the bill_number, fill the others fields and store information in the DB.
but if i update the bill_number in the bill table, need that also the bill_number in the inventory table update all those entries.
I have tried with foreignkey, creating a for statement to update all the entries and nothing works. Also the update method but only works with 'static_values' but with variables got me a literal error.
class Bill(db.Model):
id = db.Column(db.Integer, primary_key=True)
bill_number = db.Column(db.String(64), index=True, unique=True)
bill_date = db.Column(db.Date, index=False, unique=False)
class Location(db.Model):
id = db.Column(db.Integer, primary_key=True)
location_name = db.Column(db.String(64), index=True, unique=True)
class Inventory(db.Model):
id = db.Column(db.Integer, primary_key=True)
serial_number = db.Column(db.String(64), index=True, unique=True)
bill_number = db.Column(db.String(64), db.ForeignKey('bill.bill_number'))
location = db.Column(db.String(64), db.ForeignKey('location.location_name'))
update = Inventory.query.filter(Inventory.bill_number==old_bill).update({ Inventory.bill_number : new_bill})
where old_bill and new_bill comes from a form, if a try with:
Inventory.query.filter(Inventory.bill_number=='666').update({ Inventory.bill_number : '999'})
works like a charm, but i need to use the values from the form, not those static values. Foreignkeys are used but dont know if they really works how in the way that i implemented.
Even tried with:
old = form.old.data
new = form.new.data
all = Inventory.query.filter_by(bill_number=old).all()
for i in all:
i.bill_numer = new
db.session.commit()
but no, cant update the entire inventory table.
I have a relationship that is one to one between cage codes and duns numbers.
I have set up my relationship that looks like, where I store a ForeignKey on each of the respective tables.
class Cage(Base):
__tablename__ = 'DimCage'
id = Column(Integer, primary_key=True)
cage = Column(String(8), unique=True, nullable=False)
duns_id = Column(Integer, ForeignKey('DimDuns.id'))
duns = relationship('Duns', uselist=False, back_populates='cage')
class Duns(Base):
__tablename__ = 'DimDuns'
id = Column(Integer, primary_key=True)
duns = Column(String(10), unique=True, nullable=False)
dunsInt = Column(Integer, unique=True, nullable=False)
cage_id = Column(Integer, ForeignKey('DimCage.id'))
cage = relationship('Cage', uselist=False, back_populates='duns')
When I create the tables I get the below error, how to do I set up my foreign keys so I can keep a reference on both tables?
sqlalchemy.exc.AmbiguousForeignKeysError: Can't determine join between 'DimCage' and 'DimDuns'; tables have more than one foreign key constraint relationship between them. Please specify the 'onclause' of this join explicitly.
And During handling of the above exception, another exception occurred:
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship Cage.duns - there are multiple foreign key paths linking the tables. Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference to the parent table.
I believe you only need to store one foreign key for a one-to-one relationship.
See https://docs.sqlalchemy.org/en/13/orm/basic_relationships.html#one-to-one
You shouldn't lose any data access this way. If you remove the duns_id column from Cage, you now access the id by cage.duns.id instead of cage.duns_id.
You could set the child primary key as a foreign key to the parent.
For a design perspective, it is not a bad choice to use foreign keys as primary keys in One-to-One relationships.
class Cage(Base):
__tablename__ = 'DimCage'
id = Column(Integer, primary_key=True)
cage = Column(String(8), unique=True, nullable=False)
class Duns(Base):
__tablename__ = 'DimDuns'
id = Column(Integer, ForeignKey(Cage.id) primary_key=True)
duns = Column(String(10), unique=True, nullable=False)
dunsInt = Column(Integer, unique=True, nullable=False)
cage = relationship('Cage', uselist=False, backref='duns')
Now both cage and duns have the same id. So:
session.add(Cage(id=1, duns=Duns(duns='Duns', dunsInt=10)))
sesion.commit()
id = 1
cage = select(Cage).where(Cage.id == id)
duns = select(Duns).where(Duns.id == cage.id)
assert cage.id == duns.id
Please note that the child cannot exist without the parent.
If the parent is going to be deleted then the child must be deleted first, unless you configure some cascade option.
I've been looking for ways to implement the CONSTRAINT FOREIGN KEY ON DELETE CASCADE in the below UsersAccessMapping model in SQLAlchemy with PyMySQL driver and MariaDB 10.0 with InnoDB in the database.
Python = 3.5.2
SQLAlchemy = 1.1.13
Flask-SQLAlchemy = 2.2
The SQL:
CREATE TABLE Users (
UserID int AUTO_INCREMENT,
Name varchar(200) NOT NULL,
Email varchar(200),
Username varchar(200) NOT NULL,
Password text NOT NULL,
Created datetime,
Updated datetime,
PRIMARY KEY (UserID)
);
CREATE TABLE UsersAccessLevels (
UsersAccessLevelID int AUTO_INCREMENT,
LevelName varchar(100) NOT NULL,
AccessDescription text,
PRIMARY KEY (UsersAccessLevelID)
);
CREATE TABLE UsersAccessMapping (
UsersAccessMappingID int AUTO_INCREMENT,
UserID int NOT NULL,
UsersAccessLevelID int NOT NULL,
PRIMARY KEY (UsersAccessMappingID),
CONSTRAINT fk_useraccess FOREIGN KEY (UserID)
REFERENCES Users(UserID) ON DELETE CASCADE,
CONSTRAINT fk_useraccess_level FOREIGN KEY (UsersAccessLevelID)
REFERENCES UsersAccessLevels(UsersAccessLevelID) ON DELETE CASCADE
);
What I have in my models.py now:
from app import db
class Users(db.Model):
"""All users' information is stored here"""
__tablename__ = "Users"
UserID = db.Column(db.Integer(), primary_key=True)
Name = db.Column(db.String(200), nullable=False)
Email = db.Column(db.String(200))
Username = db.Column(db.String(200), nullable=False)
Password = db.Column(db.Text, nullable=False)
Created = db.Column(db.DateTime)
Updated = db.Column(db.DateTime)
class UsersAccessLevels(db.Model):
"""This defines the various access levels users can have"""
__tablename__ = "UsersAccessLevels"
UsersAccessLevelID = db.Column(db.Integer, primary_key=True)
LevelName = db.Column(db.String(100), nullable=False)
AccessDescription = db.Column(db.Text)
class UsersAccessMapping(db.Model):
"""Each users' access level is defined here"""
__tablename__ = "UsersAccessMapping"
UsersAccessMappingID = db.Column(db.Integer, primary_key=True)
UserID = db.Column(db.Integer, nullable=False)
UsersAccessLevelID = db.Column(db.Integer, nullable=False)
__table_args__ = (
db.ForeignKeyConstraint(
["fk_useraccess", "fk_useraccess_level"],
["Users.UserID", "UsersAccessLevels.UsersAccessLevelID"],
ondelete="CASCADE"
)
)
There is something wrong with the table_args syntax, but I haven't been able to find any examples on how it should be. I found one that was very similar, but in that the third parameter was an empty dict. However, I want to use the ondelete="CASCADE". How would that be added?
When running the python3 manage.py db init, it throws this:
File "/srv/vortech-backend/venv/lib/python3.5/site-packages/sqlalchemy/ext/declarative/base.py", line 196, in _scan_attributes
"__table_args__ value must be a tuple, "
sqlalchemy.exc.ArgumentError: __table_args__ value must be a tuple, dict, or None
I tried changing ondelete="cascade" to a dict {"ondelete": "cascade"}, but that doesn't work either. It gives the same error as above.
Update:
The problem was that the ondelete is supposed to be outside of the tuple, like this:
__table_args__ = (
db.ForeignKeyConstraint(
["fk_useraccess", "fk_useraccess_level"],
["Users.UserID", "UsersAccessLevels.UsersAccessLevelID"]
),
ondelete="CASCADE"
)
However, with this change there is still a syntax error, as ondelete="CASCADE" is not defined. Changing it to a dict {"ondelete": "cascade"} throws this:
File "/srv/vortech-backend/venv/lib/python3.5/site-packages/sqlalchemy/sql/base.py", line 282, in _validate_dialect_kwargs
"named <dialectname>_<argument>, got '%s'" % k)
TypeError: Additional arguments should be named <dialectname>_<argument>, got 'ondelete'
Okay, after some testing and reading, the answer is that SQLAlchemy does some internal magic to achieve it. So, this will accomplish the same result as the SQL:
from app import db # The value is from: db = SQLAlchemy(app)
class Users(db.Model):
"""All users' information is stored here"""
__tablename__ = "Users"
UserID = db.Column(db.Integer(), primary_key=True)
Name = db.Column(db.String(200), nullable=False)
Email = db.Column(db.String(200))
Username = db.Column(db.String(200), nullable=False)
Password = db.Column(db.Text, nullable=False)
Created = db.Column(db.DateTime)
Updated = db.Column(db.DateTime)
class UsersAccessLevels(db.Model):
"""This defines the various access levels users can have"""
__tablename__ = "UsersAccessLevels"
UsersAccessLevelID = db.Column(db.Integer, primary_key=True)
LevelName = db.Column(db.String(100), nullable=False)
AccessDescription = db.Column(db.Text)
class UsersAccessMapping(db.Model):
"""Each users' access level is defined here"""
__tablename__ = "UsersAccessMapping"
UsersAccessMappingID = db.Column(db.Integer, primary_key=True)
UserID = db.Column(
db.Integer, db.ForeignKey("Users.UserID", ondelete="CASCADE"), nullable=False
)
UsersAccessLevelID = db.Column(
db.Integer,
db.ForeignKey("UsersAccessLevels.UsersAccessLevelID", ondelete="CASCADE"),
nullable=False
)
The Constraints and such are automagically handled with the db.ForeignKey() parameters in the column definition. It does not need to be done on the Table directly, like in SQL.
The names for the foreign keys appear to be automatically generated by SQLAlchemy also. Here's how it looks like in the database: