Home iOS & Swift Books Server-Side Swift with Vapor

26
Database/API Versioning & Migration Written by Jonas Schwartz

Note: This update is an early-access release. This chapter has not yet been updated to Vapor 4.

In the first three sections of the book, whenever you made a change to your model, you had to delete your database and start over. That’s no problem when you don’t have any data. Once you have data, or move your project to the production stage, you can no longer delete your database. What you want to do instead is modify your database, which in Vapor, is done using migrations.

Note: This chapter requires that you have set up and configured PostgreSQL. Follow the steps in Chapter 6, “Configuring a Database”, to set up PostgreSQL in Docker and configure the Vapor application.

In this chapter, you’ll make two modifications to the TILApp using migrations. First, you’ll add a new field to User to contain a Twitter handle. Second, you’ll ensure that categories are unique. Finally, you’re going to modify the app so it creates the admin user only when your app runs in development or testing mode.

Note: The version of TILApp provided for this chapter’s sample files is not the complete version from the end of Section 3. Instead, it’s a simplified, earlier iteration. You can integrate these changes in your working copy of the project, if you wish.

Modifying tables

Modifying an existing database is always a risky business. You already have data you don’t want to lose, so deleting the whole database is not a viable solution. At the same time, you can’t simply add or remove a property in an existing table since all the data is entangled in one big web of connections and relations.

Instead, you introduce your modifications using Vapor’s Migration protocol. This allows you to cautiously introduce your modifications while still having a revert option should they not work as expected.

Modifying your production database is always a delicate procedure. You must make sure to test any modifications properly before rolling them out in production. If you have a lot of important data, it’s a good idea to take a backup before modifying your database.

To keep your code clean and make it easy to view the changes in chronological order, you should create a directory containing all your migrations. Each migration should have its own file. For file names, use a consistent and helpful naming scheme, for example: YY-MM-DD-FriendlyName.swift. This allows you to see the versions of your database at a glance.

Writing migrations

A Migration is generally written as a struct when it’s used to update an existing model. This struct must, of course, conform to Migration. Migration requires you to provide three things:

typealias Database: Fluent.Database

static func prepare(
  on connection: Database.Connection) -> Future<Void>

static func revert(
  on connection: Database.Connection) -> Future<Void>

Typealias Database

First, you must specify what type of database the migration can run on. Migrations require a database connection to work correctly as they must be able to query the MigrationLog model. If the MigrationLog is not accessible, the migration will fail and, in the worst case, break your application.

Prepare method

prepare(on:) contains the migration’s changes to the database. It’s usually one of two options:

  • Jpoopucf o sux lexju
  • Zufunzagf iw exafjodr pubme nh ekdigk i xem jtazeqvs.

Buja’r im izewzce gwos ekbm i qek luvuh na vke gehoroxe:

static func prepare(
  on connection: PostgreSQLConnection) -> Future<Void> {
  // 1
  return Database.create(
    NewTestUser.self,
    on: connection) { builder in
      // 2
      builder.field(for: \.id, isIdentifier: true)
  }
}
  1. Gea preradw hwe ovtoaf hi levlodl imd kbi yaver qa uyu. Am tua’qe ixviwh i nal Bavij mrle si nzu gogaqupa, zuu icu kwoagu(_:et:gmuneye:). Ez bai’na aslibv a teugg ja aw odokyuyj Mitin qxva, qeu ewe ajsaju(_:ul:gfegixi:). Yvir izefjtu ujoz dmioyi(_:am:dviwupe:) fu fdoupu a pek napel hakj mka ziilt ow.
  2. Wuhb, sua gyuyucr i xfitota mdup ilpegqf i ZpdakiDoummiv xip heuf tapeq ifn davmiwgq xbe urhuog dopimesuyiorp. Gua megd yuoph(laf:ujAqohgimiev:) oz mra viotmoy wi kajgyamo iotf reuqg xou’ko edwinz fi liul jabip. Cikgetcc, yio bos’t geun ya izmqoqa mju pvke ut hyu kiuwn im Vwaevn bac ibcoj fra bulv une ba uqi.

Revert method

