빠르게 훝어 보는 node.js - mongoose 스키마와 유용한 기능
쿼리
간단한 삽입,삭제,수정,조회 쿼리이외에 조금 더 향상된 쿼리를 살펴보자.
자세한 쿼리 사용 방법은 http://mongoosejs.com/docs/documents.html 를 참고하면 된다.
몇 가지 쿼리들을 살펴보면
var mongoose = require('mongoose'); mongoose.connect('mongodb://localhost:27017/mydb'); var userSchema = mongoose.Schema({ userid: String, sex : String, city : String, age : Number });
var User = mongoose.model('users',userSchema);
// select city from users where userid='terry' User.findOne({'userid':'terry'}).select('city').exec(function(err,user){ console.log("q1"); console.log(user+"\n"); return; });
// select * from users where city='seoul' order by userid limit 5 User.find({'city':'seoul'}).sort({'userid':1}).limit(5).exec(function(err,users){ console.log("q2"); console.log(users+"\n"); return; });
// using JSON doc query // select userid,age from users where city='seoul' and age > 10 and age < 29 User.find({'city':'seoul', 'age':{$gt:10 , $lt:29}}) .sort({'age':-1}) .select('userid age') .exec(function(err,users){ console.log("q3"); console.log(users+"\n"); return; });
//using querybuilder //select userid,age from users where city='seoul' and age > 10 and age < 29 User.find({}) .where('city').equals('seoul') .where('age').gt(10).lt(29) .sort({'age':-1}) .select('userid age') .exec(function(err,users){ console.log("q4"); console.log(users+"\n"); return; });
|
Figure 20 mongoose 쿼리 예제
코드 첫 부분에서는
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/mydb');
var userSchema = mongoose.Schema({
userid: String,
sex : String,
city : String,
age : Number
});
var User = mongoose.model('users',userSchema);
mongoose 모듈을 로딩하고, mongodb에 연결한다. 다음 스키마를 정의 한다. userid, sex, city를 문자열로 가지고, age라는 필드를 숫자로 갖는다. 스키마 정의가 끝났으면 이 스키마를 이용해서 users 컬렉션에 대해서 User 모델 객체를 생성한다.
첫번째 쿼리를 살펴보자
// select city from users where userid='terry'
User.findOne({'userid':'terry'}).select('city').exec(function(err,user){
console.log("q1");
console.log(user+"\n");
return;
});
첫번째 쿼리는 userid가 terry인 도큐먼트를 쿼리해서 그중에서 city 라는 이름의 필드만 리턴하는 쿼리이다.
실행 결과는 다음과 같다.
q1
{ city: 'seoul', _id: 56e62f2c1a2762d26afa6053 }
두번째 쿼리를 살펴보자
// select * from users where city='seoul' order by userid limit 5
User.find({'city':'seoul'}).sort({'userid':1}).limit(5).exec(function(err,users){
console.log("q2");
console.log(users+"\n");
return;
});
다음은 실행결과이다.
q2
{ age: 18,
city: 'seoul',
sex: 'female',
userid: 'cath',
_id: 56e62f351a2762d26afa6054 },{ age: 23,
city: 'seoul',
sex: 'female',
userid: 'stella',
_id: 56e62f3c1a2762d26afa6055 },{ age: 29,
city: 'seoul',
sex: 'male',
userid: 'terry',
_id: 56e62f2c1a2762d26afa6053 },{ age: 27,
city: 'seoul',
sex: 'female',
userid: 'yuna',
_id: 56e62f411a2762d26afa6056 }
users 컬렉션에서 city 필드가 seoul인 도큐먼트를 조회한후 결과를 sort({‘userid’:1}) 을 이용하여 userid에 따라 오름차순으로 정렬한 후에, limit(5)를 이용하여 결과중 5개만 리턴한다.
세번째 쿼리를 보면
// using JSON doc query
// select userid,age from users where city='seoul' and age > 10 and age < 29
User.find({'city':'seoul', 'age':{$gt:10 , $lt:29}})
.sort({'age':-1})
.select('userid age')
.exec(function(err,users){
console.log("q3");
console.log(users+"\n");
return;
});
users 컬렉션에서 city가 seoul이고, age가 10보다 크고, 29보다 작은 도큐먼트를 조회한 후 sort를 이용하여 내림 차순으로 정렬을 한 후, select를 이용하여 userid와 age 필드만 리턴한다
다음은 쿼리 실행결과이다.
q3
{ age: 23, userid: 'stella', _id: 56e62f3c1a2762d26afa6055 },{ age: 18, userid: 'cath', _id: 56e62f351a2762d26afa6054 }
마지막 네번째 쿼리를 보면
//using querybuilder
//select userid,age from users where city='seoul' and age > 10 and age < 29
User.find({})
.where('city').equals('seoul')
.where('age').gt(10).lt(29)
.sort({'age':-1})
.select('userid age')
.exec(function(err,users){
console.log("q4");
console.log(users+"\n");
return;
});
세번째 쿼리와 내용을 같으나, 검색 조건을 find() 안에서 json으로 정의하지 않고, 밖에 where문으로 빼서 정의했다. 이를 쿼리 빌더라고 하는데, mongoose가 where문에 따라서 자동으로 쿼리를 생성해준다.
.where('city').equals('seoul') 는 city필드가 seoul인 도큐먼트를 조회하고
.where('age').gt(10).lt(29)는 age가 10보다 크고, 29보다 작은 도큐먼트를 조회하도록 한다.
다음은 쿼리 수행 결과이다.
q4
{ age: 23, userid: 'stella', _id: 56e62f3c1a2762d26afa6055 },{ age: 18, userid: 'cath', _id: 56e62f351a2762d26afa6054 }
예제 코드는 https://github.com/bwcho75/nodejs_tutorial/tree/master/mongoosequeryexample 에 저장되어 있다.
데이타 유효성 검증
mongoose가 가지고 있는 유용한 기능중의 하나가 validator라는 데이타 유효성 검증이다.
모델 객체에 들어갈 데이타 형뿐 아니라, 데이타의 규약등을 지정할 수 있는 기능인데, 예를 들어 문자열의 길이나, 특수문자 지원 여부등을 검증할 수 있다.
앞에서 만들었던 mongo.js에서 userSchema 부분을 다음과 같이 수정해보자
var mongoose = require('mongoose'); //define validator function NameAlphabeticValidator(val){ return val.match("^[a-zA-Z\(\)]+$"); } function StringLengthValidator(val){ if(val.length>10) return null; return val; }
//define scheme var userSchema = mongoose.Schema({ userid: {type:String,validate:NameAlphabeticValidator}, sex : String, city : {type:String,validate:[ {validator:NameAlphabeticValidator,msg:'city should be alphabet only'}, {validator:StringLengthValidator,msg:'city length should be less than 10'} ] } }); |
Figure 21 mongoose validator 예제
두 개의 validator를 정의하였다. 하나는 알파벳만 허용하는 NameAlphabeticValidator이고, 다른 하나는 문자열의 길이가 10 이하인 것만 통과 시키는 StringLengthValidator이다.
Validator의 정의는 간단하게 function(value)형태로 정의한다. 검증하고자 하는 값이 value라는 인자를 통해서 들어오고, 만약 검증을 통과하면 그 값을 리턴하면 되면, 실패하면 null을 리턴하면 된다.
선언된 validator 를 스키마에 적용해보자.
validator를 적용하는 방법은 스키마에서 필드의 데이타 타입을 지정하는 부분에서 위와 같이 데이타 타입을 지정한 후, 뒷부분에 validate라는 키워드를 이용하여, 앞서 정의한 validator 명을 지정해주면 된다.
userid: {type:String,validate:NameAlphabeticValidator},
또는 다음과 같이 하나의 데이타 필드에 배열[] 을 이용하여 동시에 여러개의 validator를 적용할 수 도 있다.
다음 코드는 city 필드에 NameAlphabeticValidator와StringLengthValidator 두개를 동시에 적용한 코드이다.
city : {type:String,validate:[
{validator:NameAlphabeticValidator,msg:'city should be alphabet only'},
{validator:StringLengthValidator,msg:'city length should be less than 10'}
]
validator를 지정할때 위의 예제와 같이 msg 를 같이 정의하면, 데이타에 대한 유효성 검증이 실패했을때 나는 메세지를 정의할 수 있다.
다음은 예제에서 city이름에 10자이상의 문자열을 넣는 화면이다.
validator에 의해서 유효성 검증이 실패하고, console.log로 에러 메세지가 출력된 내용이다.
Figure 22 city 필드에 10자가 넘는 문자열을 입력하는 화면
다음은 validator에 의해서 city 필드의 유효성 검사가 실패하고, console.log에 에러 메세지가 출력된 화면이다.
Figure 23 validator에 의해서 city 필드 유효성 검증이 실패한 결과
이렇게 validator를 만들어 사용하는 것 이외에도, mongoose에서는 데이타 타입별로 미리 정해놓은 validator 들이 있다.
예를 들어 Number 타입의 경우 min,max 예약어를 이용하여 타입 정의시 값의 유효 범위를 지정해놓을 수 있다.
age: { type: Number, min: 18, max: 65 }, |
String의 경우 RegularExpression을 이용해서 문자열의 형태를 지정할 수 도 있고, maxlength를 이용하여 전체 문자열 길이에 대한 제약을 둘 수 도 있다. 각 데이타 타입별로 미리 정의되어 있는 validator는 http://mongoosejs.com/docs/schematypes.html 를 참고하기 바란다.
Setter와 Getter, Default
mongoose에서는 스키마의 각 필드에 대해서 Setter와 Getter를 저장할 수 있고, 데이타를 저장하지 않았을 경우에는 디폴트 값을 지정할 수 있다.
Setter는 데이타 객체에 데이타를 저장할때, 실행되는 메서드로 데이타를 저장하기전 변환하는 역할을 수행할 수 있다.
아래 코드를 보자
var mongoose = require('mongoose');
// setter function function upperCase (val) { return val.toUpperCase(); }
var HelloSchema = new mongoose.Schema( { name : { type:String, default:'Hello Terry',set:upperCase} } );
// default test var Hello = mongoose.model('hello',HelloSchema); var hello = new Hello();
console.log(hello);
// setter test hello.name="Upper case setter example"; console.log(hello);
|
Figure 24 mongoose setter 예제
{ name : { type:String, default:'Hello Terry',set:upperCase} } 코드 부분을 보면 “default”라는 키워드로 “Hello Terry” 라는 값을 지정하였다. name 필드는 별도의 값이 지정되지 않으면 “Hello Terry”라는 문자열을 디폴트 값으로 갖는다.
다음 set:upperCase 는, Setter를 지정하는 부분으로, Setter는 “set:{Setter 함수}” 명으로 지정한다. 여기서 사용된 Setter는 위에 코드에서 정의한 upperCase 라는 함수로, 값을 지정하면 문자열의 모든 알파벳을 대문자로 바꿔서 저장한다.
위의 예제 실행 결과를 보자
{ name: 'Hello Terry', _id: 56f94e5da92daa3a977d8525 } { name: 'UPPER CASE SETTER EXAMPLE', _id: 56f94e5da92daa3a977d8525 } |
Figure 25 mongoose setter 예제 실행 결과
처음에는 아무 값도 지정하지 않았기 때문에 name 필드에 디폴트 값인 “Hello Terry” 가 저장된다.
다음으로, hello.name="Upper case setter example";로 저장을 했지만, 지정된 Setter에 의해서, name의 모든 알파벳이 대문자로 변환되어 { name: 'UPPER CASE SETTER EXAMPLE',_id: 56f94e5da92daa3a977d8525 } 로 저장된것을 확인할 수 있다.
Setter이외에 저장된 데이타를 조회할때, 변환하는 Getter 역시 지정이 가능하다.
다음 코드를 보자
var mongoose = require('mongoose');
// setter function function lowercase (val) { return val.toLowerCase(); }
var HelloSchema = new mongoose.Schema( { name : { type:String,get:lowercase} } );
// gettert test var Hello = mongoose.model('hello',HelloSchema); var hello = new Hello(); hello.name="LOWER case setter example"; console.log(hello); console.log(hello.name);
|
Figure 26 mongoose getter 예제
Getter의 지정은 스키마에서 타입 지정시 “get:{Getter 함수명}” 식으로 지정하면 된다. 위의 예제에서는
{ name : { type:String,get:lowercase} }
와 같이 lowercase 함수를 Getter로 지정하였다.
위 예제에 대한 실행 결과를 보면 다음과 같다.
{ _id: 56f94f4314540b3d97fe17b3, name: 'LOWER case setter example' } lower case setter example
|
Figure 27 mongoose getter 예제 실행 결과
실제로 데이타 객체내에 name 필드에 저장된 값은 name: 'LOWER case setter example' 이지만, hello.name으로 해당 필드의 내용을 조회했을 경우 getter로 지정된 lowercase 함수를 통해서 모두 소문자로 변환된 lower case setter example
문자열을 리턴하는 것을 확인할 수 있다.
이렇게 직접 getter와 setter에 대한 함수를 정의할 수 있지만, mongoose에는 모든 문자열을 소문자로 변경하는 lowercase setter나, 문자열 앞뒤의 공백을 없애주는 trim setter 등이 기본적으로 제공된다.
Lowercase setter 사용예
var s = new Schema({ email: { type: String, lowercase: true }})
trim setter 사용예
var s = new Schema({ name: { type: String, trim: true }})
각 데이타 타입별로 미리 제공되는 Setterd와 Getter는 http://mongoosejs.com/docs/schematypes.html 참고하기 바란다.
스키마 타입
앞서서 mongoose의 스키마에 대해서 설명하였는데, 조금 더 자세하게 살펴보자 스키마에서는 각 필드에 대한 데이타 타입을 정의할 수 있는데, 다음과 같다.
스키마 타입 |
설명 |
예제 |
String |
문자열 |
‘Hello’ |
Number |
숫자 |
135090 |
Date |
날짜 |
ISODate("1970-06-09T15:00:00.000Z") |
Buffer |
바이너리 타입 (파일등을 저장할때 사용됨) |
파일등의 바이너리 데이타 |
Mixed |
특별한 형을 가지지 않고 아무 JSON 문서나 올 수 있음 |
‘any’:{ ‘data’:’this is any data….’} |
Objectid |
mongoDB의 objectid |
ObjectId("56f8d0b63ef9d003961e5f3f") |
Array |
배열 |
[‘Hello’ , ‘Terry’ ] |
Figure 28 mongoose 스키마 타입
설명을 돕기 위해서 예제를 보자.
다음과 같은 형태 데이타를 표현하기 위한 스키마를 저장할 것이다.
사용자의 정보를 저장하는 Profile이라는 형태의 스키마이다.
{ "_id" : ObjectId("56f93d08253b92b296080587"), "meta" : { "book" : "architecture design", "company" : "cloud consulting" }, "birthday" : ISODate("1970-06-09T15:00:00.000Z"), "address" : { "_id" : ObjectId("56f8d0b63ef9d003961e5f40"), "zipcode" : 135090, "city" : "youngin", "state" : "Kyungki" }, "name" : "terry", "recommend" : [ "I want to recommend terry", "He is good guy" ], "image" : { "data" : { "$binary" : "/9j/4AAQSkZJ (중략) Rg ", "$type" : "00" }, "contentsType" : "image/png" }, "__v" : 0 } |
Figure 29 사용자 프로파일 JSON 도큐먼트 예제
이름, 생년월일, 주소, 그리고 이 사용자에 대한 추천글과, 이 사용자에 대한 이미지 파일을 저장하는 스키마이다.
이를 스키마로 지정하면 다음과 같다.
// define scheme var addressSchema = new mongoose.Schema({ zipcode : Number, city : String, state : String });
var profileSchema = new mongoose.Schema({ name : String, address : addressSchema, birthday : Date, meta : mongoose.Schema.Types.Mixed, image : { data : Buffer, contentsType : String }, recommend : [String] });
|
Figure 30 mongoose를 이용하여 schema.js 예제에서 사용자 프로파일 스키마를 정의한 부분
주소를 저장하기 위한 스키마는 addressSchema로, 숫자로된 zipcode와, 문자열로 된 city와 state 필드를 갖는다
· name은 문자열로 이름을 저장한다.
· address는 서브 도큐먼트 타입으로, 앞에서 정의한 addressSchema 형을 참조한다.
· birthday는 날짜 타입이고,
· meta는 메타 정보를 저장하는 필드인데, Mixed 타입이다. Mixed 타입은 앞에서도 설명하였듯이, 아무 JSON 도큐먼트나 들어갈 수 있다.
· 다음으로 image는 JSON 타입으로 안에, 사진 파일을 저장하기 위해서 Buffer 형으로 data 필드를 갖고, 사진 포맷 저장을 위해서 contentsType이라는 타입을 갖는다.
· 마지막으로 recommend 필드는 사용자에 대한 추천 문자열을 배열로 갖는다.
서브 도큐먼트 vs 임베디드 도큐먼트 vs Mixed 타입
이 스키마를 보면, 스키마 내에 JSON 도큐먼트를 갖는 필드가 address,meta,image 3 가지가 있다. 각 타입의 차이점은 무엇일까?
먼저 addresss는 서브 도큐먼트 (sub document) 타입으로 mongodb에 저장하면 도큐먼트 형으로 저장이 되고, _id 필드를 갖는다. 부모 도큐먼트 (여기서는 profileSchema)에 종속 되는 도큐먼트 형태로, 단독으로는 업데이트가 불가능하고 반드시 부모 도큐먼트 업데이트시에만 업데이트가 가능하다. 이러한 서브 도큐먼트 타입은 같은 타입의 서브 도큐먼트가 반복적으로 사용될때 타입 객체를 재 사용할때 사용하면 좋다.
다음 image 필드와 같이 스키마내에 JSON 도큐먼트 포맷을 그대로 저장하는 방식을 embeded 방식이라고 하는데, 서브 도큐먼트와는 다르게 _id 필드가 붙지 않는다. 간단하게 JSON 도큐먼트를 내장할때 사용한다.
마지막으로 meta 필드의 경우 Mixed 타입을 사용했는데, 아무 포맷의 JSON 문서가 들어갈 수 있다. 컬렉션 내에서 해당 필드의 JSON 도큐먼트 포맷이 각기 다를때 사용할 수 있으며, 포맷을 정의하지 않기 때문에 유연하게 사용할 수 있다.
스키마를 정의했으면 이제 값을 넣어서 저장해보자
// create model var Profile = mongoose.model('profiles',profileSchema); var Address = mongoose.model('address',addressSchema); var p = new Profile();
// populate model p.name = "terry";
// address var a = new Address(); a.zipcode = 135090; a.city = "youngin"; a.state = "Kyungki"; p.address = a;
// birthday p.birthday = new Date(1970,05,10);
// meta p.meta = { company : 'cloud consulting', book : 'architecture design'};
// image p.image.contentsType='image/png'; var buffer = fs.readFileSync('/Users/terry/nick.jpeg'); p.image.data = buffer;
// recommend p.recommend.push("I want to recommend terry"); p.recommend.push("He is good guy");
p.save(function(err,silece){ if(err){ cosole.log(err); return; } console.log(p); });
|
Figure 31 mongoose를 이용하여 schema.js 예제에서 데이타를 저장하는 부분
값을 저장하기 위해서 모델 객체를 생성한후 Profile에 대한 데이타 객체 p와 Address에 대한 데이타 객체 a를 생성하였다.
값을 저장할때는 “{데이타 객체명}.필드=값” 형태로 저장한다.
Address 저장을 위해서 데이타 객체인 a의 zipcode,city,state 값을 저장한후에, p.address = a 를 이용해서, address 필드의 값을 채워 넣는다.
p.birthday는 Date형이기 때문에, new Date() 메서드를 이용해서, Date 객체를 생성하여 데이타를 저장한다.
p.meta는 Mixed 타입으로 직접 JSON 도큐먼트를 지정하여 저장한다.
p.image는 임베디드 도큐먼트 타입으로, p.image.data, p.image.contentsType 각각의 필드에 값을 저장한다. 이때 data 필드는 Buffer 타입으로, 이 예제에서는 /Users/terry/nick.jpeg 라는 파일을 저장 하였다. fs.readFileSync를 이용하여 인코딩 지정없이 파일을 읽게 되면, 파일 데이타를 Buffer 객체로 반환해주는데, 이 값을 p.image.data 에 지정하여 저장하였다.
그리고 마지막으로, p.recommend는 String 배열로, push 메서드를 이용하여 데이타를 추가 하였다.
데이타 객체에 모든 값이 저장되었으면 이를 mongodb로 저장하기 위해서 p.save 메서드를 이용하여 저장한다.
다음 데이타를 수정하는 방법을 알아보자. 앞의 예제에서 저장된 Profile 도큐먼트의 _id가 '56f93d08253b92b296080587' 라고 하자. 아래 예제는 Profile 컬렉션에서 _id가 '56f93d08253b92b296080587' 인 도큐먼트를 찾아서 birthday를 2월( Date.setMonth(1)은 2월이다. 0부터 시작한다.)로 바꿔서 save 메서드를 이용해서 저장하는 예제이다.
var mongoose = require('mongoose'); var fs = require('fs'); mongoose.connect('mongodb://localhost:27017/mydb');
// define scheme var addressSchema = new mongoose.Schema({ zipcode : Number, city : String, state : String });
var profileSchema = new mongoose.Schema({ name : String, address : addressSchema, birthday : Date, meta : mongoose.Schema.Types.Mixed, image : { data : Buffer, contentsType : String }, recommend : [String] });
// create model var Profile = mongoose.model('profiles',profileSchema); var Address = mongoose.model('address',addressSchema); var p = new Profile();
Profile.findOne({_id:'56f93d08253b92b296080587'},function(err,p){ console.log(p); p.birthday.setMonth(1); p.save(function(err,silece){ if(err){ cosole.log(err); return; } console.log(p); }); });
|
Figure 32 mongoose에서 데이타를 조회하여 Date 필드를 업데이트 하는 예제
저장된 데이타를 robomongo를 이용해서 mongodb에서 확인해보면 다음과 같다.
Figure 33 예제 실행 결과, Date 필드 수정 내용이 반영되지 않은 결과
기대했던 결과와는 다르게, birthday가 2월로 바뀌지 않고, 처음에 생성했던 6월로 되어 있는 것을 볼 수 있다.
mongoose의 save 메서드는 필드의 값이 변환된 것만 자동으로 인식하여 save시 저장하는데, 몇몇 타입의 경우 자동으로 그 변경된 값을 인식하지 못한다.
Date와, Mixed 필드가 그러한데, 이 경우에는 mongoose 에게 해당 필드의 값이 변경되었음을 강제적으로 알려줘서 변경된 값을 인식하여 저장하게 해야 한다.
이때 markedModified(“필드명”) 메서드를 사용한다. 아래 코드는 markedModified를 이용하여 birthday 필드가 변경되었음을 명시적으로 알려주고, 값을 저장하도록 변경한 코드이다.
Profile.findOne({_id:'56f93d08253b92b296080587'},function(err,p){ console.log(p); p.birthday.setMonth(1); p.markModified('birthday'); p.save(function(err,silece){ if(err){ cosole.log(err); return; } console.log(p); }); });
|
Figure 34 markedModified를 이용하여 Date 필드가 수정되었음을 명시적으로 알려주도록 코드를 수정한 내용
위의 코드를 수정한 다음 다시 mongodb에 저장된 데이타를 보면 다음과 같다.
Figure 35 markedModified 반영후, Date 필드가 정상적으로 반영된 결과
성공적으로 birthday의 월이 2월로 변경된것을 확인할 수 있다.
스키마 타입 관련 예제 코드는 https://github.com/bwcho75/nodejs_tutorial/tree/master/mongooseschemeexample 를 참고하기 바란다.
나중에 시간되면, population 및 index도 보강 예정
'클라우드 컴퓨팅 & NoSQL > Vert.x & Node.js' 카테고리의 다른 글
빠르게 훝어 보는 node.js - redis 사용하기 (0) | 2016.03.29 |
---|---|
빠르게 훝어 보는 node.js - heapdump를 이용한 메모리 누수 추적 (0) | 2016.03.29 |
빠르게 훝어 보는 node.js - mongoose ODM 을 이용한 MongoDB 연동 (0) | 2016.03.25 |
Heroku 클라우드에 node.js 애플리이션을 배포하기 (4) | 2016.03.21 |
node.js production service stack (0) | 2016.03.18 |