SqlAlchemy ORM __table__ using complex joins? - sqlalchemy

I've defined a model that represents the results of a SELECT statement that joins multiple tables together and performs calculations on the columns
from sqlalchemy import select
class ComplexModel(Base):
__table__ = select([users_table.c.id, users_table.c.name, orders_table.c.total]).\
select_from(users_table.join(orders_table, users_table.c.id == orders_table.c.user_id))
and can use it like:
session.query(ComplexModel).filter(ComplexModel.name == "John").first()
How can I provide an interface that allows an IDE to detect the column names of a complex model is to define the columns as class-level attributes:
class ComplexModel:
id = Column(Integer)
name = Column(String)
total = Column(Float)
errors:
"Can't add additional column 'id' when specifying __table__"
I tried using a view:
same as above but:
__table__ = create_view(
name="users-orders-view",
selectable=select(...
But still:
Can't add additional column 'id' when specifying __table__

Related

SQLAlchemy Async: How to synchronise the columns of a declarative model with its underlying table after metadata reflection?

I have also asked this question in sqlalchemy discussion group.
I am using alembic to apply a migration for a postgresql view using an async engine. This is successfully applied to the database.
I have the following declarative mapped class to the view, defined as:
class MailingListView(Base):
"""View for mailing labels.
After metata reflection from db -> model expecting columns to
be available on this class.
"""
__tablename__ = "mailing_list_view"
# Specify the column override from the underlying view that is the primary key
id = Column(UUID(as_uuid=True), primary_key=True)
# Expecting these columns below to be mapped in this class after
# metadata reflection. Currently have to uncomment these
# to manually synchronise with view!
#
# addressee = Column(String)
# street = Column(String)
# town = Column(String)
# county = Column(String)
# postcode = Column(String)
# cursor = Column(String)
I am reflecting the views using the following:
def use_inspector(conn):
inspector = inspect(conn)
return inspector.get_view_names()
views = await connection.run_sync(use_inspector)
# I can see the table columns in __table__.c.keys()
# after the reflection below has run
await connection.run_sync(
target_metadata.reflect,
only=views,
views=True,
extend_existing=True,
)
After applying migrations and performing the above reflection I can see that my mapped model has the underlying table columns updated with those defined in the underlying view.
obj = MailingListView()
obj.__table__.c.keys()
However, the properties of my mapped class are not updated after reflection, raising an exception:
obj = MailingListView()
obj.town = "town" # this raises an exception with unavailable property
How is it possible for a postgresql db (asyncpg) + async sqlalchemy to:
Synchronise the columns of a declarative model with its underlying table after metadata reflection?
Currently, I have to manually specify the columns in the declarative model.

SQLALchemy update ARRAY column [duplicate]

I'm working on a project using Flask and a PostgreSQL database, with SQLAlchemy.
I have Group objects which have a list of User IDs who are members of the group. For some reason, when I try to add an ID to a group, it will not save properly.
If I try members.append(user_id), it doesn't seem to work at all. However, if I try members += [user_id], the id will show up in the view listing all the groups, but if I restart the server, the added value(s) is (are) not there. The initial values, however, are.
Related code:
Adding group to the database initially:
db = SQLAlchemy(app)
# ...
g = Group(request.form['name'], user_id)
db.session.add(g)
db.session.commit()
The Group class:
from flask.ext.sqlalchemy import SQLAlchemy
from sqlalchemy.dialects.postgresql import ARRAY
class Group(db.Model):
__tablename__ = "groups"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128))
leader = db.Column(db.Integer)
# list of the members in the group based on user id
members = db.Column(ARRAY(db.Integer))
def __init__(self, name, leader):
self.name = name
self.leader = leader
self.members = [leader]
def __repr__(self):
return "Name: {}, Leader: {}, Members: {}".format(self.name, self.leader, self.members)
def add_user(self, user_id):
self.members += [user_id]
My test function for updating the Group:
def add_2_to_group():
g = Group.query.all()[0]
g.add_user(2)
db.session.commit()
return redirect(url_for('show_groups'))
Thanks for any help!
As you have mentioned, the ARRAY datatype in sqlalchemy is immutable. This means it isn’t possible to add new data into array once it has been initialised.
To solve this, create class MutableList.
from sqlalchemy.ext.mutable import Mutable
class MutableList(Mutable, list):
def append(self, value):
list.append(self, value)
self.changed()
#classmethod
def coerce(cls, key, value):
if not isinstance(value, MutableList):
if isinstance(value, list):
return MutableList(value)
return Mutable.coerce(key, value)
else:
return value
This snippet allows you to extend a list to add mutability to it. So, now you can use the class above to create a mutable array type like:
class Group(db.Model):
...
members = db.Column(MutableList.as_mutable(ARRAY(db.Integer)))
...
You can use the flag_modified function to mark the property as having changed. In this example, you could change your add_user method to:
from sqlalchemy.orm.attributes import flag_modified
# ~~~
def add_user(self, user_id):
self.members += [user_id]
flag_modified(self, 'members')
To anyone in the future: so it turns out that arrays through SQLAlchemy are immutable. So, once they're initialized in the database, they can't change size. There's probably a way to do this, but there are better ways to do what we're trying to do.
This is a hacky solution, but what you can do is:
Store the existing array temporarily
Set the column value to None
Set the column value to the existing temporary array
For example:
g = Group.query.all()[0]
temp_array = g.members
g.members = None
db.session.commit()
db.session.refresh(g)
g.members = temp_array
db.session.commit()
In my case it was solved by using the new reference for storing a object variable and assiging that new created variable in object variable.so, Instead of updating the existing objects variable it will create a new reference address which reflect the changes.
Here in Model,
Table: question
optional_id = sa.Column(sa.ARRAY(sa.Integer), nullable=True)
In views,
option_list=list(question.optional_id if question.optional_id else [])
if option_list:
question.optional_id.clear()
option_list.append(obj.id)
question.optional_id=option_list
else:
question.optional_id=[obj.id]

