annotation in admin list with many to many relation - django-admin

I have a link between ads and products and stores, and I want to sort them in the admin by store:
class Ad(models.Model):
products = models.ManyToManyField(Product, blank = True)
device = models.ForeignKey(Display, on_delete = models.CASCADE)
class Product(models.Model):
name = models.CharField("name", max_length = 128)
store = models.ForeignKey(Store, on_delete = models.CASCADE)
class Store(models.Model):
name = models.CharField("name", max_length = 128)
so each Ad can have 0, 1, 2, 3 ... products linked to it. Now I want to make the field "store" sortable in the Ads admin list, therefore I tried to overwrite the get_queryset method of AdAdmin but got stuck on the way:
class AdAdmin(admin.ModelAdmin):
list_display = ["get_store", ... ]
def get_queryset(self, request):
qs = super().get_queryset(request)
qs = qs.annotate(storename = ####)
return qs
#admin.display(ordering = "storename")
def get_store(self, obj):
try:
return obj.products.all().first().store.name
except AttributeError:
try:
return obj.device.store.name
except AttributeError:
return "NEW"
So I want to annoatate my queryset by storename and I want to be able to sort by store.name alphabetically on my Ad list admin page. I already found out how to annotate empty stores:
qs = qs.annotate(storename = Case(When(products_pk__isnull = True, then = Value("NEW"), default = Value("old")))
But this only got me so far ... how would I assign the value store.name to default dynamically using my given logic?
Every Ad has a condition: It can only be linked to (many) products of the same store.

As you mentioned, every Ad can only have products from one store. Then you can take the store name from any Product or distinct result for all Products.
To access the Product from the Ad model, you can add a related_query_name
# models.py
class Ad(models.Model):
products = models.ManyToManyField(
Product,
related_name="product_ads",
related_query_name="product_ads_qs",
blank=True,
)
...
# admin.py
from django.db.models import OuterRef
#admin.register(Ad)
class AdAdmin(admin.ModelAdmin):
list_display = ["get_store", ...]
def get_queryset(self, request):
qs = super().get_queryset(request)
qs = qs.annotate(
# Get all store names for each Ad object
storename=Product.objects.filter(product_ads_qs=OuterRef("id"))
# Distinct the result. (Will fail it multiple store names returns)
.distinct("store__name")
# Get the store name
.values("store__name")
)
return qs
#admin.display(ordering="storename")
def get_store(self, obj):
# You can change the default value here. No need for Case operation
return obj.storename or "NEW"
It should work. I am unsure about the performance, maybe a better ORM can be written.
UPDATE
Distinct can be replaced with [:1]. I forgot it yesterday, sorry.
The store is FK for Product, but we need to be sure which Product instance we have from the query. That is why OuterRef is used. It means to get the Product instance for the given Ad instance by Ad.id.
There are several ways to access the store names. It is probably faster with the related name. I am not that experienced but will try to explain:
Without the related_name, this ORM can be written like
storename=Ad.objects.filter(id=OuterRef("id")).values("products__store__name")[:1]
This ORM creates a query like the following. It started from the Ad table, and 3 joins the access to the store table.
SELECT
"app_ad"."id",
(
SELECT U3."name"
FROM "app_ad" U0
-- there is a reference table for m2m relation created by Django
LEFT OUTER JOIN "app_ad_products" U1 ON (U0."id" = U1."ad_id")
LEFT OUTER JOIN "app_product" U2 ON (U1."product_id" = U2."id")
LEFT OUTER JOIN "app_store" U3 ON (U2."store_id" = U3."id")
-- OuterRef works here and searches the Ad.id per Ad record
WHERE U0."id" = ("app_ad"."id")
-- [:1] limits the query
LIMIT 1
) AS "storename"
FROM "app_ad"
If we use a related name
storename=Product.objects.filter(product_ads_qs=OuterRef("id")).values("store__name")[:1]
The query output has 2 joins to access the store table
SELECT "app_ad"."id",
(
SELECT U3."name" FROM "app_product" U0
INNER JOIN "app_ad_products" U1 ON (U0."id" = U1."product_id")
INNER JOIN "app_store" U3 ON (U0."store_id" = U3."id")
WHERE U1."ad_id" = ("app_ad"."id") LIMIT 1
) AS "storename"
FROM "app_ad"
Bonus:
If you add the related name to Product model relation
class Product(models.Model):
name = models.CharField("name", max_length=128)
store = models.ForeignKey(
Store,
related_name="store_product",
related_query_name="store_product",
on_delete=models.CASCADE,
)
The ORM can also be written as the following, and the query will be pretty much the same as the previous one.
storename=Store.objects.filter(store_product__product_ads_qs__id=OuterRef("id")).values("name")[:1]
You can check for related naming convention.

Related

Rails: How to force ActiveRecord generate alias for an association every time (just like Hibernate in Java does it), not only when it's ambiguous?

I work on a project where there is STI Item with 5 subclasses (Item1, Item2 ... Item5). This STI (items table) is mapped over a join table item_parents to Parent record (parents table) record. The mapping is done via has_many trough:.
Each of the items has two fields: name, code both are strings. Parent has many fields, but for the sake of example let's say it has name, created_at.
On the frontend, they are displayed in one table, like this:
Parent.name | Parent.created_at | Item1.name | Item1.code | Item2.name | Item2.code | ...
Users can configure filtering for each of the columns. It can be any combination or no filter at all. For example, they can choose the following combination:
Parent.created_at before 2020.02.22
Item1.name containing 'abc'
Item2.name containing 'xyz'
Item3.code equals 'Z12'
The filtering code implemented like this:
def search(filters)
filters.reduce(Parent.all) { |query, (key, value)| apply_filter(query, key, value) }
end
def apply_filter(query, key, value)
case filter_key
when :parent_name_contains
query.where(Parent.arel_table[:name].matches("%#{value}%"))
when :parent_created_at_before
query.where(Parent.arel_table[:created_at].lt(value))
when :item1_name_contains
query.joins(:item1s).where(Item1.arel_table[:name].matches("%#{value}%"))
when :item2_name_contains
query.joins(:item2s).where(Item2.arel_table[:name].matches("%#{value}%"))
when :item1_code_equals
query.joins(:item1s).where(Item1.arel_table[:name].eq(value))
when :item2_code_equals
query.joins(:item2s).where(Item2.arel_table[:name].eq(value))
# ... and so on for all the filters
else
query
end
end
The problem
When I query by fields of two or more different subclasses of Item, ActiveRecord fails to generate correct WHERE clause. It does not use the alias that it has assigned for the association in JOIN clause.
Let's say I want to filter by Item1.name = 'i1' and Item2.name = 'i2', then what rails generates is this:
SELECT "parents".*
FROM "parents"
INNER JOIN "item_parents"
ON "item_parents"."parent_id" = "parents"."id"
INNER JOIN "items"
ON "items"."id" = "item_parents"."item_id"
AND "items"."item_type" = 'Item::Item1'
INNER JOIN "item_parents" "item_parents_parents_join"
ON "item_parents_parents_join"."parent_id" = "parents"."id"
INNER JOIN "items" "item2s_parents" -- OK. join has an alias
ON "item2s_parents"."id" = "item_parents_parents_join"."item_id"
AND "item2s_parents"."item_type" = 'Item::Item2'
WHERE "items"."name" = 'i1'
AND "items"."name" = 'i2' -- Wrong! Must be "item2s_parents"."name" = 'i2'
As a result, I have zero rows returned, because it's impossible to have an item with name equal to 'i1' AND 'i2' at the same time.
What I tried
It seemed to be a good idea to write a custom joins_item method, that would dig the query and check whether it has other joins called on it before (AR stores such information in query.values[:joins] and query.values[:left_outer_joins]) and if there is, then it would return another Arel::Table instance having the correct alias. If there is nothing joined before, then I don't need alias and return the default Arel::Table.
But then I found out that AR resolves aliases at the moment of building SQL. So even though I could guess the correct alias (or no alias) at the moment of joining it can change in the end. And this is actually what happens when you do left_outer_joins first and then joins. AR always places INNER JOINs before LEFT OUTER JOINs in the resulting SQL.
So the question is...
Is there a way to force AR to alias everything when I do joins or left_outer_joins with Arel, or any other more or less maintainable workaround/fix/monkey patch for this issue?

LEFT JOIN Query with JPA not working (select new entity with users that have n documents)

may anybody help me with this task...
persistence provider is eclipselink 2.6.
i want to retrieve a list of users that may have 0 or n documents. because both tables have a few columns i want to use SELECT NEW Entity (userId, amountDocuments), i only need the user-id and the amount of documents for this task. if the user hasn't any documents yet, "0" should be shown, e.g.:
UserId: 1 2 3 4
AmountDocs: 0 1 0 3
Mapping for Documents in Entity User is as follows:
#OneToMany(fetch=FetchType.LAZY, cascade=CascadeType.ALL,mappedBy = "user", targetEntity = UserDocument.class)
#OrderBy("sortOrder ASC")
#JoinFetch(JoinFetchType.OUTER)
protected List<UserDocument>documents;
Mapping for User in Entity UserDocument is as follows:
#ManyToOne(cascade=CascadeType.ALL)
protected User user;
and here is the jpa-query:
SELECT DISTINCT
NEW user.entity.User(u.id,count(doc.user)) FROM User u
LEFT JOIN u.documents doc ON doc.user = u
AND doc.active = 't'
GROUP BY u.id
Problem is, that i only retrieve those two users who have documents that match doc.active='t'.
I also tried it with SIZE(u.documents) which also just returns two users and additionally wrong document-count values.
What is wrong here?
Thanks in advance!
finally after spending hours with that simple stuff, the right solution came with:
SELECT DISTINCT
NEW user.entity.User(u.id,count(doc)) FROM User u
LEFT JOIN u.documents doc ON doc.user = u AND doc.active = 't'
GROUP BY u.id
i have to count the left joined documents itself not the users.

How to build inner join in Rails with conditions?

I've a model StockUpdate which keeps track of stocks for every product for a store. Table attributes are: :product_id, :stock, :store_id. I was trying to find out last entry for every product for a given store. According to that I build my query in PGAdmin which is given below and it's working fine. I'm new in Rails and I don't know how to represent it in Model. Please help.
SELECT a.*
FROM stock_updates a
INNER JOIN
(
SELECT product_id, MAX(id) max_id
FROM stock_updates where store_id = 9 and stock > 0
GROUP BY product_id
) b ON a.product_id = b.product_id AND
a.id = b.max_id
I does not clearly understand what you want to do, but I think you can do something like this:
class StockUpdate < ActiveRecord::Base
scope :a_good_name, -> { joins(:product).where('store_id = ? and stock > ?', 9, 0) }
end
You can all call StoclUpdate.a_good_name.explain to check the generated sql
What you need is really simple and can be easily accomplished with 2 queries. Otherwise it becomes very complicated in a single query (it's still doable though):
store_ids = [0, 9]
latest_stock_update_ids = StockUpdate.
where(store_id: store_ids).
group(:product_id).
maximum(:id).
values
StockUpdate.where(id: latest_stock_update_ids)
Two queries, without any joins necessary. The same could be possible with a single query too. But like your original code, it would include subqueries.
Something like this should work:
StockUpdate.
where(store_id: store_ids).
where("stock_updates.id = (
SELECT MAX(su.id) FROM stock_updates AS su WHERE (
su.product_id = stock_updates.product_id
)
)
")
Or perhaps:
StockUpdate.where("id IN (
SELECT MAX(su.id) FROM stock_updates AS su GROUP BY su.product_id
)")
And to answer your original question, you can manually specify a joins like so:
Model1.joins("INNER JOINS #{Model2.table_name} ON #{conditions}")
# That INNER JOINS can also be LEFT OUTER JOIN, etc.

Criteria to get all domains with collection less than property

Given the following domain class:
class Game {
Integer maxUsers
static hasMany = [users: User]
}
Using the Criteria API, what should I do to get all domains with the number of users less than maxUsers property?
I don't think it's possible to do with Criteria api, as Hibernate Criteria don't support HAVING clause. There is an open JIRA issue for that, you can try patches submitted there.
An alternative would be to use HQL:
def results = Game.findAll("from Game where id in (select g.id from Game g join g.users u group by g.id, g.maxUsers having count(u) < g.maxUsers)")

Using RoR with a legacy table that uses E-A-V

I'm needing to connect to a legacy database and pull a subset of data from a table that uses the entity-attribute-value model to store a contact's information. The table looks like the following:
subscriberid fieldid data
1 2 Jack
1 3 Sparrow
2 2 Dan
2 3 Smith
where fieldid is a foreign key to a fields table that lists custom fields a given customer can have (e.g. first name, last name, phone). The SQL involved is rather hairy as I have to join the table to itself for every field I want back (currently I need 6 fields) as well as joining to a master contact list that's based on the current user.
The SQL is something like this:
select t0.data as FirstName, t1.data as LastName, t2.data as SmsOnly
from subscribers_data t0 inner join subscribers_data t1
on t0.subscriberid = t1.subscriberid
inner join subscribers_data t2
on t2.subscriberid = t1.subscriberid
inner join list_subscribers ls
on (t0.subscriberid = ls.subscriberid and t1.subscriberid = ls.subscriberid)
inner join lists l
on ls.listid = l.listid
where l.name = 'My Contacts'
and t0.fieldid = 2
and t1.fieldid = 3;
How should I go about handling this with my RoR application? I would like to abstracat this away and still be able to use the normal "dot notation" for pulling the attributes out. Luckily the data is read-only for the foreseeable future.
This is exactly what #find_by_sql was designed for. I would reimplement #find to do what you need to do, something like this:
class Contact < ActiveRecord::Base
set_table_table "subscribers_data"
def self.find(options={})
find_by_sql <<EOS
select t0.data as FirstName, t1.data as LastName, t2.data as SmsOnly
from subscribers_data t0 inner join subscribers_data t1
on t0.subscriberid = t1.subscriberid
inner join subscribers_data t2
on t2.subscriberid = t1.subscriberid
inner join list_subscribers ls
on (t0.subscriberid = ls.subscriberid and t1.subscriberid = ls.subscriberid)
inner join lists l
on ls.listid = l.listid
where l.name = 'My Contacts'
and t0.fieldid = 2
and t1.fieldid = 3;
EOS
end
end
The Contact instances will have #FirstName and #LastName as attributes. You could rename them as AR expects too, such that #first_name and #last_name would work. Simply change the AS clauses of your SELECT.
I am not sure it is totally germane to your question, but you might want to take a look at MagicModel. It can generate models for you based on a legacy database. Might lower the amount of work you need to do.

Resources