9

I wish to create a mapped attribute of an object which is populated from another table.

Using the SQLAlchemy documentation example, I wish to make a user_name field exist on the Address class such that it can be both easily queried and easily accessed (without a second round trip to the database)

For example, I wish to be able to query and filter by user_name Address.query.filter(Address.user_name == 'wcdolphin').first() And also access the user_name attribute of all Address objects, without performance penalty, and have it properly persist writes as would be expected of an attribute in the __tablename__

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    addresses = relation("Address", backref="user")

class Address(Base):
    __tablename__ = 'addresses'

    id = Column(Integer, primary_key=True)
    email = Column(String(50))
    user_name = Column(Integer, ForeignKey('users.name'))#This line is wrong

How do I do this?

I found the documentation relatively difficult to understand, as it did not seem to conform to most examples, especially the Flask-SQLAlchemy examples.

Jeff Widman
  • 22,014
  • 12
  • 72
  • 88
Cory Dolphin
  • 2,650
  • 1
  • 20
  • 30
  • What exactly is wrong with the documentation? The [sqlalchemy tutorial](http://docs.sqlalchemy.org/en/rel_0_7/orm/tutorial.html) covers everything for common tasks. And advanced concepts are found in the rest of the documentation. – schlamar Jun 05 '12 at 06:10

2 Answers2

8

You can do this with a join on the query object, no need to specify this attribute directly. So your model would look like:

from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import sessionmaker, relation
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()
engine = create_engine('sqlite:///')
Session = sessionmaker(bind=engine)

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    addresses = relation("Address", backref="user")

class Address(Base):
    __tablename__ = 'addresses'
    id = Column(Integer, primary_key=True)
    email = Column(String(50))
    user_id = Column(Integer, ForeignKey("users.id"))


Base.metadata.create_all(engine)

A query after addresses with filtering the username looks like:

>>> session = Session()
>>> session.add(Address(user=User(name='test')))
>>> session.query(Address).join(User).filter(User.name == 'test').first()
<__main__.Address object at 0x02DB3730>

Edit: As you can directly access the user from an address object, there is no need for directly referencing an attribute to the Address class:

>>> a = session.query(Address).join(User).filter(User.name == 'test').first()
>>> a.user.name
'test'
schlamar
  • 9,238
  • 3
  • 38
  • 76
  • While somewhat correct, this is not my intention, as I cannot access the user_name property of the `Address` as I described in my question. When writing this in custom PHP/hand written ORM, it is a simple case of correctly generating the SQL to select User.name from the User.__table__ and store the attribute, SQLAlchemy must support this scenario? – Cory Dolphin Jun 05 '12 at 06:50
  • Brilliant, the key here is magic that happens when you created the backref, it actually 'knows' to provide Address a user object from which to query the id. – Cory Dolphin Jun 15 '12 at 07:16
3

If you truly want Address to have a SQL enabled version of "User.name" without the need to join explicitly, you need to use a correlated subquery. This will work in all cases but tends to be inefficient on the database side (particularly with MySQL), so there is possibly a performance penalty on the SQL side versus using a regular JOIN. Running some EXPLAIN tests may help to analyze how much of an effect there may be.

Another example of a correlated column_property() is at http://docs.sqlalchemy.org/en/latest/orm/mapped_sql_expr.html#using-column-property.

For the "set" event, a correlated subquery represents a read-only attribute, but an event can be used to intercept changes and apply them to the parent User row. Two approaches to this are presented below, one using regular identity map mechanics, which will incur a load of the User row if not already present, the other which emits a direct UPDATE to the row:

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base

Base= declarative_base()

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    addresses = relation("Address", backref="user")

class Address(Base):
    __tablename__ = 'addresses'

    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'))
    email = Column(String(50))

Address.user_name = column_property(select([User.name]).where(User.id==Address.id))

from sqlalchemy import event
@event.listens_for(Address.user_name, "set")
def _set_address_user_name(target, value, oldvalue, initiator):
    # use ORM identity map + flush
    target.user.name = value

    # use direct UPDATE
    #object_session(target).query(User).with_parent(target).update({'name':value})

e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)

s = Session(e)
s.add_all([
    User(name='u1', addresses=[Address(email='e1'), Address(email='e2')])
])
s.commit()

a1 = s.query(Address).filter(Address.user_name=="u1").first()
assert a1.user_name == "u1"

a1.user_name = 'u2'
s.commit()

a1 = s.query(Address).filter(Address.user_name=="u2").first()
assert a1.user_name == "u2"
Jeff Widman
  • 22,014
  • 12
  • 72
  • 88
zzzeek
  • 72,307
  • 23
  • 193
  • 185
  • Is there any benefit using this approach? A standard Python property seems more straightforward (see my answer). Or I am missing something? (Accessing it as `address.user.name` is IMO the most pythonic version, though.) – schlamar Jun 13 '12 at 20:51
  • well he wants to be able to query with it also without calling join(). This pattern was one I emphasized a lot in SQLA's early days, and it still has relevancy if you need something like an "aggregate count". But to get at a related attribute, yeah not so much. SQLA really wants you to spell out the geometry of queries. – zzzeek Jun 14 '12 at 00:40