Torronsuo, Somero

  Pääsivulle  |  TIETOTEKNIIKAN PÄÄSIVULLE

SQLite macOS-ympäristössä

SQLite valmiina

Macissä SQLite löytyy osana käyttöjärjestelmää, mikä yksinkertaistaa huomattavasti sen käyttämistä: ei tarvitse huolehtia asennuksista, kirjastotiedostoista eikä versiomuunnoksista.

Jos tuot Macin SQLite-kantaan taulujen sisällön esimerkiksi Windowsista skriptitiedostoilla, jotka sisältävät SQL-lausekkeita, niin huomaa, että tiedostojen tulee olla koodattu utf-8. Muutoin skandeja sisältäviä sanoja ei saa tauluista ulos.

Macissä sovellustiedosto (.app) sijoitetaan Applications-kansioon sellaisenaan, tai sitten perustetaan sinne oma kansio. jonne laitetaan app ja mahdolliset muut käynnistämisen vaatimat datatiedostot. SQLite tai jäljempänä esitelty FMBD eivät vaadi sinne mitään kirjastoja erikseen.

SQLite ohjelmakoodissa

Tietokannan käsittely ohjelmakoodin sisällä Xcode:ssa kannattaa tehdä käyttäen apukirjastoja (SQL wrapper), joiden avulla tietokantaoperaatiot on helppo ohjelmoida. Ei kannata käyttää Macin omaa liitäntää SQL-lirjastoonsa. Olen käyttänyt kahta tuunetuinta wrapperia: FMDB ja GRDB. Ensinmainittu on vanhempi ja sen on ohjelmoitu Objective-C -kielellä. Siitä seuraa joitain rajoituksia koodauksessa, esimerkiksi executeUpdate(..) on käytettävissä vain rajoitetusti. GRDB on kokonaan ohjelmoitu Swift-kielellä. Molemmat toimivat hyvin sekä Swift 5- että SwiftUI-ympäristössä.

GRDB JA SEN ASENTAMINEN

GRDB:n uusin versio 5.2.0 on valmistunut 29.11.2020, ja se on ohjelmoitu kielellä Swift 5.2. Se löytyy täältä
Samassa paikassa on myös peruskäyttöohjeet, ja myös yksityiskohtaisemmat käyttöohjeet. Se kannattaa asentaa CocoaPods:n avulla.


TYÖVAIHEET:

1)  Avaa terminaali ja anna komennot:
sudo gem install cocoapods
pod setup --verbose

Tämä asentaa Macciisi cocoapods:in ja se on siellä myöhemmin käytettävissä muita projektiasi varten.

2)  Sinulla pitää olla aloitettuna projektisi, jossa käytät SQLite:a. Sulje projektisi. Siirry terminaalissa kansioon, jossa on projektitiedostosi, esimerkiksi GRDBtesti.xcodeproj
Anna sitten komento
pod init

Tämä tekee tiedoston nimeltä Podfile. Avaa tiedosto tekstieditorilla (joka säilyttää utf-8) ja editoi se muotoon joka vastaa ympäristöäsi:
platform :macos, '11.0'
target 'GRDBtesti' do
use_frameworks!
pod 'GRDB.swift'
end

3) Seuraavaksi anna asennuskomento terminaalissa:
pod install

Projektin työkansio näyttää Finderissa nyt tällaiselta:
projektikansio Finderissa

Nyt avaa projektisi kaksoisklikkaamalla xcworkspace-tiedostoa, ja tästä lähtien projekti aina avataan siitä (Xcode muistaa).

Lisää jonkin kooditiedostoon alkuun lauseke import GRDB  ja käännä projektisi. Nyt ei pitäisi tulla ongelmia tämän lausekkeen johdosta.


GRDB:n käyttäminen koodissa

Alla muutamia esimerkkejä tavallisimpien SQL-lausekkeiden käyttämisestä. Ne olen testannut SwiftUI-ympäristössä, mutta toimivat myös puhtaassa Swift-ympäristössä. Lisää yksityiskohtia ja selityksiä löytyy GRDB:n dokumentaatiosta. Olen huomannut, että SwiftUI-ympäristössä kannattaa Xcode:ssa ahkerasti klikata "Clean build folder", jos on muokannut ohjelmakoodia SQL-lausekkeiden tienoilla. Tämän jälkeen Xcode ilmoittaa, että GRDB on hukassa, mutta Build-klikkaus palauttaa taas tilanteen normaaliksi.

Lisäksi Xcode saattaa suositella tällaisia muutoksia:  "Build Settings - Automatically Select Architectures". Älä hyväksy tällaisia.

Tietokannan avaaminen:

