(노드 심화 주차 프로젝트 중)
Sequelize 모듈을 사용함에 있어서 어려움울 겪었다.
- Users라는 테이블이 있다. 그리고 Services라는 테이블이 있는데 여기의 customerId와 ownerId는 모두 Users의 userId와 연결되어 있다(userId를 참조하고 있다).
목표: 하나의 ‘Service’ 데이터를 조회할 때 cutomerId와 ownerId 각각에 맞는 User 데이터를 조인해오고 싶다. 추가로, 조인해 온 User 데이터 중 nickname 컬럼만 각각 customerNickname, ownerNickname이라 덮어씌운 별명으로 가져오고 싶다.
상기한 관계는 sequelize의 Model 작성시 이미 맺어져 있다(이게 왜 중요하고 환장할 일인지는 아래에 나와 있다…). 이 때 sequelize 작성을 어떻게 하면 좋겠는가?
아래는 가능한 최선의 방법을 찾아나간 지난한 기록이다. 매우 지난하니 결과로 건너 뛰길 추천함.
Sequelize 조인문
Sequelize 조인문
외래키가 설정되어 있지 않은 상태에서 조인하기:
const serviceDetail = await Service.findOne({
include: [{
model: User,
association: Service.belongsTo(User, {
foreignKey: 'userId',
constraints: false,
}),
// attributes: [];
}],
where: { serviceId },
attributes: ['serviceId', ... ]
})
HasOne and BelongsTo insert the association key in different models from each other. HasOne inserts the association key in target model whereas BelongsTo inserts the association key in the source model.
(나중에 찾은 HasOne과 BelongsTo로 했을 때 결과가 다르게 나온 이유: https://stackoverflow.com/questions/53324942/sequelize-how-to-setup-foreign-key-and-join-on-it)
migrations 문법의 addConstraint 중
await queryInterface.addConstraint('Reviews', {
fields: ['serviceId'],
type: 'foreign key',
name: 'FK_Reviews_Services',
references: {
table: 'Services',
field: 'serviceId',
},
onDelete: 'cascade',
onUpdate: 'cascade',
});
- onDelete와 onUpdate의 옵션들은
RESTRICT
,CASCADE
,NO ACTION
,SET DEFAULT
andSET NULL
이라고 한다.
- 저 ‘name’이 무슨 뜻인지 모르겠다.
오 꿀팁
간단히 inner join, outer join 전환하기
User.findAll({
include: {
model: Task,
required: true // 연결된 모델이 있는 레코드만 불러옴(=inner join)
}
});
// 내가 신청한 쿼리:
const serviceDetail = await Service.findOne({
include: [{
model: User,
}],
where: { serviceId },
attributes: ['serviceId', 'customerNickname', 'image', 'customerRequest', 'ownerNickname', 'status', 'createdAt', 'updatedAt']
})
// 생성된 쿼리:
SELECT `Service`.`serviceId`, `Service`.`customerNickname`, `Service`.`image`, `Service`.`customerRequest`, `Service`.`ownerNickname`, `Service`.`status`, `Service`.`createdAt`, `Service`.`updatedAt`, `User`.`userId` AS `User.userId`, `User`.`nickname` AS `User.nickname`, `User`.`password` AS `User.password`, `User`.`phoneNumber` AS `User.phoneNumber`, `User`.`address` AS `User.address`, `User`.`
userType` AS `User.userType`, `User`.`point` AS `User.point`, `User`.`createdAt` AS `User.createdAt`, `Us
er`.`updatedAt` AS `User.updatedAt` FROM `Services` AS `Service` LEFT OUTER JOIN `Users` AS `User` ON `Service`.`ownerId` = `User`.`userId` WHERE `Service`.`serviceId` = '1';
find로 쿼리해올 때 내가 원하는 컬럼명으로 바꿔서 가져오기
이런 도움말이 있다:
If you wish to have attributes of an associated model on the same level as the main model then you need to include them manually indicating the associated model name:
const data = await modelOwners.findAll({
attributes: ["id", "name", "email",
[Sequelize.col('"LotsOwner"."distributionKeyId"'), 'costKey']
],
include: [
{
model: modelLotsTantiemes,
as: "LotsOwner",
...
그렇지 않고 더 간단한 방법이 있는 듯:
const result = await Table.findAll({
attributes: ['id', ['foo', 'bar']] //id, foo AS bar
});
어쨌든 시도해 보며 수정한 과정:
GET
, http://localhost:8080/api/services/2
주소로 데이터를 요청한 결과이다.
시도1:
제일 밑의 User란은 분명 두 외래키 중 사장님 외래키로 가져온 것이다.
한 번에 두 개의 외래키를 어떻게 가져오라고 하지..?
시도2:
시도3:
⇒ 에러 : Not unique table/alias: 'User’
SELECT `Service`.`serviceId`, `Service`.`customerId`, `Service`.`image`, `Service`.`customerRequest`, `Service`.`ownerId`, `Service`.`status`, `Service`.`createdAt`, `Service`.`updatedAt`, `User`.`userId` AS `User.userId`, `User`.`nickname` AS `User.ownerNickname`, `User`.`nickname` AS `User.customerNickname`
FROM `Services` AS `Service`
LEFT OUTER JOIN `Users` AS `User` ON `Service`.`ownerId` = `User`.`userId`
LEFT OUTER JOIN `Users` AS `User` ON `Service`.`ownerId` = `User`.`userId`
WHERE `Service`.`serviceId` = '2';
⇒ 아니 여기가진 제대로 됐는데 저 에러는 무슨 뜻이지..?ㅠㅠ
여러 조인을 하는데 테이블 명칭이 명확하지 않기 떄문이라고 한다.
(https://stackoverflow.com/questions/8084571/not-unique-table-alias)
테이블마다 별명을 잘 지어주면 된다.
시도4:
에러 메세지: User is associated to Service using an alias. You've included an alias (customer), but it does not match the alias(es) defined in your association (User).
에러 3: SequelizeAssociationError: You have used the alias owner in two separate associations. Aliased associations must have unique aliases.
try {
const serviceDetail = await Service.findOne({
// raw: true,
include: [{
model: User,
association: Service.belongsTo(User, {
foreignKey: 'ownerId',
constraints: false,
as: 'owner',
}),
// as: 'owner',
attributes: [['nickname', 'ownerNickname']],
// attributes: [],//['nickname', 'ownerNickname']],
// attributes: ['nickname'],
}, {
model: User,
association: Service.belongsTo(User, {
foreignKey: 'customerId',
constraints: false,
as: 'customer'
}),
// as: 'c',
attributes: [['nickname', 'customerNickname']],
// attributes: [] // ['nickname', 'customerNickname']],
}],
where: { serviceId },
attributes: ['serviceId',
// [Sequelize.col('"customer"."nickname"'), 'customerNickname'],
// ['customer.nickname', 'customerNickname'], // => 'customerNickname'가 되어야 함
'image', 'customerRequest',
// [`owner.nickname`, 'ownerNickname'], // => 'ownerNickname'가 되어야 함
'customer.nickname',
'owner.nickname',
'status', 'createdAt', 'updatedAt']
})
처음 모델을 생성할 때 association을 지정해줬는데 또 쿼리문으로 가져올 때 belongsTo로 지정해줬다고 그런듯, (https://stackoverflow.com/questions/57722509/sequelizeassociationerror-you-have-used-the-alias-in-two-separate-associations) 하지만 속 시원하지는 않다.
왜 처음 쿼리는 잘 가져오고 그 다음부터만 먹통이 되는가..?
와 뭔가 나오게 만들었다…
여기까지 하고 그냥 새 이름은 새 객체 만들어서 반환하게 하자…
…는 억울해서 적어보는 Sequelize의 고구마같은 답답함…:
- model 재정할 때 association 걸어버리면 쿼리문에서는 관계를 다시 지정할 수가 없음
⇒ 이게 외 필요하냐면 같은 테이블에서 외래키가 두 번 참조될 때 필요성이 생긴다. User 테이블의 userId를 참조해오는 외래키는 Services의 ‘customerId’와 ‘ownerId’인데, 그냥
inclues: [{ model: 'User', as: 'customer', }, { model: 'User', as: 'owner', }]
라고 두 번 써서는 얘가 ‘ownerId’만으로 조인해서 가져온다.
- 그래서 정확히 무슨 외래키로 연결된 애들을 각각 가져오겠다는 건지를 명시하려면 association을 써주어야 하는데,
inclues: [{ model: 'User', as: 'owner', association: Service.belongsTo(User, { foreignKey: 'ownerId', constraints: false, as: 'owner', }), }, { model: 'User', as: 'customer', association: Service.belongsTo(User, { foreignKey: 'customerId', constraints: false, as: 'customer', }), }]
여기서는 또 모델에서 한 번 이미 ‘관계’를 지정해줬는데 또 지정해준다고 에러를 띄운다. 그것도 첫 쿼리는 선심쓰듯 가져와주고 두번째부터 그런다.
as: ‘owner’ 이 별명 부분도 안 써주면 ‘이름 같은 테이블이 몇 갠데 지금 헷갈리게 하냐’고 파업하고, 써 준데도 association란 안에 써야 테이블 별명으로 인식해서 ‘이제 안 헷갈림ㅇㅇ’ 하지 바깥에 써주면 인식을 못한다.
이런 사소한 부분들이 다큐먼트에 잘 정리되어 있지가 않아서 정말 하나하나 실험해보면서 있지도 않은 위염이 도지는 기분이었다...
- 그 뿐인가. 그래서 쿼리문이나 모델 정의할 때 둘 중 한번만 ‘관계’를 지정해주려고, 모델 쪽에 가서 별명을 붙여줘 보았다.
class Service extends Model { static associate(models) { // define association here models.Service.belongsTo(models.User, { foreignKey: 'customerId', as: 'customer' }); models.Service.belongsTo(models.User, { foreignKey: 'ownerId', as: 'owner' }); models.Service.hasMany(models.Review, { foreignKey: 'serviceId' }); } }
그랬더니 이제는
{ include: ‘테이블_관계정할때_붙인_별명(=owner)’ }
이렇게 단순하게밖에 사용하지 말란다. 이건 아주 명확하게 문서에 나와있다: https://sequelize.org/docs/v6/core-concepts/assocs/#defining-an-aliasattibutes라든지 다른 속성 하나라도 붙이려고
include: [{ ’owner’ }, ‘customer’]
이렇게 중괄호 열잖아? 그러면 그 때부터include: [ { as: 'owner' }, // => Error: Include unexpected. Element has to be either a Model, an Association or an object. { model: 'User', as: 'owner' } // => include.model.getTableName is not a function 에러 'customer' ]
어디서도 뚜렷한 출구가 없이 명쾌한 도큐먼트도 없이 이러고 있으니까 아니 이정도로 늪같이 될 일인가 싶었다. 그냥 쿼리문 하나 ORM으로 만들었는데 이렇게 시원치 못하고 애매모호한 부분들이 많이 생길 문제일까..?
나 그냥 쿼리문 쓰게 해줘…
결론
: 조인을 두 번 해오되 Model 작성 시점에 설정한 ‘별명’ 그대로 불러온다. 그리고 통째로 긁어와진 User 정보를 그대로 받고, 백엔드 서버 내에서 새 객체를 생성해서 원하는 컬럼만 원하는 이름으로 다시 담아서 응답하도록 만들었다.
// models.service.js
...
class Service extends Model {
static associate(models) {
// define association here
models.Service.belongsTo(models.User, { foreignKey: 'customerId', as: 'customer' });
models.Service.belongsTo(models.User, { foreignKey: 'ownerId', as: 'owner' });
models.Service.hasMany(models.Review, { foreignKey: 'serviceId' });
}
}
Service.init( serviceId: ... )
...
// routes/services.routes.js
...
router.get('/:serviceId', ..., async (req, res) => {
...
try {
const serviceDetail = await Service.findOne({
include: ['owner', 'customer'],
where: { serviceId },
attributes: ['serviceId',
'image', 'customerRequest',
'status', 'createdAt', 'updatedAt']
})
const data = {
'serviceId': serviceDetail.serviceId,
'customerNickname': serviceDetail.customer.nickname,
'image': serviceDetail.image,
'customerRequest': serviceDetail.customerRequest,
'customerAddress': serviceDetail.customer.address,
'customerPhoneNumber': serviceDetail.customer.phoneNumber,
'status': serviceDetail.status,
'createdAt': serviceDetail.createdAt,
'updatedAt': serviceDetail.updatedAt
}
// 응답 -- //
} catch (e) { ... }
}
응답 예시:
속 시원히 ‘왜 이건 되고 이 방식은 안 되고’가 해결된 것은 아니나 일단 여기서 마감하기로 한다. (솔직히 속 시원히 해결되지가 않을 것 같다. 자료 자체가 중구난방인 느낌이라…)
Uploaded by N2T