Use of M2M Table and relationship to get specific data in sqlalchemy - python

I have Table
# File : MyRelations.py
ACC_ADD_TABLE = Table('acc_add_rel', METADATA,
Column('acc_id', ForeignKey('acc.id'),
nullable=False),
Column('add_id', ForeignKey('address.id'),
nullable=False),
PrimaryKeyConstraint('add_id', 'acc_id'),
)
# File : Address.py
class Address(Base):
id = Column(Integer, primary_key=True,)
type = Column(String(length=10), nullable=False)
# File : Account.py
class Account(Base):
id = Column(Integer, primary_key=True,)
addresses = relationship('Address',
secondary=ACC_ADD_TABLE
)
# default_address = relationship('Address',
# secondary=ACC_ADD_TABLE,
# primaryjoin=and_("ACC_ADD_TABLE.add_id==Address.id",
# "ACC_ADD_TABLE.acc_id==Account.id",
# "Address.type='default'")
# )
As per the example I want to access the all default addresses in account. I can use declared_attr or can write the function but is there any way to combine Table and Class attribute in single and_ operation?
Note: Address.py and Account.py both are different files and due to cycle dependency I cant import any model in other model
Thx for you help.

This works without requiring an import:
default_address = relationship('Address',
secondary=ACC_ADD_TABLE,
primaryjoin="acc.c.id==acc_add_rel.c.acc_id",
secondaryjoin="and_(address.c.id==acc_add_rel.c.add_id, address.c.type=='default')",
#uselist = True,
)
If you are certain that there is only one default address, you might use uselist=True for convenience.
Sometimes I prefer the other structure for such situations though: add a column to the Account table: default_address_id and build 1-[0..1] relationship based on this column, still checking that the referenced Address is also part of Account.addresses M-N relationship.
On the side note, a typo: in your (commented) code you should use == instead of = in "Address.type='default'". This does not solve the problem though.

Related

Why am I unable to generate a query using relationships?

