Pony ORM JOIN syntax - ponyorm

I've started using Pony and haven't actually understood how to use joins yet. In examples I saw when left_join() was used with two for clauses, but when I try to repeat it in my code, I get error like "collection was expected, got "for p in Pond""
Maybe somebody could explain how to use it or point me to docs page where it's already explained?

Let's say we have the following entities:
from pony import orm
db = orm.Database()
class Person(db.Entity):
id = orm.PrimaryKey(int, auto=True)
name = orm.Required(str)
age = orm.Required(int)
contacts = orm.Set('Contact')
class Contact(db.Entity):
id = orm.PrimaryKey(int, auto=True)
person = orm.Required('Person')
type = orm.Required(str)
value = orm.Required(str)
db.generate_mapping(create_tables=True)
with orm.db_session:
john = Person(name='John', age=23)
mary = Person(name='Mary', age=21)
mike = Person(name='Mike', age=18)
arthur = Person(name='Arthur', age=25)
john.contacts.create(type='mobile', value='1234567')
john.contacts.create(type='email', value='john#example.com')
mary.contacts.create(type='mobile', value='76543321')
mary.contacts.create(type='skype', value='mary123')
mike.contacts.create(type='mobile', value='2345678')
Now we want to print person name and a contact info for each person older then 20. There are several ways how we can do it.
The first way is when we explicitly state the join condition. This way is pretty verbose:
query = orm.select(
(p.name, c.value)
for p in Person for c in Contact
if p.age > 20 and c.person == p
)
query.show()
In this query we explicitly state the join condition: c.person == p. The query will show us the following result:
p.name|c.type|c.value
------+------+----------------
John |email |john#example.com
John |mobile|1234567
Mary |mobile|76543321
Mary |skype |mary123
As you can see, Arthur was not included into result, albeit his age is greater than 20. This is because this type of join is inner join, and the result includes only persons for which it was possible to find at least one contact.
The second way of joining is when we loop over collection attribute:
query = orm.select(
(p.name, c.value)
for p in Person for c in p.contacts
if p.age > 20
)
query.show()
This type of joins is used most often. It is very convenient, because we don't need to explicitly specify the join condition. The result of query is the same as before:
p.name|c.type|c.value
------+------+----------------
John |email |john#example.com
John |mobile|1234567
Mary |mobile|76543321
Mary |skype |mary123
Arthur is still not in the list for the same reason as before. If we want to include Arthur into result, we need to use other type of join, namely, left join:
query = orm.left_join(
(p.name, c.value)
for p in Person for c in p.contacts
if p.age > 20
)
query.show()
In that case the result of query includes Arthur with the None value instead of the phone number:
p.name|c.type|c.value
------+------+----------------
Arthur|None |None
John |email |john#example.com
John |mobile|1234567
Mary |mobile|76543321
Mary |skype |mary123
When you use left_join you need to loop over collection. In that case Pony adds the join condition into the ON section of LEFT JOIN clause of SQL command.
You cannot do explicit join as in the very first query if you use left_join, because in that case Pony does not know which condition is to put into ON section of LEFT JOIN clause.
Sometimes it may be useful to specify content of the ON section manually. Right now Pony does not support such queries, but this feature may be added in the future.
When using PonyORM in many cases it is possible to retrieve data without making joins at all. For example, you can write the following loop to print person name and phone number:
with db_session:
for p in Person.select(lambda p: p.age > 20):
print(p.name)
for c in p.contacts:
print(c.type, c.value)
In other ORMs this will lead to "N+1 query" problem, where the contacts of each person are retrieved by separate SQL query. Pony tries to automatically optimize queries to avoid "N+1 query" pattern.
In some cases the joins are implicit. For example, to find all contacts of a person whose name is started with 'M', you can write:
query = select(c for c in Contact if c.person.name.startswith('M'))
for c in query:
print(c.person.name, c.type, c.value)
In that case the Person table is joined implicitly, just because you perform attribute traversing from Contact to Person.

Related

Many-To-Many relationships with EFCore and C# 5.0 : how to get the fields from the both of tables

