Node.js ile Uygulama Geliştirme – 6 : REST Mimarisi ve RESTful Servisler Geliştirme [Sequelize, SQL ve veri modellerimiz]

Node.js dünyasında, MongoDB kullanımı çok yaygın olsada, SQL veritabanlarının kullanımıda git gide artmakta. Aslında teknık olarak çok doğru bi tanımlama olmasada(yaygınlık), pratikde karşılığı olan bir konu. Pratikde ki karşılığından kastımız ise, Node.js dünyasında, SQL veri tabanları için derli toplu ORM lerin geliştirilmesinin biraz zaman alması diyebiliriz. MongodDB ister Mongoose gibi bir ORM ile kullanılsın, ister MongoDB nin native driverlaryıla kullanılsın sahip olduğu “Javascript sorgu modeli” sayesinde node dünyası içinde çok hızlı bir popülerlik kazandı.

Son 1-2 yıldır SQL cephesinde “Sequelize, Knex ve Knex/Bookshelfgibi bir çok ORM in ortaya çıkması ve zaman içinde ciddi seviyede “production” kalitisine ulaşılmasıyla birlikte artık Node.js dünyasında SQL veri tabanlarının kullanımda ciddi şekilde artmaya başladı. Biz yazı dizimizde Sequelize ı ORM olarak kullanacağız. Knex yada knex ile birlikte Bookshelf kullanımıda aynı şekilde kolay dilerseniz ufak birkaç değişiklik yaparak Sequelize yerine knex i de kullanabilirsiniz. 

Sequelize

PostgreSQL, MySQL, MariaDB, SQLite and MSSQL veri tabanlarını destekleyen ve ayrıca güçlü bir ” transaction ” ve ” relations” desteği olan SQL ORM Sequelize.

Sequelize ile SQL sorgularımızı oluşturduğumuz modeller aracılığıyle kolayca oluşturabilmekteyiz. Oluşturduğumuz sequelize modelleri bize ayrıca bir çok kolaylık ve genel kullanım için hazırlanmış metodlar yardımcı fonksiyonlar vs de sunmakta.

Eğer bir önceki yazıdaki örnekle devam ediyorsanız, sequelize ile ilgili herşey uygulamamızda hazır. Direkt olarak kullanmaya başlayabiliriz. Diğer türlü sequelize ı node.js ile kullanmak için aşağıdaki yolu takip edebilirsiniz;

  1. npm install –save sequelize
    1. sequelize paketini npm aracılığyla indiriyoruz
  2. Kullanmak istediğiniz veri tabanı için aşağıdaki driverlardan birini indiriyoruz
    1. npm install –save pg pg-hstore   //postgres
    2. npm install –save mysql //  mysql and mariadb
    3. npm install –save tedious // MSSQL
    4. npm install –save sqlite3
  3. Modellerimizi oluşturuyoruz (örnek biraz sonra)
  4. Son olarak sequelize i “boot yada sync ” edip kullanmaya başlıyoruz (örnek birazdan)

Ben kurulumları örnek uygulamamızı baz alarak yapacağım ama genel olarak süreç basit ve kendinize göre kolayca özelleştirebilirsiniz. Genel olarak code-first diye tabir edilen “önce veri model sınıfılarımızı yazıp sonra bu sınıflardan veritabanın oluşturulması” yöntemini kullanacağız. Yazı dizisinde kullancağımız model sayısı az olduğu için ve yazı serisinin esas amacı node.js ve express olduğu için veri tabanı kısmını biraz basit tutmaya çalışacağım.

Şimdilik 3 model oluşturarak başlayalım. Modellerimizi, models klasörü altında tutacağız. Genel bir yöntem olarak bende ilk önce temel kullanıcı sınıfını olusturarak başlayacağım.

Model sınıflarımızı, sequelize ile oluşturacağız bu yüzden ilk olarak sequelize ın standart veri modeline yada base/ana sınıf diyebileceğimiz yapıya bakarak devam edelim.

sequelize.define('name', {attributes}, {options})

