Classmethod for retrieving a specific instance with sqlalchemy - sqlalchemy

I was trying to make my ORM code a bit more elegant by using classmethods in sqlalchemy and I wanted to make a method called get which would just retrieve a single existing instance of the ORM object with a few parameters. But since it seems I need a session to do it, the only way I could figure out how to do it was to pas the session as a parameter to the get method. It is working but I can't shake the feeling that I am building an antipattern.
Here is my ORM class (simplified):
class GeocellZone(Base):
__tablename__ = "geocell_zone"
__table_args__ = (UniqueConstraint("study_area_id", "version_id", "name"),)
id = Column(Integer, primary_key=True)
study_area_id = Column(Integer, ForeignKey("study_area.id"))
version_id = Column(Integer, ForeignKey("version.id"))
name = Column(Text)
geometry = Column(Geometry(srid=4326))
#classmethod
def get(cls, session, version, study_area, name):
return session.query(cls) \
.filter(cls.study_area_id == study_area.id) \
.filter(cls.version_id == version.id) \
.filter(cls.name == name) \
.first()
And here is what it looks like when I call it in my application:
import os
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from myapp.orm import *
engine = create_engine(
f"postgresql://{os.getenv('DB_USER')}:{os.getenv('DB_PASS')}#{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}",
echo=False
)
session = sessionmaker(bind=engine)()
GeocellZone.get(session, version, study_area, "Antwerpen")
This works, it returns the exact GeocellZone instance that I want. But I feel dirty passing the session to the ORM class like this. Am I overthinking this? Or is there a better way to do this?

Related

How could I update a valor in a table using SQLALCHEMY

I am using SQLALCHEMY and I can't update the table, i don't know why it doesn't work.
models.py
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, Date
Base = declarative_base()
class Book(Base):
__tablename__='books'
id= Column(Integer, primary_key=True)
title=Column(String)
author = Column(String)
pages = Column(String)
published=Column(Date)
def __repr__(self):
return "<Book(title='{}', author='{}', pages={}, published={})>"\
.format(self.title, self.author, self.pages, self.published)
crud.py
from sqlalchemy import create_engine, update
from sqlalchemy.orm import sessionmaker
engine = create_engine(config.DATABASE_URI)
Session = sessionmaker(bind=engine)
s= Session()
def recreate_database():
models.Base.metadata.drop_all(engine)
models.Base.metadata.create_all(engine)
I insert the first row on the table 'books' :
book = models.Book(
title='Deep Learning',
author='Ian Goodfellow',
pages=775,
published=datetime(2016,11,18)
)
using:
recreate_database()
s.add(book)
Then I verify that row is added:
<Book(title='Deep Learning', author='Ian Goodfellow', pages=775, published=2016-11-18 00:00:00)>
And I try to update that valor changing the value of 'pages' to 900 using:
libro=models.Base.metadata.tables['books']
u=update(libro)
u=u.values({"pages": 900})
u=u.where(libro.c.title=="Deep Learning")
engine.execute(u)
s.commit()
s.close()
I don't get error but it does not update the value of pages.
I appreciate if you could tell me where is the error on my code to update the value.
Thanks !!
its s.execute(u) not engine.execute(u)
You also declared pages to be a string column when it actually is an integer (maybe you want to change that).
edit: if you already have the book object, you can also use
book.pages = 1200
s.commit()
to change the value in your database.

Is it possible to query using raw sql and get object back?

In Hibernate it's possible to query using raw sql and get entities (objects) back. Something like: createSQLQuery(sql).addEntity(User.class).list().
Is it possible to do the same in sqlalchemy?
As #Ilja notes via link in a comment to the question, it is possible to do what you describe using .from_statement() as described in the documentation:
from sqlalchemy import Column, create_engine, Integer, select, String, text
from sqlalchemy.orm import declarative_base, Session
engine = create_engine("sqlite://")
Base = declarative_base()
class Person(Base):
__tablename__ = "person"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
def __repr__(self):
return f"<Person(id={self.id}, name='{self.name}')>"
Base.metadata.create_all(engine)
# sample data
with Session(engine) as session, session.begin():
session.add_all(
[Person(name="Adam"), Person(name="Alicia"), Person(name="Brandon")]
)
# test
with Session(engine) as session, session.begin():
sql = "SELECT id FROM person WHERE name LIKE 'A%'"
results = session.scalars(select(Person).from_statement(text(sql))).all()
print(results)
# [<Person(id=1, name='Adam')>, <Person(id=2, name='Alicia')>]
When using the entityManager you can try:
entityManager.createNativeQuery("select some native query", User.class)
According to the API:
public Query createNativeQuery(String sqlString, Class resultClass);

One-to-many relationship with wtforms_alchemy.ModelFieldList does not update children