I have the following database in PostgreSQL
EDIT: there is an Unique Key in PizzaTopping built with the fields Id_Pizza, Id_Topping
As you can see it's a Many-To-Many relationship.
When I ask Linqpad 6 to scaffold from my Database I have the following result:
The same result I have it when I use the EFCore Power Tools when I ask them to reverse engineer my database.
Reading from various sources, I've found, to ask EFCore to get the list of the toppings of the pizzas I should do something like that:
Pizzas.Where(p=>p.Description=="Margherita")
.Include(p=>p.PizzaToppings)
.ThenInclude(p=>p.IdToppingNavigation)
The query EFCore 5 returns this query:
SELECT P."Id_Pizza",
P."Description",
T0."Id_PizzaTopping",
T0."Id_Pizza",
T0."Id_Topping",
T0."Id_Topping0",
T0."Description"
FROM "Pizza" AS P
LEFT JOIN
(SELECT P0."Id_PizzaTopping",
P0."Id_Pizza",
P0."Id_Topping",
T."Id_Topping" AS "Id_Topping0",
T."Description"
FROM "PizzaTopping" AS P0
INNER JOIN "Topping" AS T ON P0."Id_Topping" = T."Id_Topping") AS T0 ON P."Id_Pizza" = T0."Id_Pizza"
WHERE P."Description" = 'Margherita'
ORDER BY P."Id_Pizza",
T0."Id_PizzaTopping",
T0."Id_Topping0"
Since I want to return a list with Pizza, topping like:
margherita, mozzarella
margherita, tomato sauce
marinara, garlic
marinara, tomato sauce
I tried with add .Select(topping=>topping.description) but it gets the pizza description.
So how can I take the toppings descriptions who they are in the topping table?
I tried to put a .Select() after the .ThenInclude() but I still see the Pizza entity and the p.PizzaToppings does not contain the property description of the topping table.
In this case you do not need Include but SelectMany with custom projection:
var query =
from p in Pizzas
from pt in p.PizzaToppings
select new
{
Pizza = p.Description,
Topping = pt.IdToppingNavigation.Description
}

Translate nested query SQL to EF