Manytomanyfields through table data list_diplay repeating grouped data

screendata
I have Django model manytomany through table:
bridge_qual_id, bridge_uoc_id, sequence_id
bridge_qual_id is only one record where units in the qualification are attached with sequence extra field for delivery order.
I want qualification to display only once and listing all units for it.
I tried format_html_join in models class method and calling in list_display but may be doing correct.
At the moment admin.ModelAdmin code is just listing table column names:
list_display = ['bridge_qual', 'bridge_uoc', 'uoc_sequence']
I can not do order_by on field in MySql in backend so tried following to change queryset but no effect.
#admin.register(tp_uoc_sequence, site=pg_site)
class tpuocAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super(tpuocAdmin, self).get_queryset(request)
qs.extra(where=
["""bridge_uoc_id IN (
SELECT bridge_qual_id FROM tp_uoc_sequence
WHERE bridge_qual_id=1) as modified group by bridge_qual_id
ORDER BY bridge_qual_id"""
])
return qs
list_display = ['bridge_qual', 'bridge_uoc', 'uoc_sequence']

How can I express a foreign key as a pair in Django?

Here are some Django models:
class User (Model):
name = CharField (max_length=100)
class ThingVersion (Model):
timestamp = DateTimeField (auto_now_add = True)
class ThingPartition (Model):
version = ForeignKey (ThingVersion)
partition_number = IntegerField ()
class UserInPartition (Model):
user = ForeignKey (User)
version = ForeignKey (ThingVersion)
partition_number = IntegerField ()
class Meta:
unique_together = (('user', 'version'))
I have deliberately not done the "obvious" thing, which is this
class UserInPartition (Model):
user = ForeignKey (User)
partition = ForeignKey (ThingPartition)
because I require the unique_together constraint, which AFAIK isn't possible to express if I use the more-normalized partition = ForeignKey (ThingPartition) approach.
How do I express that the UserInPartition.(version,partition_number) pair is a foreign key to ThingPartition?
Alternatively, is there a way to use the normalized partition = ForeignKey (ThingPartition) approach and constrain the UserInPartition model to only have one partition_number per (user,version)?
I'm trying to understand the goal you want to achieve to offer better answer. Why do you have partition number on both user in partition and thingpartition?
But what you could do is to turn ThingPartition into Through model for many to many relation.
https://docs.djangoproject.com/en/2.0/ref/models/fields/#django.db.models.ManyToManyField.through
Basically using through model would do exactly what you want-
have one model, where you refer to user in partition, partition number and version
enforce unique together for all 3 things in this model
Basically, your setup would look something like this:
class User (Model):
name = CharField (max_length=100)
something_that_makes_sense = ManyToManyField(ThingVersion, through=ThingPartition)
class ThingVersion (Model):
timestamp = DateTimeField (auto_now_add = True)
class ThingPartition (Model):
user = ForeignKey (User)
version = ForeignKey (ThingVersion)
partition_number = IntegerField ()
class Meta:
unique_together = ('user', 'version', 'partition_number')
That would be my best offer based on the limited knowledge i have.

Unique validator in WTForms with SQLAlchemy models