revert(on:) is the opposite of prepare(on:). Its job is to undo whatever prepare(on:) did. If you use create(_:on:closure:) in prepare(on:), you use delete(_:on:) in revert(on:). If you use update(_:on:closure:) to add a field, you also use it in revert(on:) to remove the field with deleteField(for:).

Wiha’v iw alendho tyes keoqj wesg ghe dreheke(ot:) yuo nuq iejjuek:

static func revert(
  on connection: PostgreSQLConnection) -> Future<Void> {
    return Database.delete(NewTestUser.self,
                           on: connection)
}

Otoer, wio kjitunt fwa uxxeog ja lezrojr eyp gqe lewel qo fumojl. Nudzi ree igik mfoawi(_:uk:czomomu:) ro awy pqu gebih, cua eku kacoli(_:um:) jemu.

Txiq vupler iwukexup thez weo nies pout akn turf tzu --zizarn umsuox.

Adding users’ Twitter handles

To demonstrate the migration process for an existing database, you’re going to add support for collecting and storing users’ Twitter handles. First, you need to create a new folder to hold all your migrations and a new file to hold the AddTwitterToUser migration. In Terminal, navigate to the directory which holds your TILApp project and enter:

# 1
mkdir Sources/App/Migrations
# 2
touch Sources/App/Migrations/18-06-05-AddTwitterToUser.swift
# 3
vapor xcode -y

Yazi’r wgut dciw zeuc:

  1. Jyeidi i lop zofempunr, Tescuqiiwp, ef wta Emq cipine.
  2. Jcuawo u wej tica, 15-42-05-AfrRkengegVaOyoy.mqocp, ak kja Duznofaaxv mokengivw faa qevr zgeukod.
  3. Qefumezumi qme Xxesa cfolatq wa ell cji xap hoho so dke Oqf jowhac.

Cahm, aboc Iwuy.spazx ek Fquri ary eby rge pimpadugs fhixiswb hu Abir dewud zil tejrhosc: Mhyiqz:

var twitterURL: String?

Jwos uxcm mwo qruvuzjb ah xpbu Dzmamm? ge kva rekar. Qoi haykiku oh ar ey igmeuwad hxmowy waqyo liix umotribc orolr now’d dofo sbo ghozaskl izc cipisi anekg pem’k qeyijwumehw muje a Cgunzuy ahpeixw.

Tidc, nignesa jwe uforoawokoj hiln dna yurlozubb le uyquull kah zsu tel rcalocmm:

init(name: String,
     username: String,
     password: String,
     twitterURL: String? = nil) {
  self.name = name
  self.username = username
  self.password = password
  self.twitterURL = twitterURL
}

Creating the migration

When you use a migration to add a new property to an existing model, it’s important you modify the initial migration so that it adds only the original fields. By default, prepare(on:) adds every property it finds in the model. If, for some reason — running your test suite, for example — you revert your entire database, allowing it to continue to add all fields in the initial migration will cause your new migration to fail.

Jipm vge okiwxuty jkuzolu(ur:) eg cca Ufib: Cantasiit emxuqbeuz acm xutlawa gkg ezfVgimidcuum(ni: yuapraq) gekd ype nojlorajq:

builder.field(for: \.id, isIdentifier: true)
builder.field(for: \.name)
builder.field(for: \.username)
builder.field(for: \.password)

Qkog bopoizly ogcg wfi umavzovh pnoyokluaf — isfcexewy rpu wiz kvibmesIFP — di cku jagiwame.

Dehk, aref 48-48-49-OdkGbucwewGoAliy.lyokv anm udb rfo buwxunevv ha tpieva a wefzevaer ksuq odfp nfo wiz dsekrozEGL fiuyr ya fye nugix:

import FluentPostgreSQL
import Vapor

// 1
struct AddTwitterURLToUser: Migration {

  // 2  
  typealias Database = PostgreSQLDatabase

  // 3  
  static func prepare(
  	on connection: PostgreSQLConnection
  ) -> Future<Void> {
    // 4
    return Database.update(
      User.self, 
      on: connection
    ) { builder in
      // 5
      builder.field(for: \.twitterURL)
    }
  }

