I have a Prisma model for a Postgres database with a User that can belong to many Organisations, and organizations that can have many users. So I've created a typical many-to-many relationship.
model Organization {
organizationId Int #id #default(autoincrement())
slug String #unique
name String
users UserInOrganization[]
}
model User {
userId Int #id #default(autoincrement())
name String
organizations UserInOrganization[]
}
model UserInOrganization {
fkOrganizationId Int
fkUserId Int
organization Organization #relation(fields: [fkOrganizationId], references: [organizationId], onDelete: Cascade)
user User #relation(fields: [fkUserId], references: [userId], onDelete: Cascade)
role Role #relation(fields: [fkRoleId], references: [roleId])
##id([fkOrganizationId, fkUserId])
}
Next, I need to get all the organizations for a specific user and slug.
If I'd write it in SQL I'd do something like:
select o.name, u."userId" from "Organization" as o
join "UserInOrganization" as uio on o."organizationId" = uio."fkOrganizationId"
join "User" as u on u."userId" = uio."fkUserId"
where u."userId" = 1 and o.slug='slug'
But what would that be in Prisma? Doing something like below seems to give me all the organizations matching the slug (not considering the where on the userId). The where-clause is just filtering the users for each organization. π₯Ί
const organization = await prisma.organization.findUnique({
where: { slug: slug },
select: {
name: true,
users: { where: { user: { userId: userId } } },
},
});
How can I get just the organizations that have the actual userId?
You can use include with nested select to get organisations that match slug and userId.
Here's the query for it:
const organization = await prisma.organization.findUnique({
where: {
slug: 'prisma',
},
include: {
users: {
where: {
user: {
userId: 1,
},
},
select: {
user: true,
},
},
},
});
console.log(JSON.stringify(organization, null, 2));
Here's the output:
{
"organizationId": 1,
"slug": "prisma",
"name": "Prisma",
"users": [
{
"user": {
"userId": 1,
"name": "John Doe"
}
}
]
}
I have the following models, and many-to-many relation between them:
model User {
id String #id #default(cuid())
name String?
email String? #unique
followings Artist[]
}
model Artist {
id String #id #default(cuid())
name String #unique
spotifyId String #unique
followers User[]
}
When a user logs into my app, I retrieve their current followed artists, and need to update my database.
I have managed to select artists data from database (for updating user <-> artist relation), sample data:
const followings = [
{
id: '...',
name: 'MARINA',
spotifyId: '6CwfuxIqcltXDGjfZsMd9A'
},
{
id: '...',
name: 'Dua Lipa',
spotifyId: '6M2wZ9GZgrQXHCFfjv46we'
},
]
Now, this is my user object:
const user = {
id: 'someId',
name: 'someName',
email: 'someEmail'
}
I tried to insert or update user <-> artist relation with this query but I'm getting Bad Request error:
await prisma.user.upsert({
where: {
email: user.email
},
create: {
name: user.name,
email: user.email,
followings: {
connectOrCreate: followings
}
},
update: {
followings: {
connectOrCreate: followings
}
}
})
Please advise what I need to do. Thanks in advance.
P.S. I took the idea of the query from Updating a many-to-many relationship in Prisma post, but it didn't work for me, so please don't mark duplicate.
connectOrCreate should specify where key with id (so Prisma could find this entity) and create key with all required model fields (so Prisma could create it if it not already present), but you just passing an array of models. Change your code to this one:
await prisma.user.upsert({
where: {
email: 'user.email',
},
create: {
name: 'user.name',
email: 'user.email',
followings: {
connectOrCreate: [
{
create: {
name: 'MARINA',
spotifyId: '6CwfuxIqcltXDGjfZsMd9A',
},
where: { id: '...' },
},
],
},
},
update: {
followings: {
connectOrCreate: [
{
create: {
name: 'MARINA',
spotifyId: '6CwfuxIqcltXDGjfZsMd9A',
},
where: { id: '...' },
},
],
},
},
});
I recently started studying server development with GraphQL and Apollo.
In the code below, the formula for fetching each data is somewhat understandable.
schema.js
const { gql } = require('apollo-server');
const _ = require('lodash');
const onepieces = [
{
"id": "onepiece1",
"title": "μνΌμ€ 1κΆ",
"price": "1,360",
"desc": "λν°μ€λ λͺ¨νμ μλ"
},
{
"id": "onepiece2",
"title": "μνΌμ€ 2κΆ",
"price": "1,360",
"desc": "λκ²° λ²κΈ° ν΄μ λ¨"
}
];
const narutos = [
{
"id": "naruto1",
"title": "λ루ν 1κΆ",
"price": "1,360",
"desc": "λ루ν λͺ¨νμ μμ"
},
{
"id": "naruto2",
"title": "λ루ν 2κΆ",
"price": "1,360",
"desc": "λ루ν μ μκ°λ§μ"
}
];
const typeDefs = gql`
type Onepiece { id: ID, title: String, price: String, desc: String }
type Naruto { id: ID, title: String, price: String, desc: String }
type Query {
onepiece(id: String!): Onepiece,
naruto(id: String!): Naruto,
getOnepieces: [Onepiece],
getNarutos: [Naruto]
}
`;
const resolvers = {
Query: {
onepiece: (parent, args) => _.find(onepieces, {id: args.id}),
naruto: (parent, args) => _.find(narutos, {id: args.id}),
getOnepieces: () => onepieces,
getNarutos: () => narutos
}
};
module.exports = { typeDefs, resolvers };
But It's inefficient code. If the category of comic book increases, I should continue to add the query. So I want to improve More convenient and readable.
For example, I would like to manage the Onepiece and Naruto categories in Comic Book.
How can I improve?
You might start by writing a GraphQL enum of the possible categories.
enum Category { ONEPIECE NARUTO }
Since both kinds of comic books have the same structure, you can have a single GraphQL type to represent them. We'll incorporate the category we just wrote so that you can tell which one is which.
type ComicBook implements Node {
id: ID!
category: Category!
title: String!
price: String!
desc: String!
}
There's a somewhat standard convention for retrieving arbitrary GraphQL objects by their ID; while it comes from Facebook's Relay Javascript client it's not specifically tied to that client, and I'd use it here.
interface Node {
id: ID!
}
type Query {
node(id: ID!): Node
}
This replaces your top-level queries to retrieve specific kinds of books by ID; you could write a query like
{
node(id: "naruto1") {
... on ComicBook { category title price desc }
}
}
Now that you have the category enum, you can also write a single top-level query to return comic books possibly filtered by category
type Query {
comicBooks(category: Category): [ComicBook!]!
}
{
comicBooks(category: ONEPIECE) { id title price desc }
}
There's some corresponding code changes to make this work; I'd probably start by combining the two lists of comic books into one and adding a similar category field there.
Once you've done this, if you add a third category, you need to add it to the enum and add it to the data set, but you should not need to make any other changes to either the code, the GraphQL schema, or the queries.
Maybe my terminology is not accurate. I'm using AWS AppSync. My schema:
type Book {
title: String
author: Author
}
type Author {
name: String
}
type Query {
getBook(title:String!): Book
}
The resolver for getBook returns an object shaped as:
{
title: <string>
authorId: <number>
}
Where authorId is always returned.
What I'd like to do is specify a resolver for the field Book.author that will receive authorId and fetch that object from its own data store. Is this possible?
If what I'm trying to do is not possible, what is the proper way of doing this, where one data store is a table with two columns - { title, authorId }, and a separate store has a table with a list of authors, where the primary key is a column authorId. Since these are two different services, I can't just join the two like a SQL query.
As long as authorId is returned from the getBook resolver, it will be accessible via $ctx.source.authorId when resolving Book.author.
I reproduced your API with local resolvers using your schema:
Query.getBook request mapping template:
{
"version": "2018-05-29",
"payload": {
"title": "$context.arguments.title",
"authorId": "2" ## returned in addition to other fields. It will be used by Book.author resolver.
}
}
Query.getBook response mapping template:
$util.toJson($context.result)
Book.author request mapping template:
{
"version": "2018-05-29",
"payload": {
"name": "author name with authorId: $context.source.authorId"
}
}
Book.author response mapping template:
$util.toJson($context.result)
The following query:
query {
getBook(title:"AWS AppSync") {
title
author {
name
}
}
}
will yield the results:
{
"data": {
"getBook": {
"title": "AWS AppSync",
"author": {
"name": "author name with authorId: 2"
}
}
}
}
You might need to have bookID as parent's ID inside Author:
type Author {
# parent's id
bookID: ID!
# author id
id: ID!
name: String!
}
type Book {
id: ID!
title: String!
author: Author!
}
When Create Resource, just make:
- Book.id as primary key of BookTable
- Author.bookID as primary key and Author.id as sort key of
AuthorTable
You also need to attach resolver for Book.author using $ctx.source.id
After you attach Book.author resolver, you are good to go. You can get result something like below:
getBook(title: "xx") {
id
title
author {
id
name
}
}
I have the following database setup for my status posts. For each post, users can like the post, comment on the post or can even be tagged in the original post by the author.
I'm trying to setup my Resourceful controller 'Post' to bring back all the data via JSON object but I can't properly find the comment, likes or tags usernames. I'm using Sentry 2 for auth if that makes a difference.
Here's the database setup:
CREATE TABLE Users (
id INT NOT NULL AUTO_INCREMENT,
first_name VARCHAR(30),
last_name VARCHAR(30),
many more...
);
CREATE TABLE Posts (
postID INT NOT NULL AUTO_INCREMENT,
caption VARCHAR(200),
description VARCHAR(200),
fromID INT(10) UNSIGNED NOT NULL,
toID INT(10) UNSIGNED NOT NULL,
icon VARCHAR(200),
link VARCHAR(200),
message TEXT,
storyType INT,
type ENUM ('LINK', 'PHOTO', 'STATUSUPDATE', 'VIDEO' ),
createdTime DATE,
PRIMARY KEY (postID),
FOREIGN KEY (fromID) REFERENCES users (id),
FOREIGN KEY (toID) REFERENCES users (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
CREATE TABLE Likes (
likeID INT NOT NULL AUTO_INCREMENT,
fromID INT(10) UNSIGNED NOT NULL,
postID INT NOT NULL,
createdDate DATE,
PRIMARY KEY (likeID),
FOREIGN KEY (fromID) REFERENCES users (id),
FOREIGN KEY (postID) REFERENCES Posts (postID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
CREATE TABLE Comments (
commentID INT NOT NULL AUTO_INCREMENT,
fromID INT(10) UNSIGNED NOT NULL,
postID INT NOT NULL,
comment TEXT,
createdDate DATE,
PRIMARY KEY (commentID),
FOREIGN KEY (fromID) REFERENCES users (id),
FOREIGN KEY (postID) REFERENCES Posts (postID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
CREATE TABLE Tags (
tagID INT NOT NULL AUTO_INCREMENT,
userID INT(10) UNSIGNED NOT NULL,
postID INT NOT NULL,
PRIMARY KEY (tagID),
FOREIGN KEY (userID) REFERENCES users (id),
FOREIGN KEY (postID) REFERENCES Posts (postID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
My Post Controller, I just have a simple page that spits out everything. I don't want to loop through anything in my view file, I just want to bring back the json full object.
class PostController extends BaseController {
public function show($id)
{
$post = Post::with(array('comments', 'from', 'tags', 'likes'))->find($id);
return View::make('samplepage')->with('data', $post);
}
}
My Post Model:
class Post extends Eloquent {
protected $table = 'Posts';
protected $primaryKey = 'postID';
public function comments()
{
return $this->hasMany('Comment','postID');
}
public function tags()
{
return $this->hasMany('Tag','postID');
}
public function likes()
{
return $this->hasMany('Like','postID');
}
public function from()
{
return $this->belongsTo('User', 'fromID')->select(array('id', 'first_name', 'last_name'));
}
public function users()
{
return $this->belongsTo('User', 'fromID');
}
}
Comment Model:
class Comment extends Eloquent {
protected $table = 'Comments';
protected $primaryKey = 'commentID';
public function post() {
return $this->belongsTo('Post','fromID');
}
public function user() {
return $this->belongsTo('User', 'fromID')->select(array('id', 'first_name', 'last_name'));
}
}
Tag Model:
class Tag extends Eloquent {
protected $table = 'Tags';
protected $primaryKey = 'tagID';
}
I even setup the following in my user model but it makes no difference.
User Model:
public function posts() {
return $this->hasMany('Post','id');
}
public function comments() {
return $this->hasMany('Comment','id');
}
Everything works great with this setup and when I hit posts/2 with this the following code, I get the below object back.
$post = Post::with(array('comments', 'from', 'tags', 'likes'))->find($id);
return View::make('samplepage')->with('data', $post);
{
postID: "2",
toID: "8",
comments: [
{
commentID: "2",
comment: "second comment",
fromID: "1",
postID: "2",
createdDate: "2014-02-15"
}
],
from: {
id: "4",
first_name: Paul,
last_name: Davis
},
tags: [
{
tagID: "1",
userID: "2",
postID: "2"
},
{
tagID: "2",
userID: "3",
postID: "2"
}
],
likes: [
{
likeID: "1",
fromID: "2",
postID: "2",
createdDate: "2013-01-04"
},
{
likeID: "2",
fromID: "3",
postID: "2",
createdDate: "2013-02-05"
}
]
}
But what I want is the following, where for each tag, like and comment to concatenate the first and last name and get them back with the object.
{
postID: "2",
toID: "4",
comments: [
{
commentID: "2",
comment: "second comment",
fromID: "1",
from: {
"name": "Jason Terry",
"id": "721286625"
},
postID: "2",
createdDate: "2014-02-15"
}
],
from: {
id: "4",
first_name: Paul,
last_name: Davis
},
tags: [
{
tagID: "1",
userID: "2",
from: {
"name": "David Lee",
"id": "721286625"
},
postID: "2"
},
{
tagID: "2",
userID: "3",
from: {
"name": "Paul Pierce",
"id": "721286625"
},
postID: "2"
}
],
likes: [
{
likeID: "1",
fromID: "2",
from: {
"name": "David Lee",
"id": "721286625"
},
postID: "2",
createdDate: "2013-01-04"
},
{
likeID: "2",
fromID: "3",
from: {
"name": "Al Davis",
"id": "721286625"
},
postID: "2",
createdDate: "2013-02-05"
}
]
}
I have searched Stackoverflow, countless Laravel blogs, the official documentation for 2 weeks now and I can't seem to solve this. Any help is wonderfully appreciate.
Update:
With Tony's answer below I added
$post = Post::with(array('comments.users', 'from', 'tags.users', 'likes.users'))->find($id);
Then I added
public function users()
{ return $this->belongsTo('User', 'fromID')->select(array('id', 'first_name', 'last_name'));
}
to the comments, tags, and likes model. And the object works great now.
But my debugger shows the following
select `id`, `first_name`, `last_name` from `users` where `users`.`id` in ('1')
select `id`, `first_name`, `last_name` from `users` where `users`.`id` in ('4')
select `id`, `first_name`, `last_name` from `users` where `users`.`id` in ('2', '3')
select `id`, `first_name`, `last_name` from `users` where `users`.`id` in ('2', '3')
In short, it runs 4 queries on my users table. Isn't this redundant? Shouldn't it be doing 1 query to the users table instead of 1 query for the original post user, 1 query for the comments users, 1 query for the tags users, and 1 query for the likes users?
It looks like you want to use nested relationships.
$post = Post::with('comments.from', 'from', 'tags.from', 'likes.from')->find($id);
You would also need the "from" relationship coded into each of those models.
To get your concatenated name; you'd need the following in your User model
protected $appends = array('name');
protected $hidden = array('first_name', 'last_name'); //this is optional
public function getNameAttribute()
{
return $this->attributes['first_name'] . ' ' . $this->attributes['last_name'];
}
it is little old post but you can also do this,
public function from()
{
return $this->belongsTo('User', 'fromID')
->select(array('id',DB::raw("CONCAT(firstname,' ', lastname) as name")));
}