var dbg: DatabaseQueue? = nildo
{
    dbg = try DatabaseQueue(path: polkuz)   // polkuz on sqlite-tiedoston absoluuttinen polku
}
catch
{
    // virhe
}

Jos sinulla on do - catch -rakenne, jossa sisällä GRDB-toimintaa, niin älä laita catch-osan sisään return-lauseketta. Ei se ole hyvä tapa muulloinkaan.

Tietokantataulun luominen ja taulun päivittämiset:

do
{
   try dbg.write
   {
       db in
      try db.execute(sql: "CREATE TABLE Kasvit (idi INTEGER PRIMARY KEY, nimi TEXT, harvi INT"
   
      try db.execute(
         sql: "INSERT INTO Kasvit (nimi, harvi) VALUES (?, ?)",
         arguments: ["Pujo", 2])
   
      try db.execute(
         sql: "UPDATE Kasvit SET harvi = ? WHERE idi = ?",
         arguments: [6,  4])

      try db.execute(
         literal: "UPDATE Kasvit SET nimi = \(name) WHERE idi = \(id)")
   }

}
catch
{
    // virhe
}

Huomaa edellä literal-rakenne, joka muistuttaa merkkijonoa String, mutta oikeasti se ei sitä ole. Kirjasto käsittelee sen SQLLiteral-rakenteena välttyen siten SQL-injektiolta.

Edellä oleva dbg.write -rakenne toimii samalla transaktiona: jos jokin osa pettää, niin mitään osaa ei lopulta tule suoritetuksi. Transaktioon kuuluvat osat voivat olla myös loopin sisällä, esimerkiksi:
do
{
    try dbg!.write
    {
        db in
        for i in 0..<pait.count
        {
            try db.execute(literal: "INSERT INTO zb_apu (iKas) VALUES (\(pait[i]))")
            // TAI ensin kootaan SQLLiteral, esim:
            let query = SQLLiteral(
                 sql: "INSERT INTO zc_apu (izKas, Knimi) VALUES (?, ?)",
                 arguments: [kast[i], nimet[i]])
            try db.execute(literal: query)
        }
    }
}
catch
{
    // VIRHE
}

DROP-komento voidaan toteuttaa mukavasti funktiolla:

do
{
    try dbg!.write
    {
        db in
        try db.drop(table: tauluz)
    }
}
catch
{
    // taulua ei ollut tai muu virhe
}

Tietoja tietokannasta haetaan seuraavilla tavallisilla tavoilla. Käytettävissä on myös blobien käsittely (ei tässä demossa):
do
{
    try dbg!.read
    {
        db in
        let rows = try Row.fetchCursor(db, sql: sqz)
        while let row = try rows.next()
        {
            let ii: Int? = row["iPaikka"]   // Int?   voi olla kannassa null
            let ipa = (ii == nil) ? 0 : ii!
                   
            var z: String? = row["Vnimi"]
            let vnimiz = (z == nil) ? "" : z!

            z = row["Mika"]
            let mikaz = (z == nil) ? "" : z!
        }
    }
}
catch
{
    // virhe
}

Jos SQL-lausekkeissasi on virhe, esimerkiksi siellä mainittua kenttää ei ole olemassa, niin useinkaan GRDB ei anna virheilmoitusta, vaan ohjelman toiminta vain pysähtyy. Xcode:ssa testatessa toiminta saattaa päättyä ilmoitukseen, että virhe on jonkun GRDB-osan Swift-tiedostossa. Joten sovelluksesi tulee testata huolella tällaisten virheiden varalta, ja huolehtia, että jokainen SQL-lauseke tulee testatessa ajettua.

Xcode:ssa koodia ei voi aina testata rivi kerrallaan. Esimerkiksi SwiftUI:ssa edelläesitetyn fetchCursor-while-loopin sisällä on käytettävä print()-lausekkeita, jos haluaa seurata mitä tapahtuu missäkin. Tämä voi olla SwiftUI:n ongelma.

FMDB JA SEN ASENTAMINEN

FFMDB-paketin nykyinen versio v. 2.7.5 on valmistunut 7.5.2020. Sen voi asentaa usealla eri tavalla, joista alla on selitetty yksi niistä. Parhaat tavat ovat alla selitetty, tai sen vaihtoehtona CocoaPods:n käyttäminen.

FMDB:n voi ladata itselleen täältä. Klikkaa "Code / Dowload ZIP". Koneellesi latautui tiedosto fmdb-master.zip. Avaa paketti ja kansiosta fmbd-master / src / fmdb tarvitset h- ja m-tiedostoja. Nämä asennetaan erikseen kullekin projektille. Kirjastossa olevia osia FMDatabasePool  ja FMDatabaseQueue et tarvitse, ellei tietokantojen käyttösi ole erityisen monipuolista. Koska ne jäävät pois, niin tiedostoa FMDatabase.h pitää itse editoida. Kommentoi sieltä pois neljäs rivi:
// #import "FMDatabasePool.h"

TYÖVAIHEET:

1)  Lisää 'libsqlite3' standard library projektiin:

