반응형


ORM이란?

ORM(Object Relational Mapping)은 application과 Database사이를 맵핑시켜주는 도구이다. 한층더 추상화된 layer에서 Database에 대한 작업을 할 수 있게 해준다. ORM을 사용함으로써 얻는 장단점(pros and cons)은 다음과 같다.

1) Pros of ORM

  • 특정 DBMS에 종속되지 않는다.
  • SQL문이 코드에 들어가지 않아 깔끔한 코드를 유지할 수 있다.
  • ORM이 nesting데이터를 바인딩해준다.

2) Conf of ORM

  • RAW query에 비해 performance가 느리다.
  • Query tuning이 힘들다.
  • 서비스가 복잡해 질수록 ORM으로 할 수 있는 작업의 범위에 한계가 있다.

ORM에 대한 정보는 이곳링크를 통해 더 알아볼 수 있다. What is ORM? / Pros and Cons



Sequelize? Promise!

ORM의 종류는 여러가지가 있지만 Nodejs에서 가장 많이 사용되고 있는 ORM은 Sequelize다. Sequelize는 PostgreSQL, MySQL, MariaDB, SQLite, MSSQL을 지원하고 트랜잭션이나 relation, read replication등도 지원한다. 복잡한 쿼리에 사용될 부분들에 대해서는 오픈된 이슈들이 꽤 있고 지금도 활발하게 pull request나 commit이 이뤄지고 있다. 그리고 가장 큰 특징은 Promise를 기본으로 동작한다는 것이다. Promise는 Promise/A+ 로 불리는 spec에 따라 정의된 비동기작업 제어방식이다. ES6에는 native로 Promise가 포함되었다.

Promise의 장점은 다음과 같다.

  • 복잡한 비동기 코드를 깔끔하고 쉽게 만들 수 있는 방법을 제공한다.
  • Chaining 을 통해 값을 전달하거나 연속된 일련의 작업을 처리할 수 있다.
  • Error handling에 대한 처리를 깔끔하게 할 수 있다.

Promise를 구현한 라이브러리에는 대표적으로 QRSVPbluebird가 있다. Sequelize는 이중에서도 bluebird 라이브러리를 살짝 수정한 버전을 사용하고 있다. Promise를 비동기작업을 제어하는 방식으로 사용하는 만큼 Promise에 대해 알고 있는 부분이 많다면 Sequelize의 이용도 한결 수월해진다. Promise에 대해 더 알아보고 싶다면 다음을 참조하자. Javascript Promisehttps://www.promisejs.orgES6 Promise , Awesome-promise



테이블 생성 예


  • publisher 테이블 - 출판사에 관한 정보
  • books테이블 - 책에 대한 정보
  • user테이블 - 유저에 대한 정보
  • rent_history테이블 - 대여내역에 관한 정보




Setting up a connection

Sequelize의 설치는 npm install sequelize로 간단히 할 수 있다. 또한 Sequelize에서 postgres에 대한 작업을 하기 위해서는 추가로 pg, pg-hstore를 설치해야하므로 두개 모듈도 npm으로 설치하도록 하자. 설치 이후에는 서버가 구동될때 DB와 Connection을 맺도록 설정해야 한다. Connection에 대한 정보는 Sequelize객체를 생성할 때 parameter로 들어간다.

new Sequelize(database, [username=null], [password=null], [options={}])

var sequelize = new Sequelize('postgres://sequelize:1234@localhost/sequelize');
var sequelize = new Sequelize('sequelize', 'sequelize', '1234', {
    host: 'localhost',
    dialect: 'postgres'
});

connection연결은 위와 같이 두가지 방법 모두 가능하다.


Model Define

Sequelize에서 Model은 Database공간의 Table의 Schema를 표현하는 수단이다. Table Schema에 대응되는 Model을 정의한 이후에 실제로 메모리 공간에 올려 캐쉬하려면 import를 해야하는데 import는 바로 다음에 알아보도록하자. Model에 대한 정의는 Sequelize의 define 메소드를 이용한다. 예시는 다음과 같다.