I defined some WTForms forms in an application that uses SQLALchemy to manage database operations.
For example, a form for managing Categories:
class CategoryForm(Form):
name = TextField(u'name', [validators.Required()])
And here's the corresponding SQLAlchemy model:
class Category(Base):
__tablename__= 'category'
id = Column(Integer, primary_key=True)
name = Column(Unicode(255))
def __repr__(self):
return '<Category %i>'% self.id
def __unicode__(self):
return self.name
I would like to add a unique constraint on the form validation (not on the model itself).
Reading the WTForms documentation, I found a way to do it with a simple class:
class Unique(object):
""" validator that checks field uniqueness """
def __init__(self, model, field, message=None):
self.model = model
self.field = field
if not message:
message = u'this element already exists'
self.message = message
def __call__(self, form, field):
check = self.model.query.filter(self.field == field.data).first()
if check:
raise ValidationError(self.message)
Now I can add that validator to the CategoryForm like this:
name = TextField(u'name', [validators.Required(), Unique(Category, Category.name)])
This check works great when the user tries to add a category that already exists \o/
BUT it won't work when the user tries to update an existing category (without changing the name attribute).
When you want to update an existing category : you'll instantiate the form with the category attribute to edit:
def category_update(category_id):
""" update the given category """
category = Category.query.get(category_id)
form = CategoryForm(request.form, category)
The main problem is I don't know how to access the existing category object in the validator which would let me exclude the edited object from the query.
Is there a way to do it? Thanks.
In the validation phase, you will have access to all the fields. So the trick here is to pass in the primary key into your edit form, e.g.
class CategoryEditForm(CategoryForm):
id = IntegerField(widget=HiddenInput())
Then, in the Unique validator, change the if-condition to:
check = self.model.query.filter(self.field == field.data).first()
if 'id' in form:
id = form.id.data
else:
id = None
if check and (id is None or id != check.id):
Although this is not a direct answer I am adding it because this question is flirting with being an XY Problem. WTForms primary job is to validate that the content of a form submission. While a decent case could be made that verifying that a field's uniqueness could be considered the responsibility of the form validator, a better case could be made that this is the responsibility of the storage engine.
In cases where I have be presented with this problem I have treated uniqueness as an optimistic case, allowed it to pass form submission and fail on a database constraint. I then catch the failure and add the error to the form.
The advantages are several. First it greatly simplifies your WTForms code because you do not have to write complex validation schemes. Secondly, it could improve your application's performance. This is because you do not have to dispatch a SELECT before you attempt to INSERT effectively doubling your database traffic.
The unique validator needs to use the new and the old data to compare first before checking if the data is unique.
class Unique(object):
...
def __call__(self, form, field):
if field.object_data == field.data:
return
check = DBSession.query(model).filter(field == data).first()
if check:
raise ValidationError(self.message)
Additionally, you may want to squash nulls too. Depending on if your truly unique or unique but allow nulls.
I use WTForms 1.0.5 and SQLAlchemy 0.9.1.
Declaration
from wtforms.validators import ValidationError
class Unique(object):
def __init__(self, model=None, pk="id", get_session=None, message=None,ignoreif=None):
self.pk = pk
self.model = model
self.message = message
self.get_session = get_session
self.ignoreif = ignoreif
if not self.ignoreif:
self.ignoreif = lambda field: not field.data
#property
def query(self):
self._check_for_session(self.model)
if self.get_session:
return self.get_session().query(self.model)
elif hasattr(self.model, 'query'):
return getattr(self.model, 'query')
else:
raise Exception(
'Validator requires either get_session or Flask-SQLAlchemy'
' styled query parameter'
)
def _check_for_session(self, model):
if not hasattr(model, 'query') and not self.get_session:
raise Exception('Could not obtain SQLAlchemy session.')
def __call__(self, form, field):
if self.ignoreif(field):
return True
query = self.query
query = query.filter(getattr(self.model,field.id)== form[field.id].data)
if form[self.pk].data:
query = query.filter(getattr(self.model,self.pk)!=form[self.pk].data)
obj = query.first()
if obj:
if self.message is None:
self.message = field.gettext(u'Already exists.')
raise ValidationError(self.message)
To use it
class ProductForm(Form):
id = HiddenField()
code = TextField("Code",validators=[DataRequired()],render_kw={"required": "required"})
name = TextField("Name",validators=[DataRequired()],render_kw={"required": "required"})
barcode = TextField("Barcode",
validators=[Unique(model= Product, get_session=lambda : db)],
render_kw={})
Looks like what you are looking for can easily be achieved with ModelForm which is built to handle forms that are strongly coupled with models (the category model in your case).
To use it:
...
from wtforms_components import Unique
from wtforms_alchemy import ModelForm
class CategoryForm(ModelForm):
name = TextField(u'name', [validators.Required(), Unique(Category, Category.name)])
It will verify unique values while considering the current value in the model. You can use the original Unique validator with it.
This worked for me, simple and easy:
Make sure that every time when a new row created in DB it must have unique name in colomn_name_in_db otherwise it will not work.
class SomeForm(FlaskForm):
id = IntegerField(widget=HiddenInput())
fieldname = StringField('Field name', validators=[DataRequired()])
...
def validate_fieldname(self, fieldname):
names_in_db = dict(Model.query.with_entities(Model.id,
Model.colomn_name_in_db).filter_by(some_filtes_if_needed).all())
if fieldname.data in names_in_db.values() and names_in_db[int(self.id)] != fieldname.data:
raise ValidationError('Name must be unique')