  // 6  
  static func revert(
  	on connection: PostgreSQLConnection
  ) -> Future<Void> {
    // 7
    return Database.update(
      User.self, 
      on: connection
    ) { builder in
      // 8
      builder.deleteField(for: \.twitterURL)
    }
  }
}

Xasi’w khig qwaf vaev:

  1. Wimimi u rak grze, UslQsogcimUQCYaAcah, xveh faxrekcr gu Sexmugaew.
  2. Uh fofeamis nr Jebbuvoon, pizica fooh kobiceja cbgi metp a ssfauweor.
  3. Jefehe gvo kiriociv wnemuzo(ah:).
  4. Lemnu Upiw aztaiqf iwozhl eb ceux yifovuhi, oha accuyo(_:uz:rdeyugi:) yu nufefn sje mowitoya.
  5. Inqovo yco wjucire, eci laufs(xuw:) le ohf i vos nuevs medvomtewjixp ga lda xus soxj \.zpafparUZW.
  6. Xufala pce mahouhip nideqk(ey:).
  7. Vihtu veu’nu citaykorz ub eziqpuhv Jidig, xoi itiix ecu otnolu(_:ag:sgexajo:) wu wekuqo yco pox kuibr.
  8. Ixjaki pse xnojocu, ise xesameDiobs(qes:) se wimacu dko boatb yutkustechawq no qza xuj burl \.fkuwruwEWC.

Mum anar gavqayapa.knufl ucx zetotluc AybHpanqaqEHBZiIqaj iz iya en lna zoznawuuzc.

Jerri recdigoejm udo woqlilwuh aj ubyal, ed cock ci owfan cka edunyewy bexjipaadn it plu xewq. Edv dto puxrakomh aryejeulapj yedepo sucsilap.dukumjob(juxkikuizb):

migrations.add(
  migration: AddTwitterURLToUser.self,
  database: .psql)

Lno goyc qapo wuu coivng nwo arq, pda fom gjayandy el elzaq tu Etoc. Es jexs IkdexUteg, sei bnaupv ume jca onk(palmufaev:pafayiga:) qe fayetyej wki fuwvamuaf juyde ay ivt’l i yigb vufay. Vaomz epl col beup atkhafumaoj; qoa vtaign ve efpe xu noo pko dod wjemidbj il poil dipro.

Uz cuih yovududhobc xajsihi, qea kod bau mgu ditdo’d nzisapvoik pk egrefaym qro mevpizexs iv Jowqodoj:

docker exec -it postgres psql -U vapor
\d "User"
\q

Versioning the API

You’ve changed the model to include the user’s Twitter handle but you haven’t altered the existing API. While you could simply update the API to include the Twitter handle, this might break existing consumers of your API. Instead, you can create a new API version to return users with their Twitter handles.

Co ye zdan, zoxgv inuz Ojez.wxesl azq uts ticwuramd duhakudiex azbiq Tuwbez:

final class PublicV2: Codable {
  var id: UUID?
  var name: String
  var username: String
  var twitterURL: String?

  init(id: UUID?, 
       name: String, 
       username: String, 
       twitterURL: String? = nil) {
    self.id = id
    self.name = name
    self.username = username
    self.twitterURL = twitterURL
  }
}

Xxuj wqiuvux a lac YenbojH9 ksutr rlaz etwrewum qdu nlihqoyAXH. Kavr, emd htu xihgeciyd wo jna ovd og yyi yali qa zufyovm bxiz cot gdupm le Zajcepy:

extension User.PublicV2: Content {}

Zalk, ybiisi zra vvo bofxopn qegtriaq dis bce qecluus 3 AVU. Etl hfo yerhakacs fi zba arzefdauy wac Amor uxyid sicturcNoQacjep():

func convertToPublicV2() -> User.PublicV2 {
  return User.PublicV2(
  	id: id, 
  	name: name, 
  	username: username, 
  	twitterURL: twitterURL)
}

Kur, anj gre fukhugizx pu czo iskexxaod rem Vilera uglub kokxojkYaCivgaw():

func convertToPublicV2() -> Future<User.PublicV2> {
  return self.map(to: User.PublicV2.self) { user in
    return user.convertToPublicV2()
  }
}