sequelize.define('Publisher', {
    pub_id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true},
    name: {type: DataTypes.STRING(32), allowNull: false},
    established_date: {type: DataTypes.DATE, defaultValue: DataTypes.NOW}
}, {
    classMethods: {},
    tableName: 'publisher',
    freezeTableName: true,
    underscored: true,
    timestamps: false
});

define 메소드의 첫번째 파라미터는 model의 name이다. 두번째 파라미터가 실제로 Table Schema와 맵핑되는 정보이다. 즉 table의 각 column들에 대한 schema를 표현한다. 대표적인 설정 값들 몇개를 알아보자.

  • type : Data type
  • primaryKey : 기본키 인지 아닌지 설정 (default: false)
  • autoIncrement : SERIAL(auto increment)인지 아닌지 (default: false)
  • allowNull : NOT NULL 조건인지 아닌지 (default: true)
  • unique : Unique조건인지 아닌지에 대한 옵션. column하나로만 이루어진 unique라면 true/false로 지정한다. 복수개의 column이라면 동일한 문자열을 각 column의 unique속성에 넣어준다.
  • comment : column에 대한 comment
  • validate : 각 column에 대한 validation check옵션을 넣어준다.

세번째 파라미터는 config 옵션이 들어간다. 대표적인 옵션은 이와같다.

  • timestamps : Sequelize는 테이블을 생성한 후 자동적으로 createdAt, updatedAt column을 생성한다. Database에 해당 테이블이 언제 생성되었고 가장 최근에 수정된 시간이 언제인지 추적할 수 있도록 해준다. 기능을 끄려면 false로 설정한다.
  • paranoid : paranoid가 true이면 deletedAt column이 table에 추가된다. 해당 row를 삭제시 실제로 데이터가 삭제되지 않고 deletedAt에 삭제된 날짜가 추가되며 deletedAt에 날짜가 표기된 row는 find작업시 제외된다. 즉 데이터는 삭제되지 않지만 삭제된 효과를 준다. timestamps 옵션이 true여야만 사용할 수 있다.
  • underscored : true이면 column이름을 camalCase가 아닌 underscore방식으로 사용한다.
  • freezeTableName : Sequelize는 define method의 첫번째 파라미터 값으로 tablename을 자동변환하는데 true이면 이작업을 하지 않도록 한다.
  • tableName : 실제 Table name
  • comment : table 에 대한 comment




Data Type

Sequelize에서 지원하는 DataType은 다음과 같다. (sequelize 공식 사이트에서 발췌)

Sequelize.STRING                      // VARCHAR(255)
Sequelize.STRING(1234)                // VARCHAR(1234)
Sequelize.STRING.BINARY               // VARCHAR BINARY
Sequelize.TEXT                        // TEXT

Sequelize.INTEGER                     // INTEGER
Sequelize.BIGINT                      // BIGINT
Sequelize.BIGINT(11)                  // BIGINT(11)

Sequelize.FLOAT                       // FLOAT
Sequelize.FLOAT(11)                   // FLOAT(11)
Sequelize.FLOAT(11, 12)               // FLOAT(11,12)

Sequelize.REAL                        // REAL        PostgreSQL only.
Sequelize.REAL(11)                    // REAL(11)    PostgreSQL only.
Sequelize.REAL(11, 12)                // REAL(11,12) PostgreSQL only.

Sequelize.DOUBLE                      // DOUBLE
Sequelize.DOUBLE(11)                  // DOUBLE(11)
Sequelize.DOUBLE(11, 12)              // DOUBLE(11,12)

Sequelize.DECIMAL                     // DECIMAL
Sequelize.DECIMAL(10, 2)              // DECIMAL(10,2)

Sequelize.DATE                        // DATETIME for mysql / sqlite, TIMESTAMP WITH TIME ZONE for postgres
Sequelize.BOOLEAN                     // TINYINT(1)

Sequelize.ENUM('value 1', 'value 2')  // An ENUM with allowed values 'value 1' and 'value 2'
Sequelize.ARRAY(Sequelize.TEXT)       // Defines an array. PostgreSQL only.

Sequelize.JSON                        // JSON column. PostgreSQL only.
Sequelize.JSONB                       // JSONB column. PostgreSQL only.

Sequelize.BLOB                        // BLOB (bytea for PostgreSQL)
Sequelize.BLOB('tiny')                // TINYBLOB (bytea for PostgreSQL. Other options are medium and long)