Xcode:ssa vasemmalla tiedostot-osiossa klikkaa projekti, keskiosan vasemmalta ylhäällä valitse oikea target (projektisi nimi), klikkaa Build Phases, Link Binary klikkaa +, avautuvasta ikkunasta valitse libsqlite3.dylib

2)  Tee tiedosto "FMDB-Bridging-Header.h", sen sisältö on:

    #import "FMDatabase.h"
    #import "FMResultSet.h"
    #import "FMDatabaseAdditions.h"

    Kopioi seuraavat FMDB-tiedostot projektiisi, vaikka niiden kielenä on Objective-C:
FMBD-tiedostoluettelo

3)   Mene Build Settings -> Swift Compiler - Code Generation
    - lisää kohtaan 'Objective-C Bridging Header': FMDB-Bridging-Header.h
    sekä kohtiin Debug että Release

    tai jos olit laittanut siltatiedoston projektiisi johonkin kansioon, niin em. kohtiin on laitettava myös kansion nimi:
    kansionNimi/FMDB-Bridging-Header.h

4)  Tietokannan ottaminen käyttöön swift-tiedostossa:

    Edellä esitettyjen toimenpiteiden jälkeen voit käyttää FMDB:n luokkia ja metodeita. Omaan luokkaasi kirjoitat esimerkiksi luokkamuuttujaksi:
    var db: FMDatabase?

Ja jatkat siitä vaikkapa hyödyntäen seuraavan kappaleen koodiesimerkkejä.

Kysymyksiä ja vastauksia löytyy paljon googlaamalla.

FMBD:n käyttäminen koodissa

Seuraavat koodiesimerkit perustuvat FMDB-kirjaston versioon 2.7.

Swift-sovelluksen sisältä ei voi tietokannalle antaa DROP TABLE -komentoa ilman että putsaa tilannetta:
db!.closeOpenResultSets()

Seuraavat esimerkit edellyttävät, että on määritelty tietokanta db:
var db: FMDatabase?

Avaa tietokanta:
let db = FMDatabase(path: dbpolkuz)  // tietokantatiedostoon yhteys, anna koko polku
// ylläolevan täsmällisempi vaihtoehto on:
let db: FMDatabase? = FMDatabase(path: dbpolkuz)
if db == nil
{
   // dbpolkuz ei kelpaa tietokannaksi
   return
}
guard db!.open() else
{
  
// Tietokantaa ei saatu auki
}

Tee päivityskysely:
let sqz = "UPDATE Kasvit SET Sunimi = 'Pujo' WHERE idkas = 1234"
do
{
    try db!.executeUpdate(sqz, values: nil)
}
catch
{
    print("Ongelma: \(error.localizedDescription)")
}


let muutoksiakpl = Int(db!.changes)   // montako riviä poistettu, lisätty, muutettu
let tulosid = Int(db!.lastInsertRowId())   // INSERTissä tehty uusin rivi


Tee tavallinen kysely, jossa vastauksena vain yksi luku:
let sqz = "SELECT count(*) AS kpl FROM Kasvit"
do
{
   let rs = try db!.executeQuery(sqz, values: nil)
   if  rs.next()
   {
      let k = rs.int(forColumn: "kpl") // jos kannassa NULL, tuloksena 0, Int32
      kpl = Int(k)  // jos halutaan Int
   }
}
catch
{
    print("Ongelma \(error.localizedDescription)")
}

Tee tavallinen kysely, jossa tuloksena on useita arvoja:
var blobi: Data?
let sqz = "SELECT idKoh, Polku, Bitmap FROM Kohteet WHERE alue = 1234"
do
{
   let rs = try db!.executeQuery(sqz, values: nil)
   while rs.next()
   {
      let i = rs.int(forColumn: "idKoh")  // jos kannassa null, saadaan i = 0, Int32
      idkuva = Int(i)
      let pz = rs.string(forColumn: "Polku")
      polkuz = (pz == nil) ? "(ei polkua)" : pz!
      blobi = rs.data(forColumn: "Bitmap")
      // tee arvoilla jotain
      let imax = NSImage.init(data: blobi!)  // blobin jatkokäsittelyä tästä
   }
}
catch
{
    print("Ongelma \(error.localizedDescription)")
}