şeklinde bir signature yada interface sahip base/ana sınıfımız. sequelize ın define() metoduna ilk parametre olarak, “model ismini” , ikinci parametre olarak “attributes / tablomuzdaki sütünları” ve son olarakda bazı ayarlar ve konfigürasyonları geçerek modelimizi oluşturabilmekteyiz. İlk olarak kullanıcı sınıfımızı oluşturarak başlayalım,

models/User.js

import bcrypt from "bcrypt";

module.exports = (sequelize, DataType) => {
    const Users = sequelize.define("Users", { //Users tablomuz
            id: {
                type: DataType.INTEGER,
                primaryKey: true,
                autoIncrement: true 
            },
            name: {
                type: DataType.STRING,
                allowNull: false,
                validate: {
                    notEmpty: true
                }
            },
            password: {
                type: DataType.STRING,
                allowNull: false,
                validate: {
                    notEmpty: true
                }

            },
            email: {
                type: DataType.STRING,
                unique: true,
                allowNull: false,
                validate: {
                    notEmpty: true,
                    isEmail: true
                }
            },
            role: {
                type: DataType.STRING,
                defaultValue: 'kullanici',
                validate: {
                    isIn: [['kullanici', 'yonetici', 'calisan']],
                }
            }
        },
        // Model options, hooks and scope
        {
            defaultScope: {
                attributes: ["id", "name", "email", "role"]
            },
            scopes: {
                "login": {
                    attributes: ["id", "name", "email", "role", "password"]
                }
            },

            paranoid: true,
            hooks: {
                beforeCreate: (user) => {
                    const salt = bcrypt.genSaltSync();
                    user.password = bcrypt.hashSync(user.password, salt);
                }
            },
            classMethods: {
                associate: (models) => {
                    return false;
                },
                isPasswordCorrect: (encoded, password) => {
                    console.log(encoded, " , ", password)
                    return bcrypt.compareSync(password, encoded);
                }
            }
        }
    );
    Users.hook('afterCreate', function (user, options) {

        delete user.dataValues.password
        delete user.dataValues.updated_at
        delete user.dataValues.deleted_at

        return Promise.resolve(user);
    });
    return Users;
};

Modelimizin, “id”, “name”, “email”, “role”, “password” dan oluşan 4 attribute / alani var ve birde biz aksini belirtmediğimiz sürece Sequelize trafından “timestamps/ zaman damgaları” dediğimiz “created_at ve updated_et” alanları otomatik olarak her modele eklenecektir. Toplamda 6 sütünumuz olmuş oldu.

atribute tanımları gördüğünüz gibi kendi içinde bir çok özelliğe sahip, örneğin doğrulama veri türleri vs bir çok özelliği direkt olarak veri modelimiz iöinde attributelarımız ile tanımlayabilmekteyiz. Örneğin “id” alanımızı autoIncrement ve primaryKey oalrak tanımladık. diğer bir örnekde email alanımız. Bu alanın her zaman unique/tekil olması için ve geçerli bir email olması için aşağıdaki validation/doğrulamaları ekledik;

unique: true,
allowNull: false,
validate: {
    notEmpty: true,
    isEmail: true
}

