Introducere

Ca dezvoltator de software la un moment dat, este posibil să trebuiască să faceți față migrărilor bazelor de date într-un fel sau altul.

Pe măsură ce software-ul sau aplicațiile evoluează și se îmbunătățesc în timp, baza de date trebuie să fie, de asemenea. Și trebuie să ne asigurăm că datele rămân consecvente pe întreaga aplicație.

Există o serie de moduri diferite în care o schemă se poate schimba de la o versiune a aplicației dvs. la următoarea.

  • Se adaugă un nou membru
  • Un membru este eliminat
  • Un membru este redenumit
  • Tipul unui membru este modificat
  • Reprezentarea unui membru este modificată

Deci, cum gestionați toate modificările de mai sus?

prin GIPHY

Există două strategii:

  • Scrieți un script care se va ocupa de actualizarea schemei, precum și de descreșterea acesteia la versiunile anterioare
  • Actualizați documentele pe măsură ce sunt utilizate

Al doilea este mult mai dependent de cod și trebuie să rămână în baza de cod. Dacă codul este într-un fel eliminat, atunci multe dintre documente nu pot fi actualizate.

De exemplu, dacă au existat 3 versiuni ale unui document, [1, 2, and 3] și eliminăm codul de actualizare de la versiunea 1 la versiunea 2, orice documente care există încă ca versiunea 1 nu pot fi actualizate. Eu personal văd acest lucru ca o suprasolicitare a menținerii codului și devine inflexibil.

Deoarece acest articol este despre automatizarea migrațiilor, vă voi arăta cum puteți scrie un script simplu care să aibă grijă de schimbările de schemă, precum și de testele unitare.

A fost adăugat un membru

Când un membru a fost adăugat la schemă, documentul existent nu va avea informațiile. Deci, trebuie să interogați toate documentele în care acest membru nu există și să le actualizați.

Să continuăm cu scrierea unui cod.

Există deja câteva module npm disponibile, dar am folosit biblioteca nod-migrare. Și eu am încercat pe alții, dar unii dintre ei nu mai sunt bine întreținuți și m-am confruntat cu probleme de pregătire cu alții.

Condiții prealabile

  • nod-migrare – Cadrul de migrare abstract pentru Node
  • mongodb – un driver nativ al MongoDB pentru Nodejs
  • Mocha – Cadrul de testare
  • Chai – Biblioteca de afirmații pentru scrierea cazurilor de testare
  • Pasare albastra: Promiteți biblioteca pentru gestionarea apelurilor API asincronizate
  • mkdirp: Ca. mkdir -p dar în Node.js
  • rimraf: rm -rf pentru Nod

Starea migrației

O stare de migrare este cea mai importantă cheie pentru urmărirea migrației dvs. curente. Fără aceasta, nu vom putea urmări:

  • Câte migrații s-au făcut
  • Care a fost ultima migrație
  • Care este versiunea actuală a schemei pe care o folosim

Și fără stări, nu există nicio modalitate de a reveni, actualiza și viceversa la o stare diferită.

Crearea migrațiilor

Pentru a crea o migrare, executați migrate create <title> cu un titlu.

În mod implicit, un fișier în ./migrations/ va fi creat cu următorul conținut:

'use strict'

module.exports.up = function (next) {
  next()
}

module.exports.down = function (next) {
  next()
}

Să luăm un exemplu de User schemă în care avem o proprietate name care include ambele first și last Nume.

Acum vrem să schimbăm schema pentru a avea o separare last nume proprietate.

Deci, pentru a automatiza acest lucru, vom citi name în timpul rulării și extrageți numele de familie și salvați-l ca proprietate nouă.

Creați o migrare cu această comandă:

$ migrate create add-last-name.js

Acest apel se va crea ./migrations/{timestamp in milliseconds}-add-last-name.js sub migrations din directorul rădăcină.

Să scriem cod pentru a adăuga un nume de familie în schemă și, de asemenea, pentru a-l elimina.

Sus Migrare

Vom găsi toți utilizatorii unde lastName proprietatea nu există și creați o proprietate nouă lastName în acele documente.

'use strict'
const Bluebird = require('bluebird')
const mongodb = require('mongodb')
const MongoClient = mongodb.MongoClient
const url="mongodb://localhost/Sample"
Bluebird.promisifyAll(MongoClient)