Sequelize.UUID                        // UUID datatype for PostgreSQL and SQLite, CHAR(36) BINARY for MySQL (use defaultValue: Sequelize.UUIDV1 or Sequelize.UUIDV4 to make sequelize generate the ids automatically)

Sequelize.RANGE(Sequelize.INTEGER)    // Defines int4range range. PostgreSQL only.
Sequelize.RANGE(Sequelize.BIGINT)     // Defined int8range range. PostgreSQL only.
Sequelize.RANGE(Sequelize.DATE)       // Defines tstzrange range. PostgreSQL only.
Sequelize.RANGE(Sequelize.DATEONLY)   // Defines daterange range. PostgreSQL only.
Sequelize.RANGE(Sequelize.DECIMAL)    // Defines numrange range. PostgreSQL only.

Sequelize.ARRAY(Sequelize.RANGE(Sequelize.DATE)) // Defines array of tstzrange ranges. PostgreSQL only.

INTEGER, BIGINT, FLOAT, DOUBLE type에는 unsigned와 zerofill에 대한 옵션도 지정할 수 있다

Sequelize.INTEGER.UNSIGNED              // INTEGER UNSIGNED
Sequelize.INTEGER(11).UNSIGNED          // INTEGER(11) UNSIGNED
Sequelize.INTEGER(11).ZEROFILL          // INTEGER(11) ZEROFILL
Sequelize.INTEGER(11).ZEROFILL.UNSIGNED // INTEGER(11) UNSIGNED ZEROFILL
Sequelize.INTEGER(11).UNSIGNED.ZEROFILL // INTEGER(11) UNSIGNED ZEROFILL

zerofill은 남는 자릿수를 0으로 채우는 방식이다. 예를 들면 INTEGER(4).ZEROFILL type인 column에 값이 ‘4’가 들어갈경우 0004로 표기되는 방식이다. model정의와 Datatype에 대한 정보는 이곳에서 볼 수 있다. sequelize model definitionsequelize data types

지금까지 Sequelize를 사용하기에 앞서 ORM, Promise에 대한 원론적인 이야기와 함께 Sequelize 설정과 간단한 옵션에 대한 부분을 알아보았다. 다음 챕터에는 실제로 Sequelize가 application에서 어떻게 쿼리를 수행하고 리턴하는지에 대한 부분을 간단한 예제와 함께 살펴보자.



Sync

Sequelize에서는 INSERT, SELECT, DELETE, UPDATE와 같은 DML뿐만 아니라 DDL문도 지원하는데 model에 정의된 스펙을 기준으로 Database의 테이블들을 동기화할 수 있다. 그때 사용하는 메소드가 sync메소드이다. 첫 번째 챕터 model define에 관한 예제였던 publisher model내역이다.

sequelize.define('Publisher', {
    pub_id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true},
    name: {type: DataTypes.STRING(32), allowNull: false},
    established_date: {type: DataTypes.DATE, defaultValue: DataTypes.NOW}
}, {
    classMethods: {},
    tableName: 'publisher',
    freezeTableName: true,
    underscored: true,
    timestamps: false
});

이렇게 정의된 model에 대해서 sync를 하면 어떻게 될까? postgres에서 Database공간의 table의 리스트를 보는 명령어는 \dt 이다. sync를 하기전 database의 상황을 살펴보자

sequlize=> \dt
No relations found.

현재 Database공간에는 아무런 테이블도 생성되지 않았다. 그렇다면 다음과 같이 sync 메소드를 호출해보자.

db.Publisher.sync();

콘솔창에서는 로그를 확인할 수있다.


CREATE TABLE IF NOT EXISTS “publisher” 가 맨 윗줄에 보이는가? Publisher model에 대한 sync메소드를 호출시 하는 작업은 publisher테이블을 생성하는 일이다. NOT EXISTS조건이 있기 때문에 sync를 매번 하더라도 table이 없을때만 테이블 생성이 될 것이다. 다시 한번 테이블 리스트를 확인해보자.

 콘솔창 로그2