I'm experimenting with relationship functionality within SQLAlchemy however I've not been able to crack it. The following is a simple MRE:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, ForeignKey, Integer, create_engine
from sqlalchemy.orm import relationship, sessionmaker
Base = declarative_base()
class Tournament(Base):
__tablename__ = "tournament"
__table_args__ = {"schema": "belgarath", "extend_existing": True}
id_ = Column(Integer, primary_key=True)
tournament_master_id = Column(Integer, ForeignKey("belgarath.tournament_master.id_"))
tournament_master = relationship("TournamentMaster", back_populates="tournament")
class TournamentMaster(Base):
__tablename__ = "tournament_master"
__table_args__ = {"schema": "belgarath", "extend_existing": True}
id_ = Column(Integer, primary_key=True)
tour_id = Column(Integer, index=True)
tournament = relationship("Tournament", back_populates="tournament_master")
engine = create_engine("mysql+mysqlconnector://root:root#localhost/")
Session = sessionmaker(bind=engine)
session = Session()
qry = session.query(Tournament.tournament_master.id_).limit(100)
I was hoping to be able to query the id_ field from the tournament_master table through a relationship specified in the tournament table. However I get the following error:
AttributeError: Neither 'InstrumentedAttribute' object nor 'Comparator' object associated with Tournament.tournament_master has an attribute 'id_'
I've also tried replacing the two relationship lines with a single backref line in TournamentMaster:
tournament = relationship("Tournament", backref="tournament_master")
However I then get the error:
AttributeError: type object 'Tournament' has no attribute 'tournament_master'
Where am I going wrong?
(I'm using SQLAlchemy v1.3.18)
Your ORM classes look fine. It's the query that's incorrect.
In short you're getting that "InstrumentedAttribute" error because you are misusing the session.query method.
From the docs the session.query method takes as arguments, "SomeMappedClass" or "entities". You have 2 mapped classes defined, Tournament, and TournamentMaster. These "entities" are typically either your mapped classes (ORM objects) or a Column of these mapped classes.
However you are passing in Tournament.tournament_master.id_ which is not a "MappedClass" or a column and thus not an "entity" that session.query can consume.
Another way to look at it is that by calling Tournament.tournament_master.id_ you are trying to access a 'TournamentMaster' record (or instance) from the 'Tournament' class, which doesn't make sense.
It's not super clear to me what exactly you hoping to return from the query. In any case though here's a start.
Instead of
qry = session.query(Tournament.tournament_master.id_).limit(100)
try
qry = session.query(Tournament, TournamentMaster).join(TournamentMaster).limit(100)
This may also work (haven't tested) to only return the id_ field, if that is you intention
qry = session.query(Tournament, TournamentMaster).join(Tournament).with_entities(TournamentMaster.id_).limit(100)

Is it possible to use session.insert for one to main relationships in SQLAlchemy?

I have read in the following link:
Sqlalchemy adding multiple records and potential constraint violation
That using SQLAlchemy core library to perform the inserts is much faster option, rather than the ORM's session.add() method:
i.e:
session.add()
should be replaced with:
session.execute(Entry.__table__.insert(), params=inserts)
In the following code I have tried to replace .add with .insert:
from sqlalchemy import Column, DateTime, String, Integer, ForeignKey, func
from sqlalchemy.orm import relationship, backref
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Department(Base):
__tablename__ = 'department'
id = Column(Integer, primary_key=True)
name = Column(String)
class Employee(Base):
__tablename__ = 'employee'
id = Column(Integer, primary_key=True)
name = Column(String)
# Use default=func.now() to set the default hiring time
# of an Employee to be the current time when an
# Employee record was created
hired_on = Column(DateTime, default=func.now())
department_id = Column(Integer, ForeignKey('department.id'))
# Use cascade='delete,all' to propagate the deletion of a Department onto its Employees
department = relationship(
Department,
backref=backref('employees',
uselist=True,
cascade='delete,all'))
from sqlalchemy import create_engine
engine = create_engine('postgres://blah:blah#blah:blah/blah')
from sqlalchemy.orm import sessionmaker
session = sessionmaker()
session.configure(bind=engine)
Base.metadata.create_all(engine)
d = Department(name="IT")
emp1 = Employee(name="John", department=d)
s = session()
s.add(d)
s.add(emp1)
s.commit()
s.delete(d) # Deleting the department also deletes all of its employees.
s.commit()
s.query(Employee).all()
# Insert Option Attempt
from sqlalchemy.dialects.postgresql import insert
d = insert(Department).values(name="IT")
d1 = d.on_conflict_do_nothing()
s.execute(d1)
emp1 = insert(Employee).values(name="John", department=d1)
emp1 = emp1.on_conflict_do_nothing()
s.execute(emp1)
The error I receive:
sqlalchemy.exc.CompileError: Unconsumed column names: department
I can't quite understand the syntax and how to do it in the right way, I'm new to the SQLAlchemy.
It looks my question is similar to How to get primary key columns in pd.DataFrame.to_sql insertion method for PostgreSQL "upsert"
, so potentially by answering either of our questions, you could help two people at the same time ;-)
I am new to SQLAlchemy as well, but this is what I found :
Using your exact code, adding department only didn't work using "s.execute(d1)", so I changed it to the below and it does work :
with engine.connect() as conn:
d = insert(Department).values(name="IT")
d1 = d.on_conflict_do_nothing()
conn.execute(d1)
I found on SQLAlchemy documentation that in the past it was just a warning when you try to use a virtual column that doesn't really exist. But from version 0.8, it has been changed to an exception.
As a result, I am not sure if you can do that using the insert. I think that SQLAlchemy does it behind the scene in some other way when using session.add(). Maybe some experts can elaborate here.
I hope that will help.

can I store a Dictionary as the property of an object?

Using Python/Flask/SQLAlchemy/Heroku.
Want to store dictionaries of objects as properties of an object:
TO CLARIFY
class SoccerPlayer(db.Model):
name = db.Column(db.String(80))
goals_scored = db.Column(db.Integer())
^How can I set name and goals scored as one dictionary?
UPDATE: The user will input the name and goals_scored if that makes any difference.
Also, I am searching online for an appropriate answer, but as a noob, I haven't been able to understand/implement the stuff I find on Google for my Flask web app.
I would second the approach provided by Sean, following it you get properly
normalized DB schema and can easier utilize RDBMS to do the hard work for you. If,
however, you insist on using dictionary-like structure inside your DB, I'd
suggest to try out hstore
data type which allows you to store key/value pairs as a single value in
Postgres. I'm not sure if hstore extension is created by default in Postgres
DBs provided by Heroku, you can check that by typing \dx command inside
psql. If there are no lines with hstore in them, you can create it by
typing CREATE EXTENSION hstore;.
Since hstore support in SQLAlchemy is available in version 0.8 which is not
released yet (but hopefully will be in coming weeks), you need to install it
from its Mercurial repository:
pip install -e hg+https://bitbucket.org/sqlalchemy/sqlalchemy#egg=SQLAlchemy
Then define your model like this:
from sqlalchemy.dialects.postgresql import HSTORE
from sqlalchemy.ext.mutable import MutableDict
class SoccerPlayer(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False, unique=True)
stats = db.Column(MutableDict.as_mutable(HSTORE))
# Note that hstore only allows text for both keys and values (and None for
# values only).
p1 = SoccerPlayer(name='foo', stats={'goals_scored': '42'})
db.session.add(p1)
db.session.commit()
After that you can do the usual stuff in your queries:
from sqlalchemy import func, cast
q = db.session.query(
SoccerPlayer.name,
func.max(cast(SoccerPlayer.stats['goals_scored'], db.Integer))
).group_by(SoccerPlayer.name).first()
Check out HSTORE docs
for more examples.
If you are storing such information in a database I would recommend another approach:
class SoccerPlayer(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
team_id = db.Column(db.Integer, db.ForeignKey('Team.id'))
stats = db.relationship("Stats", uselist=False, backref="player")
class Team(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
players = db.relationship("SoccerPlayer")
class Stats(db.Model):
id = db.Column(db.Integer, primary_key=True)
player_id = db.Column(db.Integer, db.ForeignKey('SoccerPlayer.id'))
goals_scored = db.Column(db.Integer)
assists = db.Column(db.Integer)
# Add more stats as you see fit
With this model setup you can do crazy things like this:
from sqlalchemy.sql import func
max_goals_by_team = db.session.query(Team.id,
func.max(Stats.goals_scored).label("goals_scored")
). \
join(SoccerPlayer, Stats). \
group_by(Team.id).subquery()
players = SoccerPlayer.query(Team.name.label("Team Name"),
SoccerPlayer.name.label("Player Name"),
max_goals_by_team.c.goals_scored). \
join(max_goals_by_team,
SoccerPlayer.team_id == max_goals_by_team.c.id,
SoccerPlayer.stats.goals_scored == max_goals_by_team.c.goals_scored).
join(Team)
thus making the database do the hard work of pulling out the players with the highest goals per team, rather than doing it all in Python.
Not even django(a bigger python web framework than flask) doesn't support this by default. But in django you can install it, it's called a jsonfield( https://github.com/bradjasper/django-jsonfield ).
What i'm trying to tell you is that not all databases know how to store binaries, but they do know how to store strings and jsonfield for django is actually a string that contains the json dump of a dictionary.
So, in short you can do in flask
import simplejson
class SoccerPlayer(db.Model):
_data = db.Column(db.String(1024))
#property
def data(self):
return simplejson.loads(self._data)
#data.setter
def data(self, value):
self._data = simplejson.dumps(value)
But beware, this way you can only assign the entire dictionary at once:
player = SoccerPlayer()
player.data = {'name': 'Popey'}
print player.data # Will work as expected
{'name': 'Popey'}
player.data['score'] = '3'
print player.data
# Will not show the score becuase the setter doesn't know how to input by key
{'name': 'Popey'}

Sqlalchemy class is not iterable / converting relationship to a set

I'm trying to convert a one-to-many part of a many-to-many relationship to a python set(). See the following play example for code:
message_tos = Table('message_tos', Base.metadata,
Column('message_id', Integer, ForeignKey('message.id')),
Column('address_id', Integer, ForeignKey('address.id'))
)
class Address(Base):
address = Column(String, nullable=False, unique=True)
emails = relationship('Message')
def __init__(self, address):
self.address = address
class Email(Base):
from = relationship('Address', backref='from')
to = relationship('Address', secondary=message_tos, backref='messages_to')
message_id = Column(String, nullable=False, unique=True)
...
def whatever(*args, **kwargs):
"""
...
"""
email = session.query(Email).filter(Email.message_id==message_id).first()
blah = set(email.to).union(email.from) # This lines throws an error that Address is not iterable
Is there any way to run the set(email.to) code (which places an Address object into a set), or am I going about this completely the wrong way? Obviously, I could just do set([email.to]), however this is an entire extra order of complexity (and this function may be called multiple times with potentially very long .to or .from lists) which I'd rather not have
Your error most probably occurs not in the set(email.to) part, but in .union(email.from), since the email.from is not iterable. According to your code, email.from is an instance of Address.
This should work though: blah = set(email.to).union([email.from]). I assume that did not really call the property from, as it is a reserved keyword in python. I guess sender is a good name.
Also note that in SA searching for a primary key can be done clearer with the Query.get:
email = session.query(Email).get(message_id)

Python - SqlAlchemy. How to relate tables from different modules or files?

I have this class in one file and item class in another file in the same module. If they are in different modules or files when I define a new Channel, I got an error because Item is not in the same file. How can I solve this problem? If both classes are in the same file, I don't get any error.
ChannelTest.py
from ItemTest import Item
metadata = rdb.MetaData()
channel_items = Table(
"channel_items",
metadata,
Column("channel_id", Integer,
ForeignKey("channels.id")),
Column("item_id", Integer,
ForeignKey("items.id"))
)
class Channel(rdb.Model):
""" Set up channels table in the database """
rdb.metadata(metadata)
rdb.tablename("channels")
id = Column("id", Integer, primary_key=True)
title = Column("title", String(100))
items = relation("Item",
secondary=channel_items, backref="channels")
Item.py Different file, but in the same module
class Item(rdb.Model):
""" Set up items table in the database """
rdb.metadata(metadata)
rdb.tablename("items")
id = Column("id", Integer, primary_key=True)
title = Column("title", String(100))
Thanks in advance!
"NoReferencedTableError: Could not find table 'items' with which to generate a foreign key"
All your table definitions should share metadata object.
So you should do metadata = rdb.MetaData() in some separate module, and then use this metadata instance in ALL Table()'s.
The string method should work, but if it doesn't than there is also the option of simply importing the module.
And if that gives you import loops than you can still add the property after instantiating the class like this:
import item
Channel.items = relation(item.Item,
secondary=item.channel_items,
backref='channels')

Categories