I have a database with multiple tables that frequently need to be queried with LEFT JOIN so that results contain aggregated data from other tables. Snippet from my Prisma schema:
model posts {
id Int @id @unique @default(autoincrement())
user_id Int
movie_id Int @unique
title String @db.Text
description String? @db.Text
tags Json?
created_at DateTime @default(now()) @db.DateTime(0)
image String? @default("https://picsum.photos/400/600/?blur=10") @db.VarChar(256)
year Int
submitted_by String @db.Text
tmdb_rating Decimal? @default(0.0) @db.Decimal(3, 1)
tmdb_rating_count Int? @default(0)
}
model ratings {
id Int @unique @default(autoincrement()) @db.UnsignedInt
entry_id Int @db.UnsignedInt
user_id Int @db.UnsignedInt
rating Int @default(0) @db.UnsignedTinyInt
created_at DateTime @default(now()) @db.DateTime(0)
updated_at DateTime? @db.DateTime(0)
@@id([entry_id, user_id])
}
If I wanted to return the average rating when querying posts, I could use a query like:
SELECT
p.*, ROUND(AVG(rt.rating), 1) AS user_rating
FROM
posts AS p
LEFT JOIN
ratings AS rt ON rt.entry_id = p.id
GROUP BY p.id;
I'm not exactly sure how/whether I can achieve something similar with Prisma, because as it stands right now, it seems like this would require two separate queries, which isn't optimal because there is sometimes the need for 2 or 3 joins or SELECTs from other tables.
How can I make a query/model/something in Prisma to achieve the above?
Despite the accepted answer, the actual answer is: No.
For actual performant joins to work, they must solve an issue that's been open for about a year and a half as of writing this response: https://github.com/prisma/prisma/issues/5184
Currently, there is no way to join tables together. Queries that include relations only include the relational data by using separate queries.
Yes, this is possible with Prisma!. For making this work, you need to specify on your "schema.prisma" file how are models related with each other. That way, code generation will set the possible queries/operations.
Change it to this:
model Post {
id Int @id @unique @default(autoincrement()) @map("id")
userId Int @map("user_id")
movieId Int @unique @map("movie_id")
title String @map("title") @db.Text
description String? @map("description") @db.Text
tags Json? @map("tags")
createdAt DateTime @default(now()) @map("created_at") @db.DateTime(0)
image String? @default("https://picsum.photos/400/600/?blur=10") @map("image") @db.VarChar(256)
year Int @map("year")
submittedBy String @map("submitted_by") @db.Text
tmdbRating Decimal? @default(0.0) @map("tmdb_rating") @db.Decimal(3, 1)
tmdbRatingCount Int? @default(0) @map("tmdb_rating_count")
ratings Rating[]
@@map("posts")
}
model Rating {
id Int @unique @default(autoincrement()) @map("id") @db.UnsignedInt
userId Int @map("user_id") @db.UnsignedInt
rating Int @default(0) @map("rating") @db.UnsignedTinyInt
entryId Int
entry Post @relation(fields: [entryId], references: [id])
createdAt DateTime @default(now()) @map("created_a") @db.DateTime(0)
updatedAt DateTime? @map("updated_a") @db.DateTime(0)
@@id([entryId, userId])
@@map("ratings")
}
Note: Please follow the naming conventions (singular form, PascalCase). I made those changes for you at the schema above. @@map allows you to set the name you use on your db tables.
Then, after generating the client, you will get access to the relational operations.
// All posts with ratings data
const postsWithRatings = await prisma.post.findMany({
include: {
// Here you can keep including data from other models
ratings: true
},
// you can also "select" specific properties
});
// Calculate on your API
const ratedPosts = postsWithRatings.map( post => {
const ratingsCount = post.ratings.length;
const ratingsTotal = post.ratings.reduce((acc, b) => acc + b.rating, 0)
return {
...post,
userRating: ratingsTotal / ratingsCount
}
})
// OR...
// Get avg from db
const averages = await prisma.rating.groupBy({
by: ["entryId"],
_avg: {
rating: true
},
orderBy: {
entryId: "desc"
}
})
// Get just posts
const posts = await prisma.post.findMany({
orderBy: {
id: "desc"
}
});
// then match the ratings with posts
const mappedRatings = posts.map( (post, idx) => {
return {
...post,
userRating: averages[idx]._avg.rating
}
})
You could also create a class with a method for making this easier. But I strongly recommend you to implement GraphQL on your API. That way, you can add a virtual field inside your post type. Any time a post is requested alone or in a list, the average will be calculated. In that same way, you would have the flexibility to request data from other models and the "JOINS" will get handled for you automatically.
Last but not least, if you ever want to do a lot of queries at the same time, you can take advantage of the Prisma transactions.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With