publisher 테이블이 생성된 것을 확인할 수 있다. 세부적인 테이블정보를 보면 publisher model을 정의했던 내용대로 pub_id는 primary key로 지정되었고 auto increment sequence를 갖게 되었다. 나머지 column들도 정의된대로 테이블이 생성된 것을 확인 할 수 있다. sync와 반대되는 개념의 메소드는 drop이다. drop은 테이블을 drop한다. drop메소드를 실행하면

db.Publisher.drop();

 콘솔창 로그3

drop명령어가 실행되고 publisher테이블이 삭제된 것을 확인할 수 있다. sync에는 몇가지 옵션을 parameter로 넘길 수 있는데 대표적으로 force옵션이 있다.

db.Publisher.sync({force: true});

force를 true로 놓고 sync를 실행하면 publisher테이블은 먼저 drop된 이후에 새로 생성이 된다. force옵션의 경우에는 민감한 작업을 실행하는 만큼 주의를 요하도록 한다. 또한 소스코드내에서 table에 대한 DDL까지 실행하는 것은 위험부담이 있기 때문에 되도록이면 table에 대한 관리는 flyway와 같은 관리모듈을 사용하거나 수동으로 진행하고 sync, drop 그리고 특히 force true 옵션 사용은 자제하도록 한다.

Querying(SELECT)

이제 Sequelize를 통해 SELECT를 사용해보도록 하자.출판사 리스트를 가져오는 쿼리는 다음과 같다.

models.Publisher.findAll().then(function(results) {
    res.json(results);
}).catch(function(err) {
    //TODO: error handling
});

publisher테이블에 있는 내용을 모두 가져오는 쿼리이다. Sequelize의 모든 쿼리 결과는 promise로 리턴 되기 때문에 findAll메소드 역시 promise를 리턴한다. 실제 쿼리 결과는 then의 callback에서 가져올 수 있다. catch문에서는 적절한 error handling을 해주면 된다. 이렇게 API를 호출해서 받아온 출판사리스트를 확인할 수 있다. Sequelize를 통해 가져온 값이다.

 스크린샷

SELECT와 관련있는 model 메소드는 findAll말고도 몇개가 더 있다. 아래 테이블을 참고하자.


이중에서 가장 많이 사용되는것은 findAll, find 이다. 공통적으로 많이 사용되는 옵션에 대해서 알아보자

  findAll([options]) -> Promise.<Array.<Instance>>

1) where : where옵션은 SQL문에서 where에 해당하는 부분을 기술하는 옵션이다.

  • type : object

  • example : SELECT * FROM publisher WHERE pub_id=1 을 sequelize로 표현하면

models.Publisher.findAll({
        where: {pub_id: 1}
}).then(function(result) {
    //TODO: query결과가 담긴 result에 대한 처리 진행
});
  • example : SELECT * FROM publisher WHERE pub_id > 1 AND pub_id < 4
models.Publisher.findAll({
        where: {
            $and: [{pub_id: {$gt: 1}}, {pub_id: {$lt: 4}}]
        }
}).then(function(results) {
        //TODO: query결과가 담긴 result에 대한 처리 진행
});

더 많은 예제는 이곳에서 찾을 수 있다. Sequelize model usage

2) attributes: table의 특정 column만 select할 때 사용

  • type: array | object

  • example : SELECT pub_id, name FROM publisher 를 sequelize로 표현하면

models.Publisher.findAll({
        attributes: ['pub_id', 'name']
}).then(function(results) {});
  • example : SELECT pub_id, name FROM publisher는 다음과 같은 방식으로도 표현
models.Publisher.findAll({
        attributes: {exclude: ['established_date']}
}).then(function(results) {});
// exclude는 table의 column중에서 제외시킬 리스트를 넣고 include에는 포함시킬 리스트를 넣는다.

3) order: SQL문의 order by에 해당하는 부분을 기술하는 옵션이다.

  • type: string | array

  • example : SELECT * FROM publisher ORDER BY pub_id DESC 는 다음과 같다.

models.Publisher.findAll({
        order: 'pub_id DESC'
}).then(function(results){});
// order: 'pub_id DESC’ 는 order: [['pub_id', 'DESC']] 로도 사용할 수 있다.

4) limit, offset: SQL문의 limit, offset에 해당하는 부분을 기술하는 옵션

  • type : Number

  • example : SELECT * FROM publisher LIMIT 1 OFFSET 1은 다음과 같다.

