SQLAlchemy Inconsistent Results Self Referential Relationships - python

So I wrote a referral system using FlaskSQLAlchemy, and when I test it, it sometimes works, and sometimes doesn't and I find it very confusing, any help would be appreciated.
Here's the models:
class User(TimestampMixin, UserMixin, db.Model):
# __tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
public_id = db.Column(db.String(MaxLengths.public_id))
email = db.Column(db.String(MaxLengths.email), unique=True, nullable=False)
password = db.Column(db.String(MaxLengths.password))
reset_password_code = db.Column(db.String(MaxLengths.reset_password_code))
attempt_reset_password = db.Column(db.Boolean(), default=False)
is_email_verified = db.Column(db.Boolean(), default=False)
is_age_verified = db.Column(db.Boolean(), default=False)
is_newsletter_registered = db.Column(db.Boolean(), default=True)
balance = db.Column(db.Float(), default=0.0)
lifetime_balance = db.Column(db.Float(), default=0.0)
reviews = db.relationship('Review', backref='user')
ref_code = db.Column(db.String(MaxLengths.ref_code))
# NOTE: Ref relationships here
referred_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
referred_by_user = db.relationship(
'User', foreign_keys=[referred_by_user_id], uselist=False
)
referred_to_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
referred_to_users = db.relationship(
'User', foreign_keys=[referred_to_user_id], post_update=True)
discount_code_used_by_id = db.Column(
db.Integer, db.ForeignKey('discount_code.id'))
profile_pic = db.Column(db.Boolean(), default=False)
id_verification_pics = db.Column(db.JSON(), default=[])
orders = db.relationship('Orders', backref='user')
login_attempts = db.relationship('LoginAttempt', backref='user')
last_seen = db.Column(ArrowType, default=arrow.utcnow())
# TODO: Update this upon placing an order
total_spent = db.Column(db.Float(), default=0.0)
billing_address_id = db.Column(db.Integer, db.ForeignKey('address.id'))
shipping_address_id = db.Column(db.Integer, db.ForeignKey('address.id'))
billing_address = db.relationship(
'Address', foreign_keys=[billing_address_id], uselist=False)
shipping_address = db.relationship(
'Address', foreign_keys=[shipping_address_id], uselist=False)
review_votes = db.relationship('ReviewVote', backref='user')
consumer_id = db.Column(db.String(MaxLengths.consumer_id))
class Orders(TimestampMixin, db.Model):
'''
Orders database model and custom order related functions
'''
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
status = db.Column(db.String(MaxLengths.order_status),
default="Order Pending")
dest_address_id = db.Column(db.Integer, db.ForeignKey('address.id'))
from_address_id = db.Column(db.Integer, db.ForeignKey('address.id'))
dest_address = db.relationship('Address', foreign_keys=[
dest_address_id], uselist=False)
from_address = db.relationship('Address', foreign_keys=[
from_address_id], uselist=False)
products = db.relationship('Product', backref='orders')
subtotal = db.Column(db.Float())
shipping_fee = db.Column(db.Float(), nullable=False)
tax_cost = db.Column(db.Float())
tax_percent = db.Column(db.Float(), nullable=False)
total = db.Column(db.Float())
discount_code = db.relationship(
'DiscountCode', uselist=False)
discount_code_amount = db.Column(db.Float)
user_balance_applied = db.Column(db.Float)
currency = db.Column(db.String(MaxLengths.currency), default="CAD")
date = db.Column(ArrowType, default=arrow.utcnow()
) # datetime.datetime.now()
# TODO: This should exist in the interac_payment obj/json
is_payment_recieved = db.Column(
db.Boolean(), nullable=False, default=False)
interac_payment = db.Column(db.JSON(), default={})
order_number = db.Column(db.String(MaxLengths.order_number), default='')
tracking_number = db.Column(db.String(MaxLengths.tracking_number))
tracking_info = db.Column(db.JSON)
ref_record = db.relationship(
'ReferralRecord', backref='order', uselist=False)
def apply_ref_bonus(self, rate=1.5):
if self.is_payment_recieved:
# user = self.get_user()
if not self.user:
print("No user found for this order")
if not self.user.referred_by_user:
print('This user wasn\'t referred by anyone')
print(self.user.referred_by_user)
if self.user and self.user.referred_by_user:
cashback = (self.subtotal * (rate / 100))
record = ReferralRecord(
referred_by_user_email=self.user.referred_by_user.email,
referred_to_user_email=self.user.email,
cashback_amount=cashback)
self.user.referred_by_user.balance += cashback
self.user.referred_by_user.lifetime_balance += cashback
self.ref_record = record
db.session.add(record)
db.session.add(self.user.referred_by_user)
db.session.add(self.user)
db.session.commit()
class DiscountCode(TimestampMixin, db.Model):
'''
:param id: int primary_key
:param code: str discount code
:param amount: float numeric representation of discount
:param is_percent_off: bool amount is treated as a % discount if True
:param minimum_subtotal: float minimum order subtotal for discount code to be valid for a given order
:param expiration_date: datetime() the date a code is no longer valid
:param uses: int the number of times the code has been used
:param max_uses: int max number of times a code can be used before becoming invalid
:param used_by: a list of User objects who have used this code (for analytical purposes)
'''
id = db.Column(db.Integer(), primary_key=True)
code = db.Column(db.String(MaxLengths.discount_code),
unique=True, nullable=False)
amount = db.Column(db.Float(), nullable=False)
is_percent_off = db.Column(db.Boolean(), default=False)
minimum_subtotal = db.Column(db.Float(), default=0.0)
# TODO: add default via arrow.shift()
start_date = db.Column(ArrowType)
expiration_date = db.Column(ArrowType)
uses = db.Column(db.Integer(), default=0)
max_uses = db.Column(db.Integer(), default=0)
order_id = db.Column(db.Integer, db.ForeignKey('orders.id'))
And here's the test case:
def referr(ref_by_user, ref_to_user):
ref_by_user.referred_to_users.append(ref_to_user)
ref_to_user.referred_by_user = ref_by_user
db.session.add(ref_by_user)
db.session.add(ref_to_user)
db.session.commit()
#app.cli.command('test-ref')
def testref():
db.drop_all()
db.create_all()
user1 = User(email='test1#example.com')
user2 = User(email='test2#example.com')
user3 = User(email='test3#example.com')
order = Orders(shipping_fee=4.00, tax_percent=0.5, subtotal=15.00, total=18.50, is_payment_recieved=True, interac_payment={})
db.session.add(user1)
db.session.add(user2)
db.session.add(user3)
db.session.add(order)
db.session.commit()
# user1.referr(user2)
# user1.referr(user3)
# referr(user1, user2)
# referr(user1, user3)
user1.referred_to_users.append(user2)
user1.referred_to_users.append(user3)
user2.referred_by_user = user1
user3.referred_by_user = user1
print("This should be == 0 - ", user1.balance)
user2.orders.append(order)
# order.user = user2
db.session.add(user1)
db.session.add(user2)
db.session.add(user3)
db.session.add(order)
db.session.commit()
# user2.apply_ref_bonus_from_order(order)
order.apply_ref_bonus()
db.session.commit()
print("This should be > 0 - ", user1.balance)
print(user1.referred_to_users)
However sometimes I get this output:
This should be == 0 - 0.0
This should be > 0 - 0.22499999999999998
[<User 'test2#example.com'>, <User 'test3#example.com'>]
and other times I get this output:
This should be == 0 - 0.0
This user wasn't referred by anyone
None
This should be > 0 - 0.0
[<User 'test2#example.com'>, <User 'test3#example.com'>]
I get different output with the same code, although whenever I try to use a referr() wrapper from either the User model or as a standalone function, I ALWAYS get the latter output. Inconsistent output happens when I don't referr a user via a function & I'm honestly really confused about the whole thing