Sequelize çok zengin bir özellik ve doğrulama alt yapısı ile birlikte gelmekte, ve güzel bir döküman sayfası mevcut. Diğer seçeneklere oradan bakabilrisiniz. Attribute tanimlarimizdan sonra, model options kısmı ile “hooks,  classMethods ve scope ” tanımlarımızı yapabilmekteyiz.

  1. hooks : her veri tabanı işleminden önce ve ya sonra çalışmasını istediğimiz fonksiyonlar. Örneğin bir kullanıcıyı kayıt etmeden önce şifresini kriptolamak için before hooks ları kullanabiliriz. Tüm hook listesine buradan bakabilirsiniz.  
    beforeCreate: (user) => {
        const salt = bcrypt.genSaltSync();
        user.password = bcrypt.hashSync(user.password, salt);
    }
  2. scopes : Öntanımlı sorgu alanları oluşturmak için kullanabileceğimiz fonksiyolarımız. Örneğin veri tabanında ürünler tablomuzda aksini belirtmediğimiz sürece bütün select sorgularında sadece stok daki ürünleri sorgulamak yada Kullanıcı kaydını veritabanından sorguladığımızda “özellikle aksini belirtmediğimiz sürece” hiç bir zaman şifre alanının seçilmemesi gibi ön tanımlı şartlarımız yada sorgu filitrelerimizi tanımlamak gibi. Scopes
    defaultScope: { //aksi belirtilmedikçe, şifre alanını seçme!
        attributes: ["id", "name", "email", "role"] 
    }
  3. classMethods : Sequelize, bizim için bir çok yardımcı metod sunmakta (find() , findOne() , findById() vs. ) bu dahili metodların haricinde bize kendi metodlarımızı da oluşturma imkanı vermekte. Bu metodlar illaki sorgu metodları olmak zorunda da değil, bizim örneğimizde kullandığımız gibi işlevsel bir metod olabileceği gibi ( isPasswordCorrect() metodu gibi) kendi özel sorgularımızı taşıyan metodlarda olabilir.  Class metodlarinin içinde, “associate()” metodu özel bir yer tutmakta, bu metod Sequelize tarafından sağlanan bir metod ve modelimizin diğer modellerle “relations / ilişki tanımlarını ” tanımlayacağımız metod( oneToMany, ManyToMany vs. gibi) Kullanıcı sınıfımızın şuan bir başka modeller ilişkisi olmadığı için bu alanı;
    associate: (models) => {
        return false;
    },

    şeklinde tanımladık. Diğer iki modelimizde bu konuyla ilgili örnek bir kullanım bulunmakta.  Son olarak model tanimiz dan sonra aşağıdaki gibi bir hook tanımlamamız da var.

    Users.hook('afterCreate', function (user, options) {
        delete user.dataValues.password
        delete user.dataValues.updated_at
        delete user.dataValues.deleted_at
        return Promise.resolve(user);
    });

hatırlarsanız, ilk maddede hookları model içinde tanımlamıştık. Bu tarz bir kullanım ise ikinci bir alternatif, Modeli tanımladıktan sonra bu şekilde hook ve classMethods tanımlama yapma imkanımız da var. Ben iki seçeceğide gösterebilmek için ikisinide kapsayan bir örnekleme yapmaya çalıştım. Kolayınıza giden hangi yöntemse o şekilde kullanabilirsiniz.

Diğer iki modelimiz için yukarıda anlattıklarımız aynen geçerli. Sadece bu iki modelin bir birleriyle olan relations/ilişki tanımları kısmı farklı. Bu iki modelimiz sırasıyla ;

models/Product.js

module.exports = (sequelize, DataType) => {
    const Products = sequelize.define("Products", {
            id: {
                type: DataType.INTEGER,
                primaryKey: true,
                autoIncrement: true
            },
            name: {
                type: DataType.STRING,
                allowNull: false,
                validate: {
                    notEmpty: true
                }
            },
            price: {
                type: DataType.DECIMAL(10, 2),
                defaultValue: 0.00,
                min: 0.00,
                max: 99999.00
            },
            salesPrice: {
                type: DataType.DECIMAL(10, 2),
                defaultValue: 0.00,
                min: 0.00,
                max: 99999.00
            },
            stockLevel: {
                type: DataType.INTEGER,
                defaultValue: 20
            }
        },
        {
            //scopes
            defaultScope: {
                attributes: ["id", "name", "price", ["stockLevel", "Stock Alarm Level"], ["salesPrice", "Sales price"]]
            },
            //scopes end

            paranoid: true,
            hooks: {},
            classMethods: {
                associate: (models) => {
                    Products.hasOne(models.ProductsStocks)
                }
            }
        }
    );
    return Products;
};

models/ProductStock.js