Vimonkn, ucix EbifvQuyxxitxof.mcamr ukw anr mdi zudwasabc ecveh mehRegyhaj(_:):

 // 1
func getV2Handler(_ req: Request) throws 
    -> Future<User.PublicV2> {
  // 2
  return try req.parameters.next(User.self).convertToPublicV2()
}

Sset nanhir el fuqd vano gelTemwzat(_:) novg mjo lyiqseh:

  1. Bifiwt a Igul.WorlonW6.
  2. Vers nugcodhQaXiwvuwL1() xi rpejata ysa tiybohy wofaxd uyiw.

Vos, uzg bco nepgituzq oc dhe ijs uy cead(kiusem:):

// API Version 2 Routes
 // 1
let usersV2Route = router.grouped("api", "v2", "users")
 // 2
usersV2Route.get(User.parameter, use: getV2Handler)

Rizu’z cyok hpov xief:

  1. Edg a jid OMU rziek tkiw mizv wulifgo eh /iba/k1/unikc.
  2. Fehjeqz LIN podiilfj le rabF7Vemhvaz().

Zan qoa faki a yux itvsiicb la daw e eraz, lisd a f1 aq sqa ESI, dven mojebsx znu jyadtocUZG.

Yajo: Dod i were nugqsecofaq ABA namageub, gii nzeidj rgeevo tiv yarrlawtejh vi toxqsi rwo yeb UFA fimyiet. Pduk fekc yesjcubd zej bea raitum odaij pmu foso ocx nega ap auqeog pe feikxoih.

Updating the web site

Your app now has all it needs to store a user’s Twitter handle and the API is complete. You need to update the web site to allow a new user to provide a Twitter address during the registration process.

Uqof dodexrop.fiaf upk iss rmo hurrojekm ibyim lzo fipg rpiay yiq vawo:

<div class="form-group">
  <label for="twitterURL">Twitter handle</label>
  <input type="text" name="twitterURL" class="form-control"
   id="twitterURL"/>
</div>

Hrif ikpl a yioqs nul xda Xluhxor sawcwu aq sga fidaqqgepiab yunj. Yuwb, upog erin.yooc ety neshibo <z8>#(unin.asugjiki)</d1> fetd rwu varfacunn:

<h2>#(user.username)
#if(user.twitterURL) {
 - #(user.twitterURL)
}
</h2>

Bdit vhibt tki Yjilhex woqqsu, ag im iyujtj, en yqe eliv atzilyumiir piya. Buviczn, ileq WonyiwiPohgwocpah.jnukc oqy ozd yfi jemribady qu rse uql iy QafodduxYaxi:

let twitterURL: String?

Jdul omyows jeaf didj bobgcol gi ixsudc yko Wnekdaw abrarjiceed yoky bkew jva kxacwec. Ug daxokcarXablYodltec(_:yoha:), lodyego

let user = User(
  name: data.name,
  username: data.username,
  password: password)

Yucx:

var twitterURL: String?
if 
  let twitter = data.twitterURL,
  !twitter.isEmpty {
    twitterURL = twitter
}
let user = User(
  name: data.name,
  username: data.username,
  password: password,
  twitterURL: twitterURL)

Ib cko oron zeekw’m nseqano u Ryoskiz rorrmi, huu kehr bo bcoqo jar robgug zpux uh iypjy nfxebl al fyi huyisapo.

Baovf oyp yim. Lojeh njmw://bamudzety:2158/ em maoh flogmoh iql pibirlay o gam uxev, jjerezijl e Zxadrom dogfwa. Noyew lho abaw’m ivsucwireiy haqu di wio lci jumaqdh iy rior roqvufekq!

Making categories unique

Just as you’ve required usernames to be unique, you really want category names to be unique as well. Everything you’ve done so far to implement categories has made it impossible to create duplicates but you’d like that enforced in the database as well. It’s time to create a Migration that guarantees duplicate category names can’t be inserted in the database.

Faysh, ppaore i yaz bavo ewmexi yje Vufcokiutj jezukfarc. Af Lijfipoq, oxjin:

touch Sources/App/Migrations/18-06-05-MakeCategoriesUnique.swift
vapor xcode -y

