인스턴스

  • 이 글은 6. Instances를 번역한 글입니다.
  • 아직 입문자이다보니 오역이 있을 수 있습니다. 양해 부탁드립니다.
  • 매끄러운 번역을 위하여 의역을 한 경우가 있습니다. 원문의 뜻을 최대한 해치지 않도록 노력했으니 안심하셔도 됩니다.
  • 영어 단어가 자연스러운 경우 원문 그대로의 영단어를 적었습니다.
  • 저의 보충 설명은 인용문에 적었습니다.

시작하기 전에

  • 이 글은 공식 도큐먼트의 6번째 챕터입니다.
  • 원문에서는 예시 코드를 제공하지만, 코드만 제공할 뿐 CRUD를 위한 데이터셋 세팅, 실습 환경에 대한 정보가 없고, 실행 결과를 확인할 수 있는 방법도 알려주지 않습니다. 그래서 간단하게나마 실습해볼 수 있도록 세팅하는 방법을 이전 글에서 소개하였으니 필요에 따라 활용해주시면 감사드리겠습니다.

변하는 인스턴스 만들기

정의된 클래스의 인스턴스를 생성하려면 다음을 따라하면 됩니다. 과거에 Ruby를 사용해봤다면 익숙한 문법일 것입니다. build 메서드를 사용하면 저장이 되지 않는 객체, 그말인즉슨 명시적으로 저장을 해야하는 객체를 반환합니다.

const project = Project.build({
  title: 'my awesome project',
  description: 'woot woot. this will make me a rich man'
})

const task = Task.build({
  title: 'specify the project idea',
  description: 'bla',
  deadline: new Date()
})

생성된 인스턴스는 클래스가 정의될 때 정해진 기본값을 자동으로 갖습니다:

// 우선, 모델을 정의한다.
const Task = sequelize.define('task', {
  title: Sequelize.STRING,
  rating: { type: Sequelize.STRING, defaultValue: 3 }
})

// 이제 인스턴스를 생성한다.
const task = Task.build({title: 'very important task'})

task.title  // ==> 'very important task'
task.rating // ==> 3 (기본값)

DB에 저장하려면 save 메서드를 사용하고, 필요에 따라 catch로 예외를 처리해줍니다.

project.save().then(() => {
  // 제대로 저장된 뒤에 할 일들
})

task.save().catch(error => {
  // mhhh, wth!
})

// 인스턴스를 생성하고, 저장한 뒤, 해당 인스턴스에 체이닝을 통하여 접근할 수 있다:
Task
  .build({ title: 'foo', description: 'bar', deadline: new Date() })
  .save()
  .then(anotherTask => {
    // anotherTask 변수를 통하여 방금 전에 저정한 task 인스턴스에 접근할 수 있다... 어게이~
  })
  .catch(error => {
    // 저런, 오류 처리를 하자.
  })

변하지 않는 인스턴스를 만들기

.build()로 생성한 인스턴스를 DB에 저장하려면 명시적으로 .save()를 호출해야 하지만, .create()를 사용하면 이 모든 작업을 생략하고, 인스턴스의 데이터를 자동으로 DB에 저장시켜줄 수 있습니다.

Task.create({ title: 'foo', description: 'bar', deadline: new Date() }).then(task => {
  // task 변수를 통하면 새로 생성된 인스턴스에 접근할 수 있다.
})

create 메서드를 통하여 어떤 특성이 설정될지 선택할 수 있습니다. 특히 이것은 사용자 입력을 폼으로 받아서 DB에 저장할 때에 편리합니다. 아래의 예시에서는, User 모델에서 username 특성만 저장하고, isAdmin 플래그 특성은 저장하지 않도록 제한하고 있습니다.

User.create({ username: 'barfooz', isAdmin: true }, { fields: [ 'username' ] }).then(user => {
  // isAdmin의 기본값이 false라고 가정하자
  console.log(user.get({
    plain: true
  })) // => { username: 'barfooz', isAdmin: false }
})

Update / Save / Persisting- DB에 인스턴스를 저장하기

이번에는 값을 변경하고, 변경점을 DB에 반영해봅시다. 2가지 방법이 있습니다:

// 방법 1
task.title = 'a very different title now'
task.save().then(() => {})

// 방법 2
task.update({
  title: 'a very different title now'
}).then(() => {})

save 메서드를 호출했을 때 어떤 특성이 저장되는지를 정의할 수도 있습니다. 컬럼 이름으로 이루어진 배열을 전달하면 됩니다. 이렇게 하면 이미 정의된 객체를 활용하여 특성을 설정할 수 있어서 편리합니다. 웹 사이트의 폼을 통하여 값을 받는 경우 등을 떠올려보면 됩니다. 이 방식은 update에서 내부적으로 사용되는 방식이기도 합니다. 아래와 같이 사용됩니다:

task.title = 'foooo'
task.description = 'baaaaaar'
task.save({fields: ['title']}).then(() => {
 // title은 'foooo'라는 값을 가지지만, description은 값이 변하지 않는다.
})