Tee tavallinen kysely (rakenteen toinen vaihtoehto):
// testasin että intForColumnIndex() ei vaikuta nopeuteen
var blobi: Data?
let sqz = "SELECT idKoh, Polku, Bitmap FROM Kohteet WHERE alue = 1234"
var rs: FMResultSet?
do
{
   let rs = try db!.executeQuery(sqz, values: nil)
}
catch
{
   print("P01-04 \(error.localizedDescription)")
   return
}
while rs!.next()
{
   let i = rs?.int(forColumn: "idKoh")  // NULL -> i = 0, Int32?
   idkuva = Int(i!)
   let pz = rs?.string(forColumn: "Polku")
   polkuz = (pz == nil) ? "(ei polkua)" : pz!
   blobi = rs?.data(forColumn: "Bitmap")
   // tee arvoilla jotain
   let imax = NSImage.init(data: blobi!)  // blobin jatkokäsittelyä tästä
}


SQLite ja FMDB-softa eivät ole virheiden hallinnassa täydellisiä. Jos esimerkiksi SELECT-kyselyssä unohdat kenttänimien välistä pilkun, SQL-lauseke menee läpi, minkä takia db!.executeQuery ei ilmoita virheestä (debug-ikkunaan tulee ilmoitus), mutta tulokset ovat tietysti epätäydellisiä, esimerkiksi:
SELECT idKoh, nimi  polku, pvm FROM Kuvat WHERE idKoh = 234

Toistuvat perättäiset SQL-kyselyt kannattaa toteuttaa transaktiona varsinkin jos ne ovat toistoluupin sisällä. Eli ensin "kerätään varastoon" kaikki toteutettavat SQL-kyselyt ja sitten "lähetetään kerralla" ne tietokantamoottorille.  Vasta kun perättäiset kyselyt onnistuvat, niin toteutetaan muutokset tietokantaan (commit()). Jos ne eivät onnistu, peruutetaan siihen asti varastoon päässeet kyselyt (rollback()). Seuraavassa koodiluuranko:
let c1 = db!.beginTransaction()   // output: true = ok, false = ongelma ja lastError... käytetttävissä
if c1
{
   var itu = OOKOO
   for i in 0..<kpl
   {
      let sqz = "INSERT INTO ......  VALUES (   \(ixkuva[i]), ....."
      do
      {
         try db!.executeUpdate(sqz, values: nil)
      }
      catch
      {
         itu = VIRHE
                  print("Ongelma \(error.localizedDescription)")
         break
      }
   }
   if itu == OOKOO
   {
      c2 = db!.commit()  // TODO: käsittele output false eli virhe
      if c2
      {
          return true
      }
   }
   else
   {
      c3 = db!.rollback()  // TODO: käsittele output false eli katso <lastErrorMessage>
      itu = VIRHE 
      return false
   }
}


Funktioiden  executeUpdate(sqz, values: nil) ja  executeQuery(sqz, values: nil)  values-osaan voidaan laittaa SQL-lausekkeen parametreja kysymysmerkkien tilalle, esimerkiksi:
let scz = "SELECT idKuva, Polku FROM Kuvat WHERE Paikka = ? AND Vuosi = ?"
do
{
   let rs = try db!.executeQuery(sqz, values: ["Mikkeli", 1991])
   while rs.next()
   {
      // jne.
   }
}
catch
{
      print("Ongelma \(error.localizedDescription)")
}

Ylläoleva ei kuitenkaan toimi odotetulla tavalla kaikissa SQL-lauseissa. Koodia testaamalla selviää nopeasti kannattaako rakennetta käyttää juuri aiotulla tavalla.

Blob-tyyppisen tiedon lukeminen kannasta ja sen kirjoittaminen kantaan tapahtuu alla olevien esimerkkien mukaisesti. Suorituskyvyn takia kannan taulussa, jossa on blob-kenttiä, tulisi olla vain sellaista dataa, jota tarvitaan blob-kentän yhteydessä. Esimerkiksi valokuvien thumbnail-kuvat omaan tauluunsa (jossa thumbnail, viite kuvan metatietoihin, ja avainkenttä). Kuvan muut tiedot (päivämäärä, polku, aihe jne.) laitetaan omaan tauluunsa.

Blob-kentän lukeminen kannasta on esitetty edellä kohdassa 'tee tavallinen kysely':

Blob-kentän kirjoittaminen kantaan (tässä valmis thumbnail  tiedostona osoitteessa polkuz):
var blob2: Data?
let scz = "UPDATE Bitmapit SET Bitmap = ?, Timestamp = '\(pvmz)' WHERE iKuva = \(idz)"
do
{
    try db!.executeUpdate(scz, values: [blob2! as AnyObject])
}
catch
{
    print("ongelma: \(error.localizedDescription)\n\(scz)")
}



--------------------------------
(sivua muokattu 22.12.2020)