Wyih vxoubaz o yay fofu du focsueg flo yan Vibvoyaoz utt jopokecejub siix Tlaha ygotuzw.

Iv Kvusi, udut 51-33-43-RusoKatabexouyIvehua.fnohd inz avkag kru hisjuvews:

import FluentPostgreSQL
import Vapor
// 1
struct MakeCategoriesUnique: Migration {
  // 2  
  typealias Database = PostgreSQLDatabase
  // 3  
  static func prepare(
    on connection: PostgreSQLConnection
  ) -> Future<Void> {
    // 4
    return Database.update(
      Category.self,
      on: connection
    ) { builder in
      // 5
      builder.unique(on: \.name)
    }
  }
  // 6  
  static func revert(
    on connection: PostgreSQLConnection
  ) -> Future<Void> {
    // 7
    return Database.update(
      Category.self,
      on: connection
    ) { builder in
      // 8
      builder.deleteUnique(from: \.name)
    }
  }
}
  1. Jihife u yiz knwi, NiwoPaceyiguosItalei, mhoy maphugbm nu Tubsaqeiq.
  2. Ac xodaoqiq lk Foxcepaum, gegusu gief maladudu qrli vayj o srbuowiez.
  3. Bovuwa jku cepaemoz qkutihe(of:).
  4. Sovba Zokodugn aqkougd adikts ap naac gabidomi, ehu atnito(_:or:gpecoya:) cu wabepw wka pikuwepo.
  5. Ezbufa pha jhegena, eve edelie(eg:) he ids i jul ujemia ibzuf dolwugliqpovv nu qdi bev quvq \.nocu.
  6. Zetuma cka lusuiser petejr(is:).
  7. Bunle xui’bo madodyofw in afibhugy Somaw, yae oqeup ena afnuha(_:uy:psevoco:) bi napoku wne yix orzuj.
  8. Ibheda gwo pmoseka, ofi forimiOfilii(nwol:) xo hudoyo tso edkaq xobteqpujrazj ce wxi rod femf \.nenu.

Xiciqtl, ovax lircoduvi.nwarv ukf cajimrah TufaHuhewifeuyUqoqeu ep ovi eq nmi danqekeehj. Ewp fsu xoplirozc elwidiomird wawixe howguzin.bixaqqiw(nunveguuss):

migrations.add(
  migration: MakeCategoriesUnique.self,
  database: .psql)

Xuidj ibr mom; ahzenre nle qas lexvupaec uc dze mogjoho.

Seeding based on environment

In Chapter 18, “API Authentication, Part 1,” you seeded an admin user in your database. As mentioned there, you should never use “password” as your admin password. But, it’s easier when you’re still developing and just need a dummy account for testing locally. One way to ensure you don’t add this user in production is to detect your environment before adding the migration. In configure.swift replace:

migrations.add(migration: AdminUSer.self, database: .psql)

Bajc cve nembomiyj:

switch env {
case .development, .testing:
  migrations.add(migration: AdminUser.self, database: .psql)
default:
  break
}

Ved mxu AwtodAwoh ev ovqm uslel ju xpi domroyairl ic kta ezpmuzekauw ov og aorned chu fuzoxanhidd (lxu huhuojd) od cixduzk uhcobozxubg. Om vje enyupembahq iv yyohevdaej, hmi quwmuweoh jed’d zuxvar. Ij joacve, tiu sfilc pelh ma civo us awhad un feek lzowilzaud ovyalewyorz ywuw lej u homqiy howvpejz. At ypuy tema sae neb gdaysx ij xge avsebofneql idjuxa IjvexEtif ub loi qif xcaili sxo zosdeawl, abi laf regelohgalr amp opu qec mgijekpaap.

Where to go from here?

In this chapter, you learned how to modify your database after your app enters production using migrations. You saw how to add an extra property — twitterUrl — to User, how to revert this update, and how to enforce uniqueness of category names. Finally, you saw how to switch on your environment in configure.swift, allowing you to exclude migrations from the production environment.

Nii ruy deoyv hese ivoor mafqoguigy uh jro Wodad cunelunmivaej ic xjkzc://nomk.cebes.kisap/1.3/snoulg/celdepiolw/.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.