module.exports.up = next => {
  let mClient = null
  return MongoClient.connect(url)
  .then(client => {
    mClient = client
    return client.db();
  })
  .then(db => {
    const User = db.collection('users')
    return User
      .find({ lastName: { $exists: false }})
      .forEach(result => {
        if (!result) return next('All docs have lastName')
        if (result.name) {
           const { name } = result
           result.lastName = name.split(' ')[1]
           result.firstName = name.split(' ')[0]
        }
        return db.collection('users').save(result)
     })
  })
  .then(() => {
    
    mClient.close()
    return next()
  })
   .catch(err => next(err))
}

Migrația în jos

În mod similar, să scriem o funcție pe care o vom elimina lastName:

module.exports.down = next => {
let mClient = null
return MongoClient
   .connect(url)  
   .then(client => {
    mClient = client
    return client.db()
  })
  .then(db =>
    db.collection('users').update(
    {
       lastName: { $exists: true }
    },
    {
      $unset: { lastName: "" },
    },
     { multi: true }
  ))
  .then(() => {
    mClient.close()
    return next()
  })
  .catch(err => next(err))

}

Rularea migrațiilor

Vedeți cum sunt executate migrațiile aici: rularea migrațiilor.

Scrierea stocării personalizate a stării

În mod implicit, migrate stochează starea migrațiilor care au fost executate într-un fișier (.migrate).

.migrate fișierul va conține următorul cod:

{
  "lastRun": "{timestamp in milliseconds}-add-last-name.js",
  "migrations": [
    {
      "title": "{timestamp in milliseconds}-add-last-name.js",
      "timestamp": {timestamp in milliseconds}
    }
  ]
}

Dar puteți oferi un motor de stocare personalizat dacă doriți să faceți ceva diferit, cum ar fi stocarea lor în baza de date la alegere.

Un motor de stocare are o interfață simplă de load(fn) și save(set, fn).

Atâta timp cât ceea ce intră ca set iese la fel pe load, atunci ești bine să pleci!

Să creăm fișier db-migrate-store.js în directorul rădăcină al proiectului.

const mongodb = require('mongodb')
const MongoClient = mongodb.MongoClient
const Bluebird = require('bluebird')

Bluebird.promisifyAll(MongoClient)
class dbStore {
   constructor () {
     this.url="mongodb://localhost/Sample" . // Manage this accordingly to your environment
    this.db = null
    this.mClient = null
   }
   connect() {
     return MongoClient.connect(this.url)
      .then(client => {
        this.mClient = client
        return client.db()
      })
   }
    load(fn) {
      return this.connect()
      .then(db => db.collection('migrations').find().toArray())
      .then(data => {
        if (!data.length) return fn(null, {})
        const store = data[0]
        // Check if does not have required properties
          if (!Object
               .prototype
               .hasOwnProperty
               .call(store, 'lastRun') 
                ||
              !Object
              .prototype
              .hasOwnProperty
             .call(store, 'migrations'))
            {
            return fn(new Error('Invalid store file'))
            }
        return fn(null, store)
      }).catch(fn)
    }
   save(set, fn) {
     return this.connect()
      .then(db => db.collection('migrations')
      .update({},
       {
         $set: {
           lastRun: set.lastRun,
         },
         $push: {
            migrations: { $each: set.migrations },
         },
      },
      {
         upsert: true,
         multi: true,
       }
      ))
       .then(result => fn(null, result))
       .catch(fn)
   }
}

module.exports = dbStore

load(fn) În această funcție verificăm doar dacă documentul de migrare existent care a fost încărcat conține lastRun proprietate și migrations matrice.

save(set,fn) Aici set este furnizat de bibliotecă și actualizăm fișierul lastRun valoare și anexare migrations la matricea existentă.

S-ar putea să vă întrebați unde este fișierul de mai sus db-migrate-store.js este folosit. Îl creăm pentru că vrem să stocăm starea în baza de date, nu în depozitul de coduri.

Mai jos sunt exemple de test în care puteți vedea utilizarea acestuia.

Automatizați testarea migrației

Instalați Mocha:

$ npm install -g mocha

Am instalat acest lucru la nivel global, astfel încât vom putea rula mocha de la terminal.

Structura