module.exports = (sequelize, DataType) => {
    const ProductsStocks = sequelize.define("ProductsStocks", {


            id: {
                type: DataType.INTEGER,
                primaryKey: true,
                autoIncrement: true
            },
            stock: {
                type: DataType.INTEGER,
                allowNull: false,
                validate: {
                    notEmpty: true
                },
                min: 0,
                max: 99999
            }


        }, {
            indexes: [
                // Create a unique index on email
                {
                    unique: true,
                    fields: ['product_id']
                }],

            defaultScope: {
                attributes: ['id', 'stock'],
                include: [
                    {
                        all: true,
                        attributes: ['name'],
                        as: 'Product'
                    }
                ]
                ,
                where: {
                    stock: {
                        $gt: 0
                    }
                }
            },
            paranoid: true,
            hooks: {},
            classMethods: {
                associate: (models) => {
                    ProductsStocks.belongsTo(models.Products)
                }
            },
            logging: process.env.NODE_ENV == 'test' ? false : true
        }
    );
    return ProductsStocks;
};

Yukaridaki iki modelimizde konuşmamız gereken kısım “associate() ” metodu içinde tanımladığımız relations/ilişkiler.

Product

associate: (models) => {
                    Products.hasOne(models.ProductsStocks)
  }

ProductStock

 associate: (models) => {
      ProductsStocks.belongsTo(models.Products)
    }

Gördüğümüz gibi, basit bir hasOne – belongsTo (bire bir) ilişkisi oluşturduk. Relations tanımları için Sequelize ın buradaki ilgili sayfasına bakabilirsiniz. Yukarıdaki model tanımlamalarız içinde gözden kaçırdığım bir noktayı hatırlatmakta fayda var. Tüm modellerimizde eklediğimiz ;

paranoid: true

tanımı. Bu tanım ile Sequelize e , silme işlemlerinde “paranoid” olmasını yani kayıtları gerçekten silmek yerine “silindi” olarak işaretlemisini söylemiş olduk. Kısaca, bir kaydı sildiğimizde o kayıt db den gerçekten silinmek yerine, sadece “silindi” olarak işaretlenecek. Model tanımlamamız şimdilik bu kadar. Yukarıda modellerimiz içinde kullandığımız iki değişken gözünüze çarpmış olmalı;

sequelize ve DataType” bu iki değişkeni nerden nasıl aldığımıza bakalım. modellerimizi tanımladığımız block hatırlasanız şu şekilde başlıyordu ;

module.exports = (sequelize, DataType) => {...}

bu iki değişkeni , modellerimize sağlamak için, bir sonraki sınıfımızı kullanacağız. db.js isimli bu sınıf, veri tabanı bağlantısını yönetmek ve Sequelize için genel ayarları yapmak/tutmak için tanımlayacağımız yer. Böylece, eğer olurda DB değişikliği(postgres den mysql e geçmek gibi) yada modellerimizi tek bir yerden yönetmek için kullanabileceğimiz ayrı bir “ara” nokta olacak.

Bu sinif, zorunlu olmakdan ziyade kullanım kolaylığı ve bir yönüyle sequelize tarafından tavsiye edilen “best practice” olduğu için bu şekilde bir kullanım yapacağız. Karmaşık gözükmesine aldırmayın, Sequelize ın örnekler kısmında ve webde hemen hemen hep aynı örneği göreceksiniz.

libs/db.js

import fs from "fs";
import path from "path";
import Sequelize from "sequelize";

let db = null;

module.exports = app => {
    if (!db) {
        const config = app.libs ? app.libs.config : app;
        const sequelize = new Sequelize(
            config.database,
            config.username,
            config.password,
            config.params
        );
        db = {
            sequelize,
            Sequelize,
            models: {}
        };
        const dir = path.join(__dirname, "../models");
        fs.readdirSync(dir).forEach(file => {
            console.log("db models file => ", file)
            const modelDir = path.join(dir, file);
            const model = sequelize.import(modelDir);
            db.models[model.name] = model;
        });
        Object.keys(db.models).forEach(key => {
            db.models[key].associate(db.models);
        });
    }
    return db;
};

Yukarıda 3 temel işlem gerçekleştirdik;

  1. Konfigürasyon dosyamızdaki ayarları kullanarak, db bağlantımızı açıp yönetmek
  2. Model klasöründeki tüm modelleri Sequelize a kayıt ettirmek
  3. Ve son olarak, bu modelleri uygulamızın tamamında kullanılabilir hale getirdik.