models.Publisher.findAll({
    offset: 1,
    limit: 1
}).then(function(result) {});

5) include: Eager loading, Relation과 관련된 옵션으로 다음 챕터에서 알아보도록 하자.

6) transaction: 어떤 하나의 트랜잭션 안에서 동작하도록 하는 옵션. 다다음 챕터에서 알아보도록하자.

findAll, find에 대해 더 알아보고 싶다면 이 곳을 참고하자 Sequelize-model-findAll

Querying(INSERT)

예제 Publisher 메뉴에 가면 이런 화면을 볼 수 있을 것이다.

 인서트스샷1

우측에는 미리 넣어놓은 5개의 출판사 정보가 있고 ‘Webframeworks.kr’ 의 이름을 가진 새 출판사를 등록해보자. Register버튼을 누르면 등록 API를 call하고 서버에서는 이와 같은 쿼리를 실행한다.

models.Publisher.create({name: options.name}).then(function(result) {
    res.json(result);
}).catch(function(err) {
    //TODO: error handling
});

options.name에는 화면에서 적었던 Webframeworks.kr 문자열이 들어있다. Sequelize model의 create메소드는 SQL의 INSERT INTO와 같은 역할을 한다. 메소드의 형태는 다음과 같다.

create(values, [options]) -> Promise.<Instance>

values에는 실제로 insert할 값들에 대한 object가 들어간다. options에는 부가적인 정보가 들어가는데 transaction에 대한 정보가 대표적이다. create메소드 역시 promise를 리턴하며 성공적으로 쿼리가 실행되었을 경우에는 insert된 row정보를 얻을 수 있다. API호출이 성공되면 화면에서 새로 추가된 출판사 정보를 확인할 수 있다.

 인서트스샷3

row여러개를 한꺼번에 insert하려면 bulkCreate 메소드를 이용하자 create와 사용방법은 똑같다. 차이점은 첫번째 파라미터에 value object들에 대한 array가 들어가야 한다는 것이다.

bulkCreate(records, [options]) -> Promise.<Array.<Instance>>

create메소드에 대해서 더 알아보고 싶다면 이곳을 참조하자 Sequelize-model-create bulkCreate메소드에 대해서 더 알아보고 싶다면 이곳을 참조하자 Sequelize-model-bulkCreate

Querying(UPDATE)

등록한 출판사의 이름이 잘못등록되어 수정이 필요하다면? Webframeworks.kr의 출판사 이름을 Webframeworks로 바꿔보자. 아래 예제 그림처럼 변경할 출판사 이름을 적고 Update버튼을 누르면 변경 API를 call하고 서버에서는 다음과 같은 쿼리를 실행한다.

 업데이트스샷1

models.Publisher.update({name: newName},
 {where: {pub_id: pub_id}, returning: true}).then(function(result) {
      res.json(result[1][0]);
 }).catch(function(err) {
      //TODO: error handling
 });

SQL문으로는 UPDATE publisher SET name='Webframeworks' WHERE pub_id=6 RETURNING * 과 같다. update메소드는 조건에 맞는 복수개의 row에 대해서 update를 실행한다. 메소드의 형태는 이렇다.

update(value, options) -> Promise.<Array.<affectedCount, affectedRows>>

values에는 update해야할 value들의 object가 들어간다. options에 사용되는 것중 대표적으로는 transaction관련 옵션이 있고 postgres에서만 사용할 수 있는 returning옵션이 있다 returning 옵션이 true이면 update결과 후 row의 정보가 리턴된다. 결과로는 array타입이 리턴되는데 첫번째 인덱스에서는 update된 row의 갯수를 두번째 인덱스에서는 update된 row들의 정보를 얻을 수 있다. API호출이 성공되면 다음과 같이 값이 정상적으로 update된것을 확인할 수 있다.

 업데이트스샷2

update메소드에 대해 더 알아보고 싶다면 이곳을 참조하자 Sequelize-model-update

Querying(DELETE)

