How to persist data into a One-to-Many SELF referential with SQLAlchemy?
Question:
I’m trying to persist a One-To-Many self-referential relationship. My table looks something like this:
class Users(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, unique=True)
connected_ids = Column(Integer, ForeignKey("users.id"))
connected_with = relationship("Users")
I arrived at this format following this page in the docs for one-to-many and another page describing how to declare self referential relationships. I’ve also already tried with the following variations:
connected_with = relationship("Users", backref="users")
connected_with = relationship("Users", backref="users", remote_side="users.c.id"")
I can insert the rows, query, commit, etc… but when trying to define a relationship, it fails with the following:
Example One:
u1 = session.get(Users, 1)
u2 = session.get(Users, 2)
u1.connected_ids = [u2.id]
Will raise:
sqlalchemy.exc.ProgrammingError: (psycopg2.errors.DatatypeMismatch) column "connected_ids" is of type integer but expression is of type integer[]
LINE 1: ...users SET last_updated=now(), connected_ids=ARRAY[2911...
Example Two (with connected_with attr):
u1.connected_with = [u2.id]
Will Raise:
AttributeError: 'int' object has no attribute '_sa_instance_state'
Example Three (with the object itself):
u1.connected_ids = [u2]
Will raise:
sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) can't adapt type 'Users'
At this point, my best guess is that the table is not defined the way I expect it to, but I also don’t know what is wrong in it.
Any pointers and help will be appreciated.
Answers:
I’m not sure if adjacency list is the pattern you want. Here is how it could work though. The ForeignKey should be a scalar, ie. single value, not a list. A many
side of the relationship can be a list though, here it is children
:
import sys
from sqlalchemy import (
create_engine,
Integer,
String,
ForeignKey,
)
from sqlalchemy.schema import (
Column,
)
from sqlalchemy.orm import Session, declarative_base, relationship, backref
username, password, db = sys.argv[1:4]
Base = declarative_base()
engine = create_engine(f"postgresql+psycopg2://{username}:{password}@/{db}", echo=True)
metadata = Base.metadata
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
parent_id = Column(Integer, ForeignKey("users.id"), nullable=True)
# OR children = relationship("User", backref=backref("parent", remote_side=id))
parent = relationship("User", back_populates="children", remote_side=id)
children = relationship("User", back_populates="parent", remote_side=parent_id)
metadata.create_all(engine)
with Session(engine) as session, session.begin():
# Create 3 users, 2 connected to root.
root = User(name="root")
a = User(name="a", parent=root)
b = User(name="b", parent=root)
session.add_all([root, a, b])
with Session(engine) as session, session.begin():
# Check that root exists and both children are connected to it.
root = session.query(User).where(User.name == "root").first()
assert len(root.children) == 2
for child in root.children:
assert child.parent == root
with Session(engine) as session, session.begin():
# Add another child to root using the children property with append.
root = session.query(User).where(User.name == "root").first()
root.children.append(User(name="c"))
with Session(engine) as session, session.begin():
# Check that root exists and that there are now 3 children instead of 2.
root = session.query(User).where(User.name == "root").first()
print(root.name)
assert len(root.children) == 3
for child in root.children:
assert child.parent == root
print(child.name)
As of SQLAlchemy 2.0, to define a self-referential relationship:
pk_int = Annotated[int, mapped_column(primary_key=True)]
str255 = Annotated[str, 255]
class Base(DeclarativeBase):
type_annotation_map = {
str255: String(255),
}
class Employee(Base):
__tablename__ = 'employees'
id: Mapped[pk_int]
manager_id: Mapped[Optional[int]] = mapped_column(ForeignKey('employees.id'))
name: Mapped[str255] = mapped_column(nullable=False)
manager: Mapped["Employee"] = relationship(
back_populates="reports", remote_side="Employee.id")
reports: Mapped[list["Employee"]] = relationship(
back_populates="manager", remote_side="Employee.manager_id")
Using the model:
louis = Employee(name="Louis")
alice = Employee(name="alice")
bob = Employee(name="bob")
louis.reports.append(alice)
louis.reports.append(bob)
session.add(louis)
session.commit()
I’m trying to persist a One-To-Many self-referential relationship. My table looks something like this:
class Users(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, unique=True)
connected_ids = Column(Integer, ForeignKey("users.id"))
connected_with = relationship("Users")
I arrived at this format following this page in the docs for one-to-many and another page describing how to declare self referential relationships. I’ve also already tried with the following variations:
connected_with = relationship("Users", backref="users")
connected_with = relationship("Users", backref="users", remote_side="users.c.id"")
I can insert the rows, query, commit, etc… but when trying to define a relationship, it fails with the following:
Example One:
u1 = session.get(Users, 1)
u2 = session.get(Users, 2)
u1.connected_ids = [u2.id]
Will raise:
sqlalchemy.exc.ProgrammingError: (psycopg2.errors.DatatypeMismatch) column "connected_ids" is of type integer but expression is of type integer[]
LINE 1: ...users SET last_updated=now(), connected_ids=ARRAY[2911...
Example Two (with connected_with attr):
u1.connected_with = [u2.id]
Will Raise:
AttributeError: 'int' object has no attribute '_sa_instance_state'
Example Three (with the object itself):
u1.connected_ids = [u2]
Will raise:
sqlalchemy.exc.ProgrammingError: (psycopg2.ProgrammingError) can't adapt type 'Users'
At this point, my best guess is that the table is not defined the way I expect it to, but I also don’t know what is wrong in it.
Any pointers and help will be appreciated.
I’m not sure if adjacency list is the pattern you want. Here is how it could work though. The ForeignKey should be a scalar, ie. single value, not a list. A many
side of the relationship can be a list though, here it is children
:
import sys
from sqlalchemy import (
create_engine,
Integer,
String,
ForeignKey,
)
from sqlalchemy.schema import (
Column,
)
from sqlalchemy.orm import Session, declarative_base, relationship, backref
username, password, db = sys.argv[1:4]
Base = declarative_base()
engine = create_engine(f"postgresql+psycopg2://{username}:{password}@/{db}", echo=True)
metadata = Base.metadata
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
parent_id = Column(Integer, ForeignKey("users.id"), nullable=True)
# OR children = relationship("User", backref=backref("parent", remote_side=id))
parent = relationship("User", back_populates="children", remote_side=id)
children = relationship("User", back_populates="parent", remote_side=parent_id)
metadata.create_all(engine)
with Session(engine) as session, session.begin():
# Create 3 users, 2 connected to root.
root = User(name="root")
a = User(name="a", parent=root)
b = User(name="b", parent=root)
session.add_all([root, a, b])
with Session(engine) as session, session.begin():
# Check that root exists and both children are connected to it.
root = session.query(User).where(User.name == "root").first()
assert len(root.children) == 2
for child in root.children:
assert child.parent == root
with Session(engine) as session, session.begin():
# Add another child to root using the children property with append.
root = session.query(User).where(User.name == "root").first()
root.children.append(User(name="c"))
with Session(engine) as session, session.begin():
# Check that root exists and that there are now 3 children instead of 2.
root = session.query(User).where(User.name == "root").first()
print(root.name)
assert len(root.children) == 3
for child in root.children:
assert child.parent == root
print(child.name)
As of SQLAlchemy 2.0, to define a self-referential relationship:
pk_int = Annotated[int, mapped_column(primary_key=True)]
str255 = Annotated[str, 255]
class Base(DeclarativeBase):
type_annotation_map = {
str255: String(255),
}
class Employee(Base):
__tablename__ = 'employees'
id: Mapped[pk_int]
manager_id: Mapped[Optional[int]] = mapped_column(ForeignKey('employees.id'))
name: Mapped[str255] = mapped_column(nullable=False)
manager: Mapped["Employee"] = relationship(
back_populates="reports", remote_side="Employee.id")
reports: Mapped[list["Employee"]] = relationship(
back_populates="manager", remote_side="Employee.manager_id")
Using the model:
louis = Employee(name="Louis")
alice = Employee(name="alice")
bob = Employee(name="bob")
louis.reports.append(alice)
louis.reports.append(bob)
session.add(louis)
session.commit()