I have this table structure:
Classic many to many relationship. I want to get all the orders for products belonging to the category for a small number of products I provide. It may be easier to show the SQL that does exactly what I want:
select o.*
from [Order] o join Product p2 on o.FKCatalogNumber=p2.CatalogNumber
where p2.FKCategoryId IN
(select c.Id
from Category c join Product p1 on p1.FKCategoryId=c.Id
where p1.CatalogNumber in ('0001', '0002')
This example gives me all the orders belonging to the categories that catalog #'s 0001 and 0002 are in.
But I am unable to wrap my head around the equivalent EF syntax for this query. I'm embarrassed to say I spent half the day on this. I bet it's easy for someone out there.
I came up with this but it's not working (and probably not even close):
string[] catNumbers = {"0001", "0002"};
var orders = ctx.Categories
.SelectMany(c => c.Products, (c, p) => new {c, p})
.Where(#t => catNumbers.Contains(#t.p.CatalogNumber))
.Select(#t => #t.p.Orders)
.ToList();
You can use query syntax (which looks very similar to SQL) in LINQ, so if you're more comfortable with SQL then you may prefer to write your query like this:
string[] catNumbers = {"0001", "0002"};
var orders = from o in ctx.Orders
join p2 in ctx.Products on o.FKCatalogNumber equals p2.CatalogNumber
where
(
from c in ctx.Categories
join p1 in ctx.Products on c.ID equals p1.FKCategoryId
where catNumbers.Contains(p1.CatalogNumber)
select c.ID
).Contains(p2.FKCategoryId)
select o;
As you can see, it's actually just your SQL query rearranged slightly, but it compiles as C#.
Note that:
the [Order] o syntax for referencing tables is replaced by o in ctx.Orders
LINQ enforces which way round you do the join condition so I had to flip your on o.FKCatalogNumber=p2.CatalogNumber to be on o.FKCatalogNumber equals p2.CatalogNumber
instead of your where p2.FKCategoryId IN (...), the equivalent c# is (...).Contains(p2.FKCategoryId)
the select comes last, not first
but those are the only major changes. Otherwise, it's written just like SQL.
I'd also draw your attention to a distinction regarding this comment:
the equivalent EF syntax for this query
The syntax here isn't specific to EF, but is just LINQ - Language Integrated Querying. It has two flavours: query syntax (sometimes called declarative) and method syntax (sometimes called fluent). LINQ works on just about any collection that implements IEnumerable or IQueryable, including EF's DbSet.
For more info on the different ways of querying, this MSDN page is a decent place to start. There's also this handy reference table showing the equivalent query syntax for each method-syntax operator, where applicable.
You can still nest queries in EF. The following looks like it works for me:
string[] catNumbers = {"0001", "0002"};
var orders = ctx.Orders
.Where(o => ctx.Products
.Where(p => catNumbers.Contains(p.CatalogNumber))
.Select(p => p.CategoryId)
.Contains(o.Product.CategoryId)
);
This produces the following SQL:
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[CatalogNumber] AS [CatalogNumber]
FROM [dbo].[Orders] AS [Extent1]
WHERE EXISTS (SELECT
1 AS [C1]
FROM [dbo].[Products] AS [Extent2]
INNER JOIN [dbo].[Products] AS [Extent3] ON [Extent2].[CategoryId] = [Extent3].[CategoryId]
WHERE ([Extent1].[CatalogNumber] = [Extent3].[CatalogNumber]) AND ([Extent2].[CatalogNumber] IN (N'0001', N'0002'))
)

JPQL: sort queryresult by "best matches" possible?

I have the following question/problem:
I'm using JPQL (JPA 2.0 and eclipselink) and I wanna create a query that gives me the results sorted the following way:
At first the results sorted ascending by the best matches. After that should appear the inferior matches.
My objects are based on a simple class called 'Person' with the attributes:
{String Id,
String forename,
String name}
For example if I'm searching for "Picol" the result should look like:
[{129, Picol, Newman}, {23, Johnny, Picol},{454, Picolori, Newta}, {4774, Picolatus, Larimus}...]
PS: I already thought about using two queries, the first is searching with "equals" and the second with "like", although I'm not quite sure how to connect both queryresults...?
Hope for your help and thanks in advance,
Florian
If, as your question seem to imply, you only have two groups (first group : forename or name equals searched string; second group : forename or name contains searched string), and if all the persons of a given group have the same "match score", then using two queries is indeed a good solution.
First query :
select p from Person p where p.foreName = :param or p.name = :param
Second query :
select p from Person p where (p.foreName like :paramSurroundedWithPercent
or p.name like :paramSurroundedWithPercent)
and p.foreName != :param
and p.name != :param
Execute both queries (each returning a List<Person>), and add all the elements of the second list to the first one (using the addAll() method)

Self m:n Relation

I have persons and a person can contact multiple other persons, so basically the "default" tables would be:
persons (id)
contacts (person1_id, person2_id)
With this schema, I'd have to issue queries like
SELECT *
FROM contacts c
WHERE ( person1_id = *id of person1* AND person2_id = *id of person2* )
OR
( person1_id = *id of person2* AND person2_id = *id of person1* )
to get the relation between two persons when I insert such a relation only once.
What is the common practice to deal with this situation?
Insert data once and do such an OR query
Insert the relation twice so that person1_id = id of person1 AND person2_id = id of person2 is enough
An entirely different approach?
Assuming:
The m:n table actually contains additional data, so if I create a relation for both ways, I'd have to duplicate the data
This is a core part of the application and most non-trivial queries involve at least a sub query that determines whether or not such a relation exists
If you write your insert logic such that person1_id < person2_id is true for all rows, then you can just write
SELECT *
FROM contacts c
WHERE person1_id = min(*id_of_person_1*, *id_of_person_2*)
AND person2_id = max(*id_of_person_1*, *id_of_person_2*)
Why don't you use Join between the tables?
something like this:
SELECT *
FROM contact c INNER JOIN person p ON p.id = c.person1_id
The the where and group bys you need to complete you're query =)
Take a look here how the results will be showed:
http://www.w3schools.com/Sql/sql_join_inner.asp
Regards,
Elkas
Try this one mate =)
SELECT c.person1_id as id_person_1, c.person2_id as id_person_2, p1.name as name_person_1, p2.name as name_person_2
FROM contact c
LEFT JOIN person p1 ON p1.id = c.person1_id
RIGHT JOIN person p2 ON p2.id = c.person2_id;
I don't know if it will work.. but give it try mate =)
"Insert the relation twice so that person1_id = id of person1 AND person2_id = id of person2 is enough"
That is how I'd do it, personally. It allows to deal with the situation where A has the contact details of B but not the other way around (e.g. a girl gives a guy her number at the bar saying "call me" as she walks out). It also makes the queries simpler.

Convert SQL to LINQ, nested select, top, "distinct" using group by and multiple order bys

I have the following SQL query, which I'm struggling to convert to LINQ.
Purpose: Get the top 10 coupons from the table, ordered by the date they expire (i.e. list the ones that are about to expire first) and then randomly choosing one of those for publication.
Notes: Because of the way the database is structured, there maybe duplicate Codes in the Coupon table. Therefore, I am using a GROUP BY to enforce distinction, because I can't use DISTINCT in the sub select query (which I think is correct). The SQL query works.
SELECT TOP 1
c1.*
FROM
Coupon c1
WHERE
Code IN (
SELECT TOP 10
c2.Code
FROM
Coupon c2
WHERE
c2.Published = 0
GROUP BY
c2.Code,
c2.Expires
ORDER BY
c2.Expires
)
ORDER BY NEWID()
Update:
This is as close as I have got, but in two queries:
var result1 = (from c in Coupons
where c.Published == false
orderby c.Expires
group c by new { c.Code, c.Expires } into coupon
select coupon.FirstOrDefault()).Take(10);
var result2 = (from c in result1
orderby Guid.NewGuid()
select c).Take(1);
Here's one possible way:
from c in Coupons
from cs in
((from c in coupons
where c.published == false
select c).Distinct()
).Take(10)
where cs.ID == c.ID
select c
Keep in mind that LINQ creates a strongly-typed data set, so an IN statement has no general equivalent. I understand trying to keep the SQL tight, but LINQ may not be the best answer for this. If you are using MS SQL Server (not SQL Server Compact) you might want to consider doing this as a Stored Procedure.
Using MercurioJ's slightly buggy response, in combination with another SO suggested random row solution my solution was:
var result3 = (from c in _dataContext.Coupons
from cs in
((from c1 in _dataContext.Coupons
where
c1.IsPublished == false
select c1).Distinct()
).Take(10)
where cs.CouponId == c.CouponId
orderby _dataContext.NewId()
select c).Take(1);