Below is a form that populates an empty parent object and creates its children. It was necessary to manually invoke a ModelFormField, which was a minor annoyance. It works great. However, when I use the form to do an update, only the object is updated -- the children are created fresh.
What is the correct way to propogate an update to the children in this framework? Effectively, I'd like to the two print statements below to print the same thing. I'd be especially grateful if the form would create (delete) children if the formdata had extra (was missing) data for the children.
from multidict import MultiDict
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from wtforms_alchemy import ModelFieldList, ModelForm, ModelFormField
Base = declarative_base()
class Child(Base):
__tablename__ = "child"
id = Column(Integer, primary_key=True)
name = Column(String)
parent_id = Column(Integer, ForeignKey("parent.id"))
class Parent(Base):
__tablename__ = "parent"
id = Column(Integer, primary_key=True)
name = Column(String)
children = relationship(Child)
class ChildForm(ModelForm):
class Meta:
model = Child
class ParentForm(ModelForm):
class Meta:
model = Parent
children = ModelFieldList(ModelFormField(ChildForm)) # annoyed!
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
# Form data
formdata = MultiDict({"name": "Foo", "children-0-name": "hotdog"})
# Create initial object
question = Parent()
form = ParentForm(formdata, obj=question)
form.populate_obj(question)
if not form.validate():
raise RuntimeError(form.errors)
session.add(question)
session.commit()
# prints: "(1, 1)"
print((question.id, question.children[0].id))
# Retrieve object and update with same data
question_get = session.query(Parent).get(question.id)
form = ParentForm(formdata, obj=question_get)
form.populate_obj(question_get)
if not form.validate():
raise RuntimeError(form.errors)
session.add(question_get)
session.commit()
# prints: "(1, 2)", want it to print the same as above
print((question_get.id, question_get.children[0].id))
In digging through the code, it looks like object ids are expected for updates. Therefore, one must use a form on update which includes the id and pass around the ids.
--- orig.py 2018-05-01 20:51:09.000000000 -0700
+++ fixed.py 2018-05-01 20:56:16.000000000 -0700
## -4,6 +4,7 ##
from sqlalchemy.orm import relationship
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
+from wtforms import IntegerField
from wtforms_alchemy import ModelFieldList, ModelForm, ModelFormField
## -24,6 +25,7 ##
class ChildForm(ModelForm):
class Meta:
model = Child
+ id = IntegerField() # Optional field used in update operations.
class ParentForm(ModelForm):
class Meta:
## -53,7 +55,11 ##
# Retrieve object and update with same data
question_get = session.query(Parent).get(question.id)
-form = ParentForm(formdata, obj=question_get)
+child_id = question_get.children[0].id
+formdata_update = MultiDict({"name": "Foo",
+ "children-0-id": child_id,
+ "children-0-name": "pizza"})
+form = ParentForm(formdata_update, obj=question_get)
form.populate_obj(question_get)
if not form.validate():
raise RuntimeError(form.errors)
Nice sleuthing. Still this seems rather cumbersome. Have you subsequently found a easier way to do an update on a form with a one to many relationship? WTForms are supposed to save developer time. But this seems like more overhead than doing it without WTForms.

How to build backref with both associatition object and secondaryjoin?