I wasn't specifying a remote_side param
# NOTE: Ref relationships here
referred_by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
referred_by_user = db.relationship(
'User', foreign_keys=[referred_by_user_id], uselist=False,
remote_side=[id] # This Solved the Issue
)
referred_to_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
referred_to_users = db.relationship(
'User', foreign_keys=[referred_to_user_id], post_update=True)

Related

FastAPI, Pydantic joinedload instead of lazy

I am trying to optimize on the performance of my models, however I can see that FastAPI is loading the relations of my model PurchaseOrderPart as lazy.. This is causing a n+1 problem, which is a big problem.
Is there any way I can make FastAPI load the relations as joined? When I am not using the response model parameter I get most of the data I need, but not all the relations.
class PurchaseOrderPart(Base):
__tablename__ = "purchase_order_part"
id = Column(BigInteger, primary_key=True, index=True)
quantity = Column(Float)
ordered_at = Column(DateTime, nullable=True)
expected_at = Column(DateTime, nullable=True)
confirmed_at = Column(DateTime, nullable=True)
delivered_at = Column(DateTime, nullable=True)
invoiced_at = Column(DateTime, nullable=True)
price_dkk = Column(BigInteger, server_default="0")
deviation = Column(String, nullable=True)
supplier_part_id = Column(BigInteger, ForeignKey("supplier_part.id"))
supplier_id = Column(BigInteger, ForeignKey("supplier.id"))
project_reference_id = Column(BigInteger, ForeignKey(
"project_reference.id"), nullable=True)
stockpile_part_id = Column(BigInteger, ForeignKey(
"stockpile_part.id"), nullable=True)
purchase_order_id = Column(BigInteger, ForeignKey(
"purchase_order.id"), nullable=True)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(),
onupdate=func.current_timestamp())
supplier_part = relationship(
"SupplierPart", uselist=False, foreign_keys=[supplier_part_id], lazy="selectin")
supplier = relationship("Supplier", uselist=False,
foreign_keys=[supplier_id], lazy="selectin")
project_reference = relationship(
"ProjectReference", uselist=False, foreign_keys=[project_reference_id], lazy="selectin")
stockpile_part = relationship(
"StockpilePart", uselist=False, foreign_keys=[stockpile_part_id], lazy="selectin")
purchase_order = relationship(
"PurchaseOrder", uselist=False, foreign_keys=[purchase_order_id], lazy="selectin")
purchase_order_part_notes = relationship(
"PurchaseOrderPartNote", back_populates="purchase_order_part", lazy="selectin")`
class PurchaseOrderPartRelation(PurchaseOrderPart):
purchase_order_part_notes: Optional[List["PurchaseOrderPartNote"]] = None
supplier: Optional[SupplierBase] = None
supplier_part: Optional[SupplierPart] = None
purchase_order: Optional[PurchaseOrder] = None
project_reference: Optional[ProjectReference] = None
stockpile_part: Optional[StockpilePartRelation] = None
part: Optional["Part"]
#################################
# Not Confirmed
#################################
#router.get(
"/not_confirmed",
summary="List all purchase order parts that are ordered but not confirmed",
status_code=status.HTTP_200_OK,
)
def get_not_confirmed_parts(request: Request, end_days_from_now: int = 7,
filter_and: Optional[List[str]] = Query(None),
filter_or: Optional[List[str]] = Query(None),
sort: Optional[List[str]] = Query(None),
page: int = 1,
size: int = 1000,
current_user: User = Depends(
get_current_user),
db: Session = Depends(get_db)):
with tracer.start_span("traces for not confirmed parts") as current_span:
query = db.query(models.PurchaseOrderPart, models.SupplierPart, models.Part).\
filter(and_(
models.PurchaseOrderPart.confirmed_at.is_(None),
models.PurchaseOrderPart.delivered_at.is_(None),
models.PurchaseOrderPart.ordered_at <= ordered_date,
models.PurchaseOrderPart.supplier_part_id == models.SupplierPart.id,
models.Part.primary_supplier_part_id == models.SupplierPart.id
))
# logger.debug(str(query))
# return query
with tracer.start_span("get filtered page trace") as current_span:
results = get_filtered_page(
db,
query,
filter_or,
filter_and,
sort,
page,
size)
return results

Why is only the product, and not its associated movements, being deleted?

I'm trying to delete a product from the Product table while also deleting all the movements of that product from the product movements table(called Movement), but only the product is being deleted, and not its movements. Does anyone know why? This is the code:
#app.route('/products/<int:product_id>/delete', methods=['GET', 'POST'])
def delete_product(product_id):
product = Product.query.get_or_404(product_id)
movements = Movement.query.filter_by(product_id=product_id).all()
for movement in movements:
db.session.delete(movement)
db.session.delete(product)
db.session.commit()
flash('The product has been deleted!', 'success')
return redirect(url_for('products'))
This is the model for the product table:
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False)
product_movements = db.relationship('Movement', backref='item', lazy=True)
and this is the one for the movement table:
class Movement(db.Model):
id = db.Column(db.Integer, primary_key=True)
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
product = db.Column(db.String(50), nullable=False)
from_location_id = db.Column(db.Integer, db.ForeignKey('location.id'))
from_location = db.Column(db.String(50))
to_location_id = db.Column(db.Integer, db.ForeignKey('location.id'))
to_location = db.Column(db.String(50))
quantity = db.Column(db.Integer, nullable=False)
timestamp = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
I figured out what was wrong, I hadn't set set the values for product_id in the Movement table.
if form.validate_on_submit():
product = Product.query.filter_by(id=form.product.data).first()
from_location = Location.query.filter_by(id=form.from_location.data).first()
to_location = Location.query.filter_by(id=form.to_location.data).first()
m = Movement(product_id = product.id, product = product.name, from_location_id = from_location.id, from_location = from_location.name, to_location_id = to_location.id, to_location = to_location.name, quantity = form.quantity.data)
db.session.add(m)
db.session.commit()
movements = Movement.query.all()
products = Product.query.all()
locations = Location.query.all()
return render_template('movements.html', movements=movements, products=products, locations=locations, form=form)
It works now.

SQLAlchemy Relationships Many to Many X 2

I am trying to connect a transaction table to a member table where there are many transactions per member but there are two members per transactions (a buyer and a seller). How do I create a relationship that will allow me to grab a memberID for the seller and a memberID for a buyer for each transaction?
class Member(db.Model, UserMixin):
__tablename__ = 'members'
id = db.Column(db.Integer, primary_key = True)
# exchangeID = db.Column(db.Integer(6),db.ForeignKey('exchanges.exchangeID'),nullable=False)
company_name = db.Column(db.String(64), nullable=False)
category = db.Column(db.String(64), nullable=False, default='Other')
description = db.Column(db.Text)
profile_image = db.Column(db.String(20), nullable=False, default='default_profile.png')
email = db.Column(db.String(64), unique=True, index=True)
# username = db.Column(db.String(64), unique=True, index=True, nullable=False)
# password_hash = db.Column(db.String(128), nullable=False)
address = db.Column(db.String(128))
phone = db.Column(db.Integer, nullable=False)
fein = db.Column(db.Integer)
webaddress = db.Column(db.String(64))
twitter = db.Column(db.String(24))
exchange_approved = db.Column(db.Boolean, default=False)
users = db.relationship('User', backref='company',lazy=True)
transactions = db.relationship('Transaction', backref='company',lazy=True)
listings = db.relationship('Listing', backref='company',lazy=True)
credit = db.relationship('Credit', backref='company',lazy=True)
def __init__(self,exchange_name,company_name,category,mail,address,phone,fein):
# self.exchange_name = exchange_name
self.company_name = company_name
self.category = category
self.email = email
self.address = address
self.phone = phone
self.fein = fein
def __repr__(self):
return f"Company Name: {self.company_name}"
class User(db.Model, UserMixin):
__tablename__ = 'users'
members = db.relationship(Member)
id = db.Column(db.Integer, primary_key = True)
companyID = db.Column(db.Integer, db.ForeignKey('members.id'),nullable=False)
buy_transactions = db.relationship('Transaction', backref='buyer',lazy=True)
sell_transactions = db.relationship('Transaction', backref='seller',lazy=True)
# exchangeID = db.Column(db.Integer(6), db.ForeignKey('exchanges.exchangeID'),nullable=False)
email = db.Column(db.String(64), unique=True, index=True, nullable=False)
username = db.Column(db.String(64), unique=True, index=True, nullable=False)
password_hash = db.Column(db.String(128), nullable=False)
phone_number = db.Column(db.Integer, nullable=False)
user_type = db.Column(db.String(14))
member_approved = db.Column(db.Boolean, default=False)
limited_trade = db.Column(db.Boolean, default=True)
member_imposed_limit = db.Column(db.Integer, default=0)
def __init__(self,username,password,company_name,email,phone):
self.username = username
self.password_hash = generate_password_hash(password)
self.companyID = companyID
self.email = email
self.phone = phone
def check_password(self,password):
# https://stackoverflow.com/questions/23432478/flask-generate-password-hash-not-constant-output
return check_password_hash(self.password_hash,password)
def __repr__(self):
return f"UserName: {self.username}"
class Transaction(db.Model, UserMixin):
__tablename__ = 'transactions'
members = db.relationship(Member)
transactionID = db.Column(db.Integer, primary_key=True)
date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
companyID = db.Column(db.Integer, db.ForeignKey('members.id'),nullable=False)
sellerID = db.Column(db.Integer, db.ForeignKey('users.id'),nullable=False)
buyerID = db.Column(db.Integer, db.ForeignKey('users.id'),nullable=False)
seller = relationship("User", foreign_keys='Transaction.sellerID')
buyer = relationship("User", foreign_keys='Transaction.buyerID')
amount = db.Column(db.Numeric(5,2), nullable=False)
commission = db.Column(db.Numeric(5,2), nullable=False)
transactionDate = db.Column(db.DateTime, server_default=db.func.now())
approved = db.Column(db.Boolean, default=False)
commission_paid = db.Column(db.Boolean, default=False)
posted = db.Column(db.Boolean, default=False)
def __init__(self,sellerID,buyerID,amount):
self.sellerID = sellerID
self.buyerID = buyerID
self.amount = amount
def __repr__(self):
return f"Trasaction Id: {self.transactionID} --- Date: {self.date} --- Amount: {self.amount}"
I am getting a 'relationship' not defined but that is obvious as I don't understand how to connect the tables as described above.
What you are missing here is an association table, that should link the two tables which need to have many-many relationship. Below is a very simple example of a many-many relationship which should give you an idea on how to proceed from there.
This is how your models.py should look like
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + 'testdb.sql'
db = SQLAlchemy(app)
# Please note the below association table needs to be actual database table and not a model class.
stu_subs = db.Table('stu_subs', db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
db.Column('subject_id', db.Integer, db.ForeignKey('subjects.id')))
class Student(db.Model):
__tablename__ = 'students'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30))
# Note the secondary attribute below, that actually sets up a many-to-many relationship
subj = db.relationship('Subjects', secondary=stu_subs, backref=db.backref('student', lazy='dynamic'))
class Subjects(db.Model):
__tablename__ = 'subjects'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20))
Finally this is how you are going to access the table fields from each table.
>>> db.create_all()
>>> from dummy import Student, Subjects
>>> student1 = Student(name="Student1")
>>> student2 = Student(name="Student2")
>>> subj1 = Subjects(name='subject1')
>>> subj2 = Subjects(name='subject2')
>>> subj3 = Subjects(name='subject3')
>>> subj4 = Subjects(name='subject4')
>>> db.session.add_all([student1, student2, subj1, subj2,subj3,subj4])
>>> db.session.commit()
>>> stu1 = Student.query.filter_by(id=1)
>>> stu2 = Student.query.filter_by(id=2).first()
>>> sub1 = Student.query.filter_by(name='subject1').first()
>>> sub2 = Subjects.query.filter_by(name='subject2').first()
>>> sub3 = Subjects.query.filter_by(name='subject3').first()
>>> sub4 = Subjects.query.filter_by(name='subject4').first()
>>> stu1.subj.extend([sub1,sub2,sub4])
>>> stu2.subj.extend([sub2,sub4])
>>> stu1.subj
[<Subjects 1>, <Subjects 2>, <Subjects 4>]
>>> subj2.student.all()
[<Student 2>, <Student 1>]
>>> >>> stu1.subj[0].name
'subject1'
>>> for subj in stu1.subj:
... print(subj.name)
...
subject1
subject2
subject4
>>> for stu in subj2.student:
... print(stu.name)
...
Student2
Student1
>>>
This is just a very basic example, ofcourse you need to extend it in your case. You would have to create multiple association tables for each db.realtionship attribute and so on.
ok...this was pretty dumb. I stopped getting the error once I put db in front of relationship so db.relationship. I haven't built out the transaction form yet to test the relationship back and forth but the site at least comes up and runs. Thx.

Order of declaring classes in SqlAlchemy

I'm sory for this newby question, but I can't figure it out for a couple days and it is driving me nuts.
I'm building a small project in order to learn flask, SqlAlchemy and Postrges.
I have major problems with declaring classes in SqlAlchemy. I have already simplified models by removing all many-to-many relationships. However, now I have new problems even with one-to many relationships, although I think I have tried all the possible options. Maybe there's a typo that I keep on overlooking, or I don't grasp something fundamental. Please let me know...
So, I have classes declared as follows in my models.py:
#login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key = True)
username = db.Column(db.String(20), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password = db.Column(db.String(60), nullable=False)
date_registered = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
role = db.Column(db.Integer, nullable=False)
role_exp_date = db.Column(db.DateTime)
#o2o
personal_datas = db.relationship('PersonalData', uselist=False, backref='user', lazy=True)
persons = db.relationship('Person', uselist=False, backref='user', lazy=True)
#o2m
posts = db.relationship('Post', backref='author', lazy=True)
comments = db.relationship('PostComment', backref='author', lazy=True)
projects_owned = db.relationship('ConstrProject', backref='owner', lazy=True)
attachments = db.relationship('Attachment', backref='author', lazy=True)
def __repr__(self):
return f"{self.username} ({self.email})"
class PersonalData(db.Model):
id = db.Column(db.Integer, primary_key = True)
date_birth = db.Column(db.DateTime)
image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
interests = db.Column(db.Text)
experties = db.Column(db.Text) #Потом сделать отдельную таблицу...
#o2o
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
#o2m
class Person(db.Model):
id = db.Column(db.Integer, primary_key = True)
first_name = db.Column(db.String(30), nullable=False)
middle_name = db.Column(db.String(40), nullable=False)
last_name = db.Column(db.String(60), nullable=False)
email = db.Column(db.String(120))
license = db.Column(db.String(120))
address = db.Column(db.String(240))
telephone = db.Column(db.String(30))
#o2o
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
#o2m
signers = db.relationship('Signer', backref='person', lazy=True)
def __repr__(self):
return f"{self.last_name.Capitalize} {self.first_name[0].Upper}. {self.middle_name[0].Upper}."
class ConstrProject(db.Model):
__tablename__ = 'constrproject'
id = db.Column(db.Integer, primary_key = True)
name = db.Column(db.String(120), nullable=False, default='New Project')
full_title = db.Column(db.Text, default='New Project')
notes = db.Column(db.Text)
public = db.Column(db.Boolean, default=True) #? check expamples
date_created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
date_last_edit = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
document_template = db.Column(db.Integer, nullable=False, default=1) #later to m2m
print_settings = db.Column(db.Integer, nullable=False, default=1) #later to m2m
address = db.Column(db.String(240))
#o2m
documents = db.relationship('Document', backref='project', lazy=True)
#m2o
owner_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) #+ #default = CurrentUser
developer_id = db.Column(db.Integer, db.ForeignKey('company.id'))
main_contractor_id = db.Column(db.Integer, db.ForeignKey('company.id'))
architect_id = db.Column(db.Integer, db.ForeignKey('company.id'))
subcontractor_id = db.Column(db.Integer, db.ForeignKey('company.id'))
other_id = db.Column(db.Integer, db.ForeignKey('company.id'))
developer = db.relationship('Company', foreign_keys=[developer_id], back_populates='constr_projects_developed')
main_contractor = db.relationship('Company', foreign_keys=[main_contractor_id], back_populates='constr_projects_main_contracts')
architect = db.relationship('Company', foreign_keys=[architect_id], back_populates='constr_projects_architect')
subcontractor = db.relationship('Company', foreign_keys=[subcontractor_id], back_populates='constr_projects_subcontracts')
other = db.relationship('Company', foreign_keys=[other_id], back_populates='constr_projects_other')
tech_control_reps_id = db.Column(db.Integer, db.ForeignKey('signer.id'), nullable=False)
main_contractor_reps_id = db.Column(db.Integer, db.ForeignKey('signer.id'), nullable=False)
architect_reps_id = db.Column(db.Integer, db.ForeignKey('signer.id'), nullable=False)
subcontractor_reps_id = db.Column(db.Integer, db.ForeignKey('signer.id'), nullable=False)
other_reps_id = db.Column(db.Integer, db.ForeignKey('signer.id'), nullable=False)
tech_control_reps = db.relationship('Signer', foreign_keys=[tech_control_reps_id], back_populates='tech_control_projects')
main_contractor_reps = db.relationship('Signer', foreign_keys=[main_contractor_reps_id], back_populates='main_contractor_projects')
architect_reps = db.relationship('Signer', foreign_keys=[architect_reps_id], back_populates='architect_projects')
subcontractor_reps = db.relationship('Signer', foreign_keys=[subcontractor_reps_id], back_populates='subcontractor_projects')
other_reps = db.relationship('Signer', foreign_keys=[other_reps_id], back_populates='others_projects')
def __repr__(self):
return f"Site: {self.name}, (id{self.id})"
class Signer(db.Model):
id = db.Column(db.Integer, primary_key = True)
decree = db.Column(db.String(120))
job_title = db.Column(db.String(120))
date_duty_start = db.Column(db.DateTime)
date_duty_end = db.Column(db.DateTime)
#o2m
person_id = db.Column(db.Integer, db.ForeignKey('person.id'), nullable=False)
company_id = db.Column(db.Integer, db.ForeignKey('company.id'), nullable=False)
#m2o
tech_control_projects = db.relationship('ConstrProject', back_populates='tech_control_reps')
main_contractor_projects = db.relationship('ConstrProject', back_populates='main_contractor_reps')
architect_projects = db.relationship('ConstrProject', back_populates='architect_reps')
subcontractor_projects = db.relationship('ConstrProject', back_populates='subcontractor_reps')
others_projects = db.relationship('ConstrProject', back_populates='other_reps')
def __repr__(self):
return f"{self.job_title} as per {self.decree}." #название компании как подтянуть
class Company(db.Model):
id = db.Column(db.Integer, primary_key = True)
name = db.Column(db.String(60))
full_title = db.Column(db.String(240))
tin = db.Column(db.Integer)
kpp = db.Column(db.Integer)
ogrn = db.Column(db.Integer)
email = db.Column(db.String(120))
address = db.Column(db.String(240))
telephone = db.Column(db.String(30))
#o2m
license_number = db.Column(db.String(40), nullable = False)
license_date_issued = db.Column(db.DateTime)
license_category = db.Column(db.String(120), default = '2nd')
license_issued_by = db.Column(db.String(120))
license_issued_by_tin = db.Column(db.Integer)
license_issued_by_kpp = db.Column(db.Integer)
license_issued_by_ogrn = db.Column(db.Integer)
signers = db.relationship('Signer', backref='company', lazy=True)
constr_projects_developed = db.relationship('ConstrProject', back_populates='developer')
constr_projects_main_contracts = db.relationship('ConstrProject', back_populates='main_contractor')
constr_projects_architect = db.relationship('ConstrProject', back_populates='architect')
constr_projects_subcontracts = db.relationship('ConstrProject', back_populates='subcontractor')
constr_projects_other = db.relationship('ConstrProject', back_populates='other')
def __repr__(self):
return f"{self.name}"
class Post(db.Model):
id = db.Column(db.Integer, primary_key = True)
title = db.Column(db.String(100), nullable=False)
date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
content = db.Column(db.Text, nullable=False)
#o2m
comments = db.relationship('PostComment', backref='Post', lazy=True)
#m2o
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
def __repr__(self):
return f"Post('{self.title}', '{self.date_posted}')"
class PostComment(db.Model):
id = db.Column(db.Integer, primary_key = True)
date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
content = db.Column(db.Text, nullable=False)
#m2o
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
def __repr__(self):
return f"Comment('{self.id}', '{self.date_posted}')"
class Document(db.Model):
id = db.Column(db.Integer, primary_key = True)
type = db.Column(db.String(60), nullable=False, default='АОСР')
date_last_edit = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
notes = db.Column(db.Text)
public = db.Column(db.Boolean, default=True)
number = db.Column(db.String(20), nullable=False)
date = db.Column(db.DateTime, default=datetime.utcnow)
job_name = db.Column(db.Text) #? обязательный? на каком этапе делать проверку?
job_place = db.Column(db.String(200))
date_job_start = db.Column(db.DateTime, default=datetime.utcnow)
date_job_end = db.Column(db.DateTime, default=datetime.utcnow)
regulations = db.Column(db.Text)
next_job_allowed = db.Column(db.String(240))
attachments_user_defined = db.Column(db.Text)
#o2m
attachments = db.relationship('Attachment', backref='document', lazy=True)
#m2o
project_id = db.Column(db.Integer, db.ForeignKey('constrproject.id'), nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
#m2m
arch_docs = db.Column(db.Text)
building_materials = db.Column(db.Text)
work_drawings = db.Column(db.Text)
def __repr__(self):
return f"АОСР ('{self.number}', '{self.job_name} {self.job_place}', '{self.project}' )"
class Attachment(db.Model):
id = db.Column(db.Integer, primary_key = True)
type_of_document = db.Column(db.String(60), nullable=False, default="QAC")
number = db.Column(db.String(50), nullable=False)
date = db.Column(db.DateTime)
date_valid_start = db.Column(db.DateTime)
date_valid_end = db.Column(db.DateTime)
contents = db.Column(db.Text)
type_of_file = db.Column(db.String(10), nullable=False, default = 'jpg')
image_file = db.Column(db.String(20), nullable=False)
#m2o
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
document_id = db.Column(db.Integer, db.ForeignKey('document.id'), nullable=False)
def __repr__(self):
if self.text:
return f'{self.text}'
return f'+{self.type_of_document} id{self.id} ({self.type_of_file})'
I cannot understand why, when I try to create an instance of a "Document"
I get errors like this:
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join
condition between parent/child tables on relationship
Signer.tech_control_projects - 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.
or
sqlalchemy.exc.InvalidRequestError: When initializing mapper
Mapper|ConstrProject|constrproject, expression 'tech_control_reps_id'
failed to locate a name ("name 'tech_control_reps_id' is not
defined"). If this is a class name, consider adding this
relationship() to the class
after both dependent classes have been defined.
I haven't even tried to create these classes. Adding "foreign_keys" doesn't seem to help either. all relationships declared as strings. I tried to use lambda's also without success.
Nevertheless moving the order of class declearations alters the error messages i get...
I cannot find any good examples (resources) of more complicated databases with multiple many-to-many and one-to-many relationships in Each class. Usually examples are very basic and obvious.
So I would really appreciate if you post links to such projects or tutorials.
I wanted to learn some of it myself so I created a working example with one-to-one and multiple one-to-many relationships based on your code (User, Person, Post and Comment classes). I hope it will be a good (simple but not trivial) example for you.
'''SQLAlchemy one-to-one and one-to-many SSCCE'''
import sqlalchemy
import sqlalchemy.ext.declarative
from passlib.hash import pbkdf2_sha256
Base = sqlalchemy.ext.declarative.declarative_base()
class User(Base):
__tablename__ = 'users'
user_id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
username = sqlalchemy.Column(sqlalchemy.String(20), unique=True, nullable=False)
email = sqlalchemy.Column(sqlalchemy.String(120), unique=True, nullable=False)
password = sqlalchemy.Column(sqlalchemy.String(100), nullable=False)
registered = sqlalchemy.Column(
sqlalchemy.DateTime(timezone=True),
nullable=False,
server_default=sqlalchemy.func.now()
)
#o2o
person = sqlalchemy.orm.relationship(
'Person',
uselist=False,
back_populates='user',
lazy='joined'
)
#o2m
posts = sqlalchemy.orm.relationship('Post', back_populates='user')
comments = sqlalchemy.orm.relationship('Comment', back_populates='user')
def __repr__(self):
return f'{self.username} ({self.email})'
class Person(Base):
__tablename__ = 'persons'
person_id = sqlalchemy.Column(
sqlalchemy.Integer,
sqlalchemy.ForeignKey('users.user_id'),
primary_key=True
)
first_name = sqlalchemy.Column(sqlalchemy.String(30), nullable=False)
middle_name = sqlalchemy.Column(sqlalchemy.String(40), nullable=False)
last_name = sqlalchemy.Column(sqlalchemy.String(60), nullable=False)
#o2o
user = sqlalchemy.orm.relationship('User', back_populates='person', lazy='joined')
def __repr__(self):
return (
f'{self.last_name.upper()}'
f' {self.first_name[:1].upper()}.'
f' {self.middle_name[:1].upper()}.'
)
class Post(Base):
__tablename__ = 'posts'
post_id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
title = sqlalchemy.Column(sqlalchemy.String(100), nullable=False)
posted = sqlalchemy.Column(
sqlalchemy.DateTime(timezone=True),
nullable=False,
server_default=sqlalchemy.func.now()
)
content = sqlalchemy.Column(sqlalchemy.Text, nullable=False)
#o2m
comments = sqlalchemy.orm.relationship('Comment', back_populates='post')
#m2o
user_id = sqlalchemy.Column(
sqlalchemy.Integer,
sqlalchemy.ForeignKey('users.user_id'),
nullable=False
)
user = sqlalchemy.orm.relationship('User', uselist=False, back_populates='posts', lazy='joined')
def __repr__(self):
return f'Post({self.title!r}, {self.posted!r})'
class Comment(Base):
__tablename__ = 'comments'
comment_id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
posted = sqlalchemy.Column(
sqlalchemy.DateTime(timezone=True),
nullable=False,
server_default=sqlalchemy.func.now()
)
content = sqlalchemy.Column(sqlalchemy.Text, nullable=False)
#m2o
user_id = sqlalchemy.Column(
sqlalchemy.Integer,
sqlalchemy.ForeignKey('users.user_id'),
nullable=False
)
user = sqlalchemy.orm.relationship(
'User',
uselist=False,
back_populates='comments',
lazy='joined'
)
post_id = sqlalchemy.Column(
sqlalchemy.Integer,
sqlalchemy.ForeignKey('posts.post_id'),
nullable=False
)
post = sqlalchemy.orm.relationship(
'Post',
uselist=False,
back_populates='comments',
lazy='joined'
)
def __repr__(self):
return f'Comment({self.comment_id!r}, {self.posted!r})'
def main():
engine = sqlalchemy.create_engine(
'postgresql+psycopg2:///stack',
echo=True,
server_side_cursors=True,
use_batch_mode=True
)
Base.metadata.create_all(engine)
Session = sqlalchemy.orm.sessionmaker(bind=engine)
session = Session()
session.commit()
try:
the_user = session.query(User).filter(User.username == 'example').one()
except sqlalchemy.orm.exc.NoResultFound:
the_user = User(
username='example',
email='example#example.com',
password=pbkdf2_sha256.hash('correct horse battery staple')
)
the_user.person = Person(first_name='Ex', middle_name='', last_name='Ample')
session.add(the_user)
print(the_user)
print(the_user.person)
if not the_user.posts:
the_user.posts.append(Post(title='First post', content='Lorem ipsum'))
session.commit()
print(the_user.posts[0])
if not the_user.posts[0].comments:
the_user.posts[0].comments.append(Comment(content='Me too', user=the_user))
session.commit()
print(the_user.posts[0].comments[0])
if __name__ == '__main__':
main()
Several comments:
I don't think it is easy to have foreign-keys both ways for a mandatory one-to-one relationship.
I made both User and Person use the same id numbers, as they are one-to-one.
I made the PostgreSQL now() as the default value for timestamps, instead of client-side utcnow.
I made the timestamps use "timestamp with timezone" type - the "timestamp" type (without timezone) is an abomination.
"user" is a bad name for a table, as this is also a keyword in PostgreSQL, so I changed it to "users". Other tables also changed to plural form for consistency.
A secure password storage is a must.
I used back_populates consistently, as it is more explicit and works better with static code analyzers than backref.

Many to Many relationship calling on Flask SQL Alchemy

I have the following badge (achievement) system database structure:
class Base(db.Model):
__abstract__ = True
id = db.Column(db.Integer, primary_key=True)
date_created = db.Column(db.DateTime, default=db.func.current_timestamp())
date_modified = db.Column(db.DateTime, default=db.func.current_timestamp(),
onupdate=db.func.current_timestamp())
class User(UserMixin, Base):
__tablename__ = 'users'
username = db.Column(db.String(20), nullable=False, unique=True)
email = db.Column(db.String(50), nullable=False, unique=True)
password = db.Column(db.String(200), nullable=False)
name = db.Column(db.String(30), nullable=False)
badges = db.relationship('UserBadge', backref='ubadge',
lazy='dynamic')
class Badge(Base):
__tablename__ = 'badges'
name = db.Column(db.String(35), unique=True)
description = db.Column(db.String(300))
imgfile = db.Column(db.String(80))
badges = db.relationship('UserBadge', backref='badge',
lazy='dynamic')
def __repr__(self):
return '<Achievement: {} - {}>'.format(self.name, self.description)
class UserBadge(Base):
__tablename__ = 'userbadges'
user_id = db.Column(db.Integer(), db.ForeignKey('users.id'))
badge_id = db.Column(db.Integer(), db.ForeignKey('badges.id'))
def __repr__(self):
return '<Achievement: {} - {}>'.format(self.user_id, self.badge_id)
So i can return all the badges by a specific user, using:
ubadges = UserBadge.query.filter_by(user_id=user.id).all()
It returns:
[<Achievement: 1 - 1>]
But instead of 1 (user_id) and 1 (badge_id) i want to show the users.name and badges.name. How can i access those attributes?
In your UserBadge class, just use:
def __repr__(self):
return '<Achievement: {} - {}>'.format(self.ubadge.name, self.badge.name)
It has both properties because you set them up using backref in the other classes.
PS: You might need to change the User backref to user, and then use self.user.name int he function above

Categories

Resources