Pentru a configura testele de bază, creați un folder nou numit „test” în rădăcina proiectului, apoi în acel folder adăugați un folder numit migrații.

Structura fișierului / folderului ar trebui să arate acum astfel:

├── package.json
├── app
│   ├── server.js
│   ├── models
│   │   └── user.js
│   └── routes
│       └── user.js
└── test
       migrations
        └── create-test.js
        └── up-test.js 
        └── down-test.js

Test – Creați migrarea

Poartă: Ar trebui să creeze directorul și fișierul de migrații.

$ migrate create add-last-name

Aceasta va crea implicit fișierul ./migrations/{timestamp in milliseconds}-add-last-name.js sub migrations din directorul rădăcină.

Acum adăugați următorul cod la create-test.js fişier:

const Bluebird = require('bluebird')
const { spawn } = require('child_process')
const mkdirp = require('mkdirp')
const rimraf = require('rimraf')
const path = require('path')
const fs = Bluebird.promisifyAll(require('fs'))

describe('[Migrations]', () => {
    const run = (cmd, args = []) => {
    const process = spawn(cmd, args)
    let out = ""
    return new Bluebird((resolve, reject) => {
       process.stdout.on('data', data => {
         out += data.toString('utf8')
       })
      process.stderr.on('data', data => {
        out += data.toString('utf8')
      })
      process.on('error', err => {
         reject(err)
      })
     process.on('close', code => {
      resolve(out, code)
     })
   })
 }
    
const TMP_DIR = path.join(__dirname, '..', '..', 'tmp')
const INIT = path.join(__dirname, '..', '..', 'node_modules/migrate/bin', 'migrate-init')
const init = run.bind(null, INIT)
const reset = () => {
   rimraf.sync(TMP_DIR)
   rimraf.sync(path.join(__dirname, '..', '..', '.migrate'))
}

beforeEach(reset)
afterEach(reset)
describe('init', () => {
   beforeEach(mkdirp.bind(mkdirp, TMP_DIR))

   it('should create a migrations directory', done => {
      init()
      .then(() => fs.accessSync(path.join(TMP_DIR, '..', 'migrations')))
      .then(() => done())
      .catch(done)
   })
 })
})

În testul de mai sus, folosim migrate-init comanda pentru a crea directorul de migrații și ștergerea acestuia după fiecare caz de testare folosind rimraf care este rm -rf în Unix.

Mai târziu îl folosim fs.accessSync funcție de verificat migrations folderul există sau nu.

Test – Up Migration

Poartă: Ar trebui să adauge lastName pentru a schema și a stoca starea de migrare.

Adăugați următorul cod la up-test.js fişier:

const chance = require('chance')()
const generateUser = () => ({
   email: chance.email(),
   name: `${chance.first()} ${chance.last()}`
 })
const migratePath = path.join(__dirname, '..', '..', 'node_modules/migrate/bin', 'migrate')
const migrate = run.bind(null, migratePath)

describe('[Migration: up]', () => {
   before(done => {
     MongoClient
     .connect(url)
     .then(client => {
       db = client.db()
      return db.collection('users').insert(generateUser())
      })
      .then(result => {
       if (!result) throw new Error('Failed to insert')
       return done()
      }).catch(done)
   })
   it('should run up on specified migration', done => {
     migrate(['up', 'mention here the file name we created above', '--store=./db-migrate-store.js'])
    .then(() => {
       const promises = []
       promises.push(
        db.collection('users').find().toArray()
       )
     Bluebird.all(promises)
    .then(([users]) => {
       users.forEach(elem => {
         expect(elem).to.have.property('lastName')
      })
      done()
    })
   }).catch(done)
 })
after(done => {
    rimraf.sync(path.join(__dirname, '..', '..', '.migrate'))
    db.collection('users').deleteMany()
    .then(() => {
      rimraf.sync(path.join(__dirname, '..', '..', '.migrate'))
      return done()
   }).catch(done)
 })
})

În mod similar, puteți nota migrarea și before() și after() funcțiile rămân practic aceleași.

Concluzie

Sperăm că acum puteți automatiza modificările schemei dvs. cu teste adecvate. 🙂

Luați codul final de la repertoriu.

Nu ezitați să bateți din palme dacă ați considerat că este o lectură utilă!