// 동등한 작업을 update로 수행하면 아래와 같다:
task.update({ title: 'foooo', description: 'baaaaaar'}, {fields: ['title']}).then(() => {
 // title은 'foooo'라는 값을 가지지만, description은 값이 변하지 않는다.
})

아무런 특성도 변하지 않은 상태에서 save를 실행하면, 이 메서드는 아무 작업도 수행하지 않습니다.

Destroying / DB의 항목을 삭제하기

DB에 항목을 생성한 뒤 해당 항목에 대한 참조를 얻었다면, DB에서 제거하는 것도 가능합니다.

Task.create({ title: 'a task' }).then(task => {
  // now you see me...
  return task.destroy();
}).then(() => {
 // now i'm gone :)
})

paranoid 옵션이 true라면, 객체는 제거되지 않고 deleteAt 컬럼이 현재의 timestamp 값을 갖게 됩니다. 강제로 삭제하려면, destroy 호출시 force: true를 인자로 전달하면 됩니다.

task.destroy({ force: true })

Working in bulk - 다수 항목에 대한 생성, 갱신, 삭제 작업

단일 인스턴스에 대한 작업뿐만 아니라, 다수 인스턴스를 한번에 생성하거나, 변경하거나, 삭제할 수 있습니다. 여기서 사용할 수 있는 메서드는 다음과 같은 형식을 가집니다:

  • Model.bulkCreate
  • Model.updated
  • Model.destroy

여러 항목에 대한 작업이기 때문에, 이 메서드들은 DAO 인스턴스를 반환하지 않습니다. BulkCreate는 인스턴스/DAO로 이루어진 배열을 반환하지만, create와 달리 autoIncrement 특성의 결과값을 갖지 않습니다. updatedestroy는 영향을 받은 레코드의 개수를 반환합니다.

bulkCreate를 먼저 살펴봅니다:

User.bulkCreate([
  { username: 'barfooz', isAdmin: true },
  { username: 'foo', isAdmin: true },
  { username: 'bar', isAdmin: false }
]).then(() => { // 결과가 전달되지 않으므로, 직접 가져와야 합니다.
  return User.findAll();
}).then(users => {
  console.log(users) // 그래야 user 인스턴스들의 배열을 얻을 수 있습니다.
})

여러 레코드들을 한번에 update합니다:

Task.bulkCreate([
  {subject: 'programming', status: 'executing'},
  {subject: 'reading', status: 'executing'},
  {subject: 'programming', status: 'finished'}
]).then(() => {
  return Task.update(
    { status: 'inactive' }, /* 변경할 특성의 값을 명시합니다. */
    { where: { subject: 'programming' }} /* 변경될 레코드의 조건을 명시합니다. */
  );
}).spread((affectedCount, affectedRows) => {
  // .update는 배열을 통하여 2개의 값을 전달하므로, spread 메서드를 사용합니다.
  // affectedRows는 returning: true를 제원하는 DB에서만 사용할 수 있습니다.

  // affectedCount는 2
  return Task.findAll();
}).then(tasks => {
  console.log(tasks) // 'programming' task들은 둘 다 status 컬럼의 값으로 'inactive'를 갖습니다.
})

마지막으로 delete합니다:

Task.bulkCreate([
  {subject: 'programming', status: 'executing'},
  {subject: 'reading', status: 'executing'},
  {subject: 'programming', status: 'finished'}
]).then(() => {
  return Task.destroy({
    where: {
      subject: 'programming'
    },
    truncate: true /* where을 무시하고 대신 truncate를 수행합니다. */
  });
}).then(affectedRows => {
  // affectedRows는 2
  return Task.findAll();
}).then(tasks => {
  console.log(tasks) // no programming, just reading :(
})

사용자로부터 값을 직접 전달받는다면, DB에 실제로 삽입되는 컬럼을 제한하는 것이 바람직합니다. bulkCreate()는 2번째 인자로 옵션 객체을 전달받습니다. 이 옵션 객체는 fields 속성(배열값)을 가지며, 항목 생성에 사용될 컬럼을 명시적으로 알려줄 수 있습니다.

User.bulkCreate([
  { username: 'foo' },
  { username: 'bar', admin: true}
], { fields: ['username'] }).then(() => {
  // 'bar'의 admin 컬럼은 무시된다
})

본래 bulkCreate는 레코드를 삽입하는 빠르고 주로 쓰이는 방법으로서 고안되었습니다. 하지만, 세련된 모델 검증 을 하면서도 동시에 여러 레코드를 삽입해야 할 때도 있을 것입니다. 그럴 때는 옵션 객체로 validate: true 속성을 넘겨주면 됩니다.

const Tasks = sequelize.define('task', {
  name: {
    type: Sequelize.STRING,
    validate: {
      notNull: { args: true, msg: 'name cannot be null' }
    }
  },
  code: {
    type: Sequelize.STRING,
    validate: {
      len: [3, 10]
    }
  }
})