Küçük uygulamalar için biraz karışık gibi gözüksede, proje büyüdükçe bu yapının size ciddi esneklik ve kolaylıklar sağladığını göreceksiniz. DB işlemlerimiz için son olarak yine “best practice” olarak sync işlemini yapıp işlemimizi tamamlayacağız.

sequelize.sync()

sync metodu ile, sequelize veri tabanı ile modellerimizin bire bir uyuştuğundan emin olacağız. Ayrıca eğer model klasöründeki model in henüz db de karşılığı(ilgili tablo) yoksa onuda oluşturacak sync metodu.  Sync metodu DB katmanındaki son nokta olduğu için, uygulamız içinde sunucumuzu başlatacağımız noktada bu işlemi yapmamız gerekmekte. Böylece DB modellerimiz, bağlantımız vs. herşey hazır ise uygulamızı başlatabiliriz.

Bu işlemi, tahmin ettiğiniz gibi, boot.js içinde yapacağız. ;

libs/boot.js

module.exports = app => {
    const config = app.libs.config;
    app.libs.db.sequelize.sync().done(() => {
        app.listen(config.port, () => {
            console.log(`Uygulamamiz ${config.port} nolu port uzerinde calismakta`)
            console.log(`Uygulama Calisma Modu : ${app.get('env')}`)
            
        })
        
    });
};

sync metodu başarılı bir şekilde çalıştıktan sonra, sunucumuzu başlatıyoruz. Db işlemlerimizide bu şekilde düzene koymuş olduk. Bundan sonra oluşturacağımız modeller(models klasörü içinde) otomatik olarak veri tabanımızla senkronize edilip, uygulamamızda kullanılabilir olacaklar. Test etmek için, terminalden ,

npm start

dediğinizde aşağıdakine benzer bir görüntü elde etmeniz lazım.

dbsetuop

Bir sonraki yazıda inş. routing yapımızı ve controllerimizi yazarak devam edelim.

Uygulamanın buraya kadar olan hali için bu repoyu temin edebilirsiniz.

 

2 thoughts on “Node.js ile Uygulama Geliştirme – 6 : REST Mimarisi ve RESTful Servisler Geliştirme [Sequelize, SQL ve veri modellerimiz]

  1. nam start komutundan sonra hata alıyorum. “TypeError: Cannot read property ‘sequelize’ of undefined”
    npm install ile sqeuelize’ı indirdim. Dosyalar birebir sizinkilerle aynı fakat sorunu bir türlü çözemiyorum. Github daki proje dosyaları üzerindende npm install ve start işlemi yaptım fakat sonuç aynı. Sroun ne olabilir?
    Şu adımda hata veryor: boot.js dosyası içerisnde ” app.libs.db.sequelize.sync().done(function () {” satırında sequelize’ı bulamıyor sanırım.

  2. Merhaba, ben repoyu indirim, npm install ve npm start ile sorunsuz calistirdim. Sanirim baska bir sorun var, npm start komutundan sonraki konsol ciktisini tamamen kopyalaip yapistirabilirseniz bir de ona bakabiliriz.

    Bendeki cikti :
    + .libsconfig.js
    + .libsdb.js
    + .middlewaresapp.middlewares.js
    ! Entity not found C:Usershuseyin1
    + .error.js
    + .libsboot.js
    Config Env => development
    db models file => Product.js
    db models file => ProductStock.js
    db models file => User.js
    Uygulamamiz 3000 nolu port uzerinde
    Uygulama Calisma Modu : development

    bir de ,
    npm install -g sequelize

    seklinde `sequelize` i global olarak yukleyip sonra `libs/config-dev.js` i kendi sisteminize gore modifiye ettiginizden emin olup tekrar `npm start` ile calistirmayi deneyin,

    windows yada mac hangisini kullanyorsaniz onuda belirtebilirsiniz.

Leave a Reply

Your email address will not be published. Required fields are marked *