I need some models for instance following:
Work - e.g. works of literature.
Worker - e.g. composer, translator or something similar has contribution to work.
Thus, a 'type' field is required to distinguish workers by division of work. As SQLAlchemy's documentation, this case can benifit from association object like following:
class Work(base):
id = Column(Integer, primary_key=True)
name = Column(String(50))
description = Column(Text)
class Worker(base):
id = Column(Integer, primary_key=True)
name = Column(String(50))
description = Column(Text)
class Assignment(base):
work_id = Column(Integer, Foreignkey('work.id'), primary_key=True)
worker_id = Column(Integer, Foreignkey('worker.id'), primary_key=True)
type = Column(SmallInteger, nullable=True)
Nonetheless, how to take advantage of backref and alternatvie join condition for building relation immediately to implement that each Work object can retrieve and modify corresponding Worker(s) via different attributions for distinction. For example:
work = session.query(Work).get(1)
work.name
>>> 'A Dream of The Red Mansions'
work.composers
>>> [<Worker('Xueqin Cao')>]
work.translators
>>> [<Worker('Xianyi Yang')>, <Worker('Naidie Dai')>]
Vice versa:
worker = session.query(Worker).get(1)
worker.name
>>> 'Xueqin Cao'
worker.composed
>>> [<Work('A Dream of The Red Mansions')>]
worker.translated
>>> []
Adding secondaryjoin directly without secondary specified seems not feasible, besides, SQLAlchemy's docs notes that:
When using the association object pattern, it is advisable that the association-mapped table not be used as the secondary argument on a relationship() elsewhere, unless that relationship() contains the option viewonly=True. SQLAlchemy otherwise may attempt to emit redundant INSERT and DELETE statements on the same table, if similar state is detected on the related attribute as well as the associated object.
Then, is there some way to build these relations elegantly and readily ?
There's three general ways to go here.
One is, do a "vanilla" setup where you have "work"/"workers" set up without distinguishing on "type" - then, use relationship() for "composer", "composed", "translator", "translated" by using "secondary" to Assignment.__table__ along with custom join conditions, as well as viewonly=True. So you'd do writes via the vanilla properties only. A disadvantage here is that there's no immediate synchronization between the "vanilla" and "specific" collections.
Another is, same with the "vanilla" setup, but just use plain Python descriptors to give "composer", "composed", "translator", "translated" views in memory, that is, [obj.worker for obj in self.workers if obj.type == 'composer']. This is the simplest way to go. Whatever you put in the "vanilla" collections shows right up in the "filtered" collection, the SQL is simple, and there's fewer SELECT statements in play (one per Worker/Work instead of N per Worker/Work).
Finally, the approach that's closest to what you're asking, with primary joins and backrefs, but note with the association object, the backrefs are between Work/Assignment and Assignment/Worker, but not between Work/Worker directly. This approach probably winds up using more SQL to get at the results but is the most complete, and also has the nifty feature that the "type" is written automatically. We're also using a "one way backref", as Assignment doesn't have a simple way of relating back outwards (there's ways to do it but it would be tedious). Using a Python function to automate creation of the relationships reduces the boilerplate, and note here I'm using a string for "type", this can be an integer if you add more arguments to the system:
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
Base = declarative_base()
def _work_assignment(name):
assign_ = relationship("Assignment",
primaryjoin="and_(Assignment.work_id==Work.id, "
"Assignment.type=='%s')" % name,
back_populates="work", cascade="all, delete-orphan")
assoc = association_proxy("%s_assign" % name, "worker",
creator=lambda worker: Assignment(worker=worker, type=name))
return assoc, assign_
def _worker_assignment(name):
assign_ = relationship("Assignment",
primaryjoin="and_(Assignment.worker_id==Worker.id, "
"Assignment.type=='%s')" % name,
back_populates="worker", cascade="all, delete-orphan")
assoc = association_proxy("%s_assign" % name, "work",
creator=lambda work: Assignment(work=work, type=name))
return assoc, assign_
class Work(Base):
__tablename__ = 'work'
id = Column(Integer, primary_key=True)
name = Column(String(50))
description = Column(Text)
composers, composer_assign = _work_assignment("composer")
translators, translator_assign = _work_assignment("translator")
class Worker(Base):
__tablename__ = 'worker'
id = Column(Integer, primary_key=True)
name = Column(String(50))
description = Column(Text)
composed, composer_assign = _worker_assignment("composer")
translated, translator_assign = _worker_assignment("translator")
class Assignment(Base):
__tablename__ = 'assignment'
work_id = Column(Integer, ForeignKey('work.id'), primary_key=True)
worker_id = Column(Integer, ForeignKey('worker.id'), primary_key=True)
type = Column(String, nullable=False)
worker = relationship("Worker")
work = relationship("Work")
e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
session = Session(e)
ww1, ww2, ww3 = Worker(name='Xueqin Cao'), Worker(name='Xianyi Yang'), Worker(name='Naidie Dai')
w1 = Work(name='A Dream of The Red Mansions')
w1.composers.append(ww1)
w1.translators.extend([ww2, ww3])
session.add(w1)
session.commit()
work = session.query(Work).get(1)
assert work.name == 'A Dream of The Red Mansions'
assert work.composers == [ww1]
assert work.translators == [ww2, ww3]
worker = session.query(Worker).get(ww1.id)
assert worker.name == 'Xueqin Cao'
assert worker.composed == [work]
assert worker.translated == []
worker.composed[:] = []
# either do this...
session.expire(work, ['composer_assign'])
# or this....basically need composer_assign to reload
# session.commit()
assert work.composers == []

appending to a association_proxy does not emit an event:

I have the following ORM class:
class Video(Base):
...
public_tag_entries = relationship("VideoTagEntry")
tags = association_proxy("public_tag_entries", "value")
Furthermore i have associated an event on append :
def video_tag_added(target, value, initiator):
print "tag added"
event.listen(Video.public_tag_entries, 'append', video_tag_added)
when I append to the public_tag_entries, the event is emitted
video.public_tag_entries.append(VideoTagEntry(value = "foo"))
However when i add using:
video.tags.append("foo")
the event is not emitted.
I tried to register an event on the video.tags association proxy, but that seems not to work.
Question: is this expected and correct behavior, or is this a bug? And is there a work around, or am i simply doing something wrong.
I would expect the association proxy to trigger orm events to the underlying attribute.
Thanks,
Jacco
can't reproduce (using 0.7.9):
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import event
Base = declarative_base()
class VideoTagEntry(Base):
__tablename__ = 'vte'
id = Column(Integer, primary_key=True)
video_id = Column(Integer, ForeignKey('video.id'))
value = Column(String)
def __init__(self, value):
self.value = value
class Video(Base):
__tablename__ = 'video'
id = Column(Integer, primary_key=True)
public_tag_entries = relationship("VideoTagEntry")
tags = association_proxy("public_tag_entries", "value")
canary = []
def video_tag_added(target, value, initiator):
print "tag added"
canary.append(value)
event.listen(Video.public_tag_entries, 'append', video_tag_added)
video = Video()
video.public_tag_entries.append(VideoTagEntry(value="foo"))
video.tags.append("foo")
assert len(canary) == 2
output:
tag added
tag added
So you need to alter this test case to look more like your code to see what the difference is.