Tasks.bulkCreate([
  {name: 'foo', code: '123'},
  {code: '1234'},
  {name: 'bar', code: '1'}
], { validate: true }).catch(errors => {
  /* console.log(errors)는 아래와 같은 결과를 보입니다:
  [
    { record:
    ...
    name: 'SequelizeBulkRecordError',
    message: 'Validation error',
    errors:
      { name: 'SequelizeValidationError',
        message: 'Validation error',
        errors: [Object] } },
    { record:
      ...
      name: 'SequelizeBulkRecordError',
      message: 'Validation error',
      errors:
        { name: 'SequelizeValidationError',
        message: 'Validation error',
        errors: [Object] } }
  ]
  */
})

설명에는 적혀있지 않지만, validate를 정의하면 미리 정해놓은 오류 메세지가 출력해줄 수 있습니다. create를 사용할 때에는 알아서 validate가 설정되있는지 체크하여 해당 메세지를 띄우지만, bulkCreate는 Sequelize가 기본으로 설정한 오류 처리를 하게 됩니다. 이때 validate 옵션을 활용하고 싶다면 예시 코드와 같이 실행하면 됩니다.

인스턴스의 값

console.log 등을 사용하여 인스턴스의 내부를 들여다보면, 여러가지가 많이 들어있다는 것을 알 수 있습니다. 그런 것들을 다 숨기고 관심있는 정보만 추리려면, get- 속성을 사용하면 됩니다. 이 메서드를 plain: true 옵션과 함께 실행하면, 해당 인스턴스의 데이터만 반환됩니다.

Person.create({
  name: 'Rambow',
  firstname: 'John'
}).then(john => {
  console.log(john.get({
    plain: true
  }))
})

// 결과:

// { name: 'Rambow',
//   firstname: 'John',
//   id: 1,
//   createdAt: Tue, 01 May 2012 19:12:16 GMT,
//   updatedAt: Tue, 01 May 2012 19:12:16 GMT
// }

참고 : 인스턴스를 JSON으로 변환할 수 있습니다. JSON.stringify(instance)를 사용해보세요. 위의 예시와 거의 유사한 결과를 얻을 수 있습니다.

아주 중요한 팁입니다. 쿼리 결과를 편리하게 활용하려면 중요한 데이터만 추려야겠죠?

인스턴스 다시 불러오기

어떤 인스턴스를 DB와 동기화하려면, reload 메서드를 사용하세요. 메서드를 호출한 인스턴스에 대하여 DB에서 값을 가져와 해당하는 컬럼에 덮어써줍니다.

Person.findOne({ where: { name: 'john' } }).then(person => {
  person.name = 'jane' // DB와 무관한 현재 인스턴스에 대한 변경
  console.log(person.name) // 'jane'

  person.reload().then(() => {
    console.log(person.name) // 'john'
  })
})

증가

동시 실행의 문제 없이 인스턴스의 값을 증가시키려면 increment를 사용하세요. 여러 가지의 방법을 제공합니다.

첫번째, 더하고 싶은 컬럼과 더하고 싶은 수치를 정합니다.

User.findById(1).then(user => {
  return user.increment('my-integer-field', {by: 2})
}).then(user => {
  // PostgresSQL의 경우, 기본값으로 갱신된 user를 반환한다.
  // 다른 DB의 경우, 갱신된 값을 확인하려면 user.reload()를 해야 한다.
})

둘째, 여러 개의 칼럼에 대하여 수행할 수 있습니다.

User.findById(1).then(user => {
  return user.increment([ 'my-integer-field', 'my-very-other-field' ], {by: 2})
}).then(/* ... */)

셋째, 더하고자 하는 칼럼과 더하고자 하는 수치에 대한 키-값으로 이루어진 객체를 정의할 수 있습니다.

User.findById(1).then(user => {
  return user.increment({
    'my-integer-field':    2,
    'my-very-other-field': 3
  })
}).then(/* ... */)

감소

동시 실행의 문제 없이 인스턴스의 값을 감소시키려면 decrement를 사용하세요. 여러 가지의 방법을 제공합니다.

첫번째, 빼고 싶은 컬럼과 빼고 싶은 수치를 정합니다.

User.findById(1).then(user => {
  return user.decrement('my-integer-field', {by: 2})
}).then(user => {
  // PostgresSQL의 경우, 기본값으로 갱신된 user를 반환한다.
  // 다른 DB의 경우, 갱신된 값을 확인하려면 user.reload()를 해야 한다.
})

둘째, 여러 개의 칼럼에 대하여 수행할 수 있습니다.

User.findById(1).then(user => {
  return user.decrement([ 'my-integer-field', 'my-very-other-field' ], {by: 2})
}).then(/* ... */)

셋째, 빼고자 하는 칼럼과 빼고자 하는 수치에 대한 키-값으로 이루어진 객체를 정의할 수 있습니다.

User.findById(1).then(user => {
  return user.decrement({
    'my-integer-field':    2,
    'my-very-other-field': 3
  })
}).then(/* ... */)

다음 편은 7. Associations 로 이어집니다.