@relation으로 단방향 관계의 relation을 지정해줄 수 있다.
예를 들어 B에서 A를 follow하면 A의 follwers에 B가 자동으로 추가된다.
A.followers[ B ]
B.following[ A ]
schema.prisma에서 user model에 followers와 following을 추가하고 migrate해준다.
//schema.prisma
model User {
.
.
. // relation끼리의 이름이 같아야한다.
followers User[] @relation("FollowRelation", references: [id])
following User[] @relation("FollowRelation", references: [id])
.
.
}
followUser의 typeDefs와 resolvers를 만들어준다.
username을 통한 follow
//followUser.typeDefs.js
import { gql } from "apollo-server-express";
export default gql`
type followUserResult{
ok: Boolean!
error: String
}
type Mutation{
followUser(username: String!): followUserResult!
}
`;
connect로 연결해줄 때 unique한 field로만 연결해줄 수 있다.
connect 대신 disconnect를 써주면 연결이 해제된다. 즉, unfollow된다.
//followUser.resolver.js
const resolverFn = async (_, { username }, { loggedInUser }) => {
const isUser = client.user.findUnique({ where: { username } });
if (!isUser) {
return {
ok: false,
error: "That user does not exist",
};
}
await client.user.update({
where: {
id: loggedInUser.id,
},
data: {
following: {
connect: {
username,
}
}
}
});
return {
ok: true
};
};
include는 원하는 관계를 갖고 오게 해준다.
prisma는 DB에 양이 엄청 많은 data를 로딩하는 경우 서버에 부담이 갈 수 있으므로 기본적으로 관계를 로딩하고 있지 않는다.
작은양의 data를 가져올 때는 include를 통해 직접 로딩을 허락해 줘야한다.
//seeProfile.resolvers.js
import client from "../../client";
export default {
Query: {
seeProfile: (_, { username }) => client.user.findUnique({
where: {
username,
},
include: {
following: true,
followers: true,
}
})
}
};
위처럼 한번에 많은 data를 불러내지 않고 한 page씩 불러내는 것을 pagination이라 한다.
Pagination 중 2가지 종류를 알아볼 것이다.
장점: 원하는 page로 이동할 수 있다.
단점: 만약 200,000개의 data를 skip하고 10개의 data를 가져오려면 200,000개의 data도 가져와야한다.
//seeFollowers.resolvers.js
import client from "../../client"
export default {
Query: {
seeFollowers: async (_, { username, page }) => {
const isUser = await client.user.findUnique({
where: { username }, select: { id: true }
}); //user 존재여부만 확인할 때는 user의 모든 data를 가져오지 않고 id만 가져온다.
if (!isUser) {
return {
ok: false,
error: "User is not found"
};
}
const followers = await client.user
.findUnique({ where: { username } })
.followers({
take: 5,
skip: (page - 1) * 5,
});
//user들 중 username을 following한 사람 수를 찾는 query
const totalFollowers = await client.user.count({
where: { following: { some: { username, } } }
});// 배열을 다 가져올 필요 없이 개수를 세고 count한 수만 가져온다.
return {
ok: true,
followers,
totalPages: Math.ceil(totalFollowers / 5),
};
}
}
};
장점: 규모가 용이하게 커질 수 있다. (무한 스크롤에 쓰임)
단점: 원하는 page로 이동 할 수 없다.
lastId를 받아와 마지막 id부터 skip만큼 다음 부터 take수 만큼 가져온다.
이 때 처음부터 일 경우 lastId가 없을 경우 skip은 0으로 해주고 cursor는 없는 것으로 한다.
// seeFollowing.resolvers.js
import client from "../../client"
export default {
Query: {
seeFollowing: async (_, { username, lastId }) => {
const isUser = await client.user.findUnique({ where: { username }, select: { id: true } });
if (!isUser) {
return {
ok: false,
error: "User is not found"
};
}
const following = await client.user.findUnique({ where: { username } }).following({
take: 5,
skip: lastId ? 1 : 0,
...(lastId && { cursor: { id: lastId } })
});
return {
ok: true,
following,
};
}
}
};
computed fields는 graphql schema에 정의돼 있지만 데이터베이스에는 없는 것으로 매번 request를 받을 때마다 계산이 된다.
query에서 user를 request할 때 graphql이 DB에서 user를 가져오고 DB에 없는 field를 확인하고 resolvers에서 찾는다. 찾으면 해당 resolver를 DB에서 가져온 user로 실행한다.
resolver의 첫번째 인자는 root로 parent를 의미한다. 그래서 부모유저와 토큰의 로그인한 유저를 비교할 수 있다.
// users.resolvers.js
import client from "../client";
export default {
User: {
totalFollowing: ({ id }) => //following 수
client.user.count({
where: {
followers: {
some: {
id,
}
}
}
}),
totalFollowers: ({ id }) => //follower 수
client.user.count({
where: {
following: {
some: {
id,
}
}
}
}), //user가 로그인한 자신인지 확인
isMe: ({ id }, _, { loggedInUser }) => {
if (!loggedInUser) {
return false;
}
return id === loggedInUser.id;
}, // user가 follow한 사람인지 확인
isFollowing: async ({ id }, _, { loggedInUser }) => {
if (!loggedInUser) {
return false;
}
const exist = await client.user.count({
where: {
username: loggedInUser.username,
following: {
some: {
id
}
}
}
});
return Boolean(exist);
}
}
};