Using PeeWee on top of SQLite, I am trying to do a left outer join between two tables that do not have a ForeignKey relation defined. I can get the data if the right table an entry that matches the left table, but if there is no match, the columns in the right table do not make it into the returned models.
class BaseModel(Model):
class Meta:
database = db
class Location(BaseModel):
location_key = CharField(primary_key=True)
lat = FloatField(null = False)
lon = FloatField(null = False)
class Household(BaseModel):
name = CharField(null=True)
location_id = CharField(null=True)
I am trying to do something like:
for h in Household.select(Household,Location).join(Location, on=(Household.location_id == Location.location_key), join_type=JOIN.LEFT_OUTER):
print(type(h), h, h.location, h.location.lat)
This works if Household.location_id matches something in Location, but if Household.location_id is None (null), then I get an AttributeError: 'Household' object has no attribute 'location'
I would have expected location to be present, but have a valid of None.
How can I check for the existence of location before using it? I am trying to avoid using ForeignKey, there are a lot of mismatches between Household.location_id and Location.location_key and PeeWee really gets angry about that...
I think I understand what you're trying to do after re-reading. What I'd suggest is to use Peewee's "on" keyword argument in the join, which can patch the related Location (if it exists) onto a different attr than "location":
query = (HouseHold
.select(HouseHold, Location)
.join(Location, on=(HouseHold.location_id == Location.location_key),
attr='location_obj', join_type=JOIN.LEFT_OUTER))
Then you can check the "location_obj" to retrieve the related object.
for house in query:
# if there was a match, get the location obj or None.
location_obj = getattr(house, 'location_obj', None)
# the location_id is still present.
print(house.location_id, location_obj)
Found my own answer. Implement __getattr__(self) in the Household model, and return None if the name is 'location'. __getattr__(self) is only called if there is no property with that name.
Related
I'm fairly new to peewee, but have some strong background on SQLAlchemy (and all the vices that come with it). I'm trying to create a custom hybrid expression that correlates to a third (or even N) table. I'll try to demonstrate in an example (non-tested) code:
class BaseModel(Model):
class Meta:
database = database
class Person(BaseModel):
id = PrimaryKeyField(column_name="person_id")
name = CharField(max_length=255, column_name="person_name")
username = CharField(max_length=255, column_name="person_username")
class PersonTree(BaseModel):
id = PrimaryKeyField(column_name="person_tree_id")
name = CharField(max_length=255, column_name="person_tree_name")
code = CharField(max_length=255, column_name="person_tree_code")
person = ForeignKeyField(
column_name="person_id",
model=Person,
field="id",
backref="tree",
)
class Article(BaseModel):
id = PrimaryKeyField(column_name="article_id")
name = CharField(max_length=255, column_name="article_name")
branch = ForeignKeyField(
column_name="person_tree_id",
model=PersonTree,
field="id",
backref="articles",
)
#hybrid_property
def username(self):
"""
This gives me the possibility to grab the direct username of an article
"""
return self.branch.person.username
#username.expression
def username(cls):
"""
What if I wanted to do: Article.query().where(Article.username == "john_doe") ?
"""
pass
With the username hybrid_property on Article, I can get the username of the Person related to an Article using the PersonTree as a correlation, so far so good, but ... What if I wanted to "create a shortcut" to query all Articles created by the "john_doe" Person username, without declaring the JOINs every time I make the query and without relying on .filter(branch__person__username="john_doe")? I know it's possible with SA (to a great extent), but I'm finding this hard to accomplish with peewee.
Just for clarification, here's the SQL I hope to be able to construct:
SELECT
*
FROM
article a
JOIN person_tree pt ON a.person_tree_id = pt.person_tree_id
JOIN person p ON pt.person_id = p.person_id
WHERE
p.username = 'john_doe';
Thanks a lot in advance!
Hybrid properties can be used to allow an attribute to be expressed as a property of a model instance or as a scalar computation in a SQL query.
What you're trying to do, which is add multiple joins and stuff via the property, is not possible using hybrid properties.
What if I wanted to "create a shortcut" to query all Articles created by the "john_doe" Person username
Just add a normal method:
#classmethod
def by_username(cls, username):
return (Article
.select(Article, PersonTree, Person)
.join(PersonTree)
.join(Person)
.where(Person.name == username))
I'm trying to make this table with a clickable field which changes the boolean for the entry to its opposite value. It works, but I want an alternative text as "False" or "True" does not look nice, and the users are mainly Norwegian.
def bool_to_norwegian(boolean):
if boolean:
return "Ja"
else:
return "Nei"
class OrderTable(tables.Table):
id = tables.LinkColumn('admin_detail', args=[A('id')])
name = tables.Column()
address = tables.Column()
order = tables.Column()
order_placed_at = tables.DateTimeColumn()
order_delivery_at = tables.DateColumn()
price = tables.Column()
comment = tables.Column()
sent = tables.LinkColumn('status_sent', args=[A('id')])
paid = tables.LinkColumn('status_paid', args=[A('id')], text=[A('paid')])
class Meta:
attrs = {'class': 'order-table'}
If you look under the "paid" entry I am testing this right now, why can't I access the data with the same accessor as I do in the args? If I change the args to args=[A('paid')] and look at the link, it does indeed have the correct data on it. The model names are the same as the ones in this table, and "paid" and "sent" are BooleanFields.
This is kind of what I ultimately want:
text=bool_to_norwegian([A('paid')])
Here is what I send to the table:
orders = Order.objects.order_by("-order_delivery_at")
orders = orders.values()
table = OrderTable(orders)
RequestConfig(request).configure(table)
The text argument expects a callable that accepts a record, and returns a text value. You are passing it a list (which it will just ignore), and your function is expecting a boolean instead of a record. There is also no need for using accessors here.
Something like this should work:
def bool_to_norwegian(record):
if record.paid:
return "Ja"
else:
return "Nei"
Then in your column:
paid = tables.LinkColumn('status_paid', text=bool_to_norwegian)
(Note, it is not clear from your question where the data is coming from - is paid a boolean? You may need to adjust this to fit).
As an aside, the way you are passing args to your columns is weird (it seems the documentation also recommends this, but I don't understand why - it's very confusing). A more standard approach would be:
id = tables.LinkColumn('admin_detail', A('id'))
or using named arguments:
id = tables.LinkColumn('admin_detail', accessor=A('id'))
I'm trying to programmatically build a search query, and to do so, I'm joining a table.
class User(db.Model):
id = db.Column(db.Integer(), primary_key=True)
class Tag(db.Model):
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.Integer(), db.ForeignKey('user.id'))
title = db.Column(db.String(128))
description = db.Column(db.String(128))
This is a bit of a contrived example - I hope it makes sense.
Say my search function looks something like:
def search(title_arg, desc_arg):
query = User.query
if title_arg:
query = query.join(Tag)
query = query.filter(Tag.title.contains(title_arg))
if desc_arg:
query = query.join(Tag)
query = query.filter(Tag.description.contains(desc_arg))
return query
Previously, I’ve kept track of what tables that have already been joined in a list, and if the table is in the list, assume it’s already joined, and just add the filter.
It would be cool if I could look at the query object, see that Tag is already joined, and skip it if so. I have some more complex query building that would really benefit from this.
If there’s a completely different strategy for query building for searches that I’ve missed, that would be great too. Or, if the above code is fine if I join the table twice, that's great info as well. Any help is incredibly appreciated!!!
You can find joined tables in query._join_entities
joined_tables = [mapper.class_ for mapper in query._join_entities]
Since SQLAlchemy 1.4, the earlier proposed solutions including _join_entities don't work anymore.
SQLAlchemy 1.4
I tried to solve this in SQLAlchemy 1.4, but there is a caveat:
This approach includes all entities in the query, so not only joined entities
from sqlalchemy.sql import visitors
from contextlib import suppress
def _has_entity(self, model) -> bool:
for visitor in visitors.iterate(self.statement):
# Checking for `.join(Parent.child)` clauses
if visitor.__visit_name__ == 'binary':
for vis in visitors.iterate(visitor):
# Visitor might not have table attribute
with suppress(AttributeError):
# Verify if already present based on table name
if model.__table__.fullname == vis.table.fullname:
return True
# Checking for `.join(Child)` clauses
if visitor.__visit_name__ == 'table':
# Visitor might be of ColumnCollection or so,
# which cannot be compared to model
with suppress(TypeError):
if model == visitor.entity_namespace:
return True
# Checking for `Model.column` clauses
if visitor.__visit_name__ == "column":
with suppress(AttributeError):
if model.__table__.fullname == visitor.table.fullname:
return True
return False
def unique_join(self, model, *args, **kwargs):
"""Join if given model not yet in query"""
if not self._has_entity(model):
self = self.join(model, *args, **kwargs)
return self
Query._has_entity = _has_entity
Query.unique_join = unique_join
SQLAlchemy <= 1.3
For SQLAlchemy 1.3 and before, #mtoloo and #r-m-n had perfect answers, I've included them for the sake of completeness.
Some where in your initialization of your project, add a unique_join method to the sqlalchemy.orm.Query object like this:
def unique_join(self, *props, **kwargs):
if props[0] in [c.entity for c in self._join_entities]:
return self
return self.join(*props, **kwargs)
Now use query.unique_join instead of query.join:
Query.unique_join = unique_join
According to the r-m-n answer:
Some where in your initialization of your project, add a unique_join method to the sqlalchemy.orm.Query object like this:
def unique_join(self, *props, **kwargs):
if props[0] in [c.entity for c in self._join_entities]:
return self
return self.join(*props, **kwargs)
Query.unique_join = unique_join
Now use query.unique_join instead of query.join:
query = query.unique_join(Tag)
I am unable to read the column of another table which is joined. It throws AttributeError
class Component(Model):
id = IntegerField(primary_key=True)
title = CharField()
class GroupComponentMap(Model):
group = ForeignKeyField(Component, related_name='group_fk')
service = ForeignKeyField(Component, related_name='service_fk')
Now the query is
comp = (Component
.select(Component, GroupComponent.group.alias('group_id'))
.join(GroupComponent, on=(Component.id == GroupComponent.group))
)
for row in comp:
print row.group_id
Now I get an error AttributeError: 'Component' object has no attribute 'group_id'
If you just want to directly patch the group_id attribute onto the selected Component, call .naive(). This instructs peewee that you don't want to reconstruct the graph of joined models -- you just want all attributes patched onto a single Component instance:
for row in comp.naive():
print row.group_id # This will work now.
I am trying to define a SQLAlchemy/Elixer model that can describe the following relationship. I have an SSP table, which has multiple Foreign Keys to the POC table. I've defined the ManyToOne relationships correctly within the SSP object (allowing me to SSP.get(1).action.first_name correctly). What I would also like to add is the other side of this relationship, where I can perform something like POC.get(1).csa and return a list of SSP objects in which this POC is defined as the idPOCCSA.
I know this would be best for a polymorphic association but I really can not change the DB schema at all (creating a new poc2ssp table with a column for type of association).
class POC(Entity):
using_options(tablename = 'poc', autoload = True)
# These two line visually display my "issue":
# csa = OneToMany('SSP')
# action = OneToMany('SSP')
class SSP(Entity):
'''
Many to One Relationships:
- csa: ssp.idPOCCSA = poc.id
- action: ssp.idPOCAction = poc.id
- super: ssp.idSuper = poc.id
'''
using_options(tablename = 'spp', autoload = True)
csa = ManyToOne('POC', colname = 'idPOCCSA')
action = ManyToOne('POC', colname = 'idPOCAction')
super = ManyToOne('POC', colname = 'idPOCSuper')
Any ideas to accomplish this? The Elixer FAQ has a good example utilizing the primaryjoin and foreign_keys parameters but I can't find them in the documentation. I was kind of hoping OneToMany() just supported a colname parameter like ManyToOne() does. Something a bit less verbose.
Try the following:
class POC(Entity):
# ...
#declare the one-to-many relationships
csas = OneToMany('SSP')
actions = OneToMany('SSP')
# ...
class SSP(Entity):
# ...
#Tell Elixir how to disambiguate POC/SSP relationships by specifying
#the inverse explicitly.
csa = ManyToOne('POC', colname = 'idPOCCSA', inverse='csas')
action = ManyToOne('POC', colname = 'idPOCAction', inverse='actions')
# ...