위에 예제 스크린샷들을 보면 맨 오른쪽에 있는 휴지통 빨간버튼이 보일 것이다. 이 버튼은 해당 출판사의 정보를 삭제하는 버튼이다. Webframeworks로 등록된 출판사를 삭제해보자. 버튼을 누르면 삭제 API를 call하고 서버에서는 다음과 같은 쿼리가 실행된다.

models.Publisher.destroy({where: {pub_id: pub_id}}).then(function(result) {
    res.json({});
}).catch(function(err) {
    //TODO: error handling
});

SQL문으로는 DELETE FROM publisher where pub_id = 6; 과 같다 destroy메소드는 SQL에서 DELETE와 같은 역할을 하며 조건에 맞는 복수개의 row를 삭제한다. 메소드의 형태는 이렇다.

destory(options) -> promise<integer>

options에 필수적으로 빼놓지 말고 넣어야 하는 사항은 where조건이다. where조건을 넣지 않고 destroy메소드를 실행하면 테이블에 있는 모든 row가 삭제되므로 꼭꼭 조심하도록 하자. 그외에는 transaction 옵션등이 들어간다. delete메소드에 대해 더 알아보고 싶다면 이곳을 참조하자. Sequelize-model-destroy

이번챕터에는 정의된 model에 대한 sync작업과 간단하게 CRUD작업을 할 수 있는 예제를 통해 Sequelize 사용법을 알아보았다. find, findAll의 경우에는 option을 어떻게 주느냐에 따라 복잡한 쿼리에 대응하는 작업도 할 수 있는 만큼 많이 사용해보길 바란다. 다음 챕터에서는 Sequelize로 테이블의 relation을 만들어주고 join을 포함한 작업 및 raw query의 사용법등을 알아보도록 한다.




Associations

예제의 다음 기능은 출판사 별로 등록된 책을 보여주고 새로운 서적을 books테이블에 등록하는 일이다. publisher테이블에 이어 books테이블도 만들어 보자. SQL문으로는 다음과 같다.

CREATE TABLE IF NOT EXISTS books (
    book_id SERIAL PRIMARY KEY,
    pub_id INTEGER REFERENCES publisher NOT NULL,
    title VARCHAR(64) NOT NULL,
    author VARCHAR(16) NOT NULL,
    stock SMALLINT NOT NULL DEFAULT 1,
    register_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

books테이블의 pubid는 책의 출판사ID이고 publisher테이블의 pubid를 foreign key로 갖는다. Sequelize의 model define으로 표현하면 이렇게 될 것이다.

sequelize.define('Books', {
    book_id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true},
    pub_id: {type: DataTypes.INTEGER, allowNull: false, references: {model: models.Publisher, key: 'pub_id'}},
    title: {type: DataTypes.STRING(64), allowNull: false},
    author: {type: DataTypes.STRING(16), allowNull: false},
    stock: {type: DataTypes.INTEGER, defaultValue: 1},
    register_date: {type: DataTypes.DATE, defaultValue: DataTypes.NOW}
}, {
    classMethods: {},
    tableName: 'books',
    freezeTableName: true,
    underscored: true,
    timestamps: false
});

여기서 publisher 테이블과 다른 점은 books테이블의 column하나가 reference라는 것이다. 출판사는 여러개의 책을 출판하므로 1:N의 관계가 성립된다. Sequelize에서는 model간의 관계를 정의하는 4가지의 association 메소드가 있다. (hasMany, hasOne, belongsTo, belongsToMany) 이 중에서 이번 예제에서 필요한 hasMany 메소드부터 하나씩 살펴보도록하자. association 설정은 다음과 같이 한다.

db.Publisher.hasMany(db.Books, {foreignKey: 'pub_id'});

Publisher model과 Books model이 1:N으로 관계되어있다는 뜻이다. hasMany는 1:N관계를 맺을 때 사용하는 메소드이다. association관련 메소드의 option항목은 대표적으로 foreignKey, as가 있다. foreignKey에는 foreignKey로 사용되는 column의 name이 들어간다





ref : http://webframeworks.kr/tutorials/expressjs/expressjs_orm_one/

ref : http://webframeworks.kr/tutorials/expressjs/expressjs_orm_two/#tocAnchor-1-1

ref : http://webframeworks.kr/tutorials/expressjs/expressjs_orm_three


반응형

+ Recent posts