diff --git a/src/blog/posts/2024/5/learning-go-day-10.md b/src/blog/posts/2024/5/learning-go-day-10.md new file mode 100644 index 0000000..4f35b5d --- /dev/null +++ b/src/blog/posts/2024/5/learning-go-day-10.md @@ -0,0 +1,177 @@ +--- +title: "Learning Go: Day Ten" +date: 2024-05-11T08:00:00.0Z +tags: + - learning + - go +excerpt: "More databases, enums, and all that sort of fun stuff." +--- + +Yesterday, [I got connected to a SQLite database](/post/learning-go-day-nine), and got my first table created. I'm going to carry on with that thread today and do some more database work. + +## Modifying the Sites table + +Yesterday, I mentioned that really my Sites should just be identified by the URL, rather than having an auto-incremented ID. There are some caveats here, and in theory I should really maintain a unique UUID or something, but this is a very simple app and I can always change it later if I need to. + +So, firstly I need to change my table definition. Because this app has no data, I'm just going to delete the existing database off my server, and have this be the initial table definition. Later on I'll need to use some form of migration to do these changes to prevent data loss: + +```go +// sites/sites.go +const createSitesTable = `CREATE TABLE IF NOT EXISTS sites ( +url TEXT NOT NULL PRIMARY KEY, +created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, +name TEXT NOT NULL +);` + +type Site struct { + created_at uint64 + Name string + Url string +} +``` + +That's more-or-less the biggest change I need to make. The nice thing about this is that it allows me to really simplify the `Save` function by using an `upsert`[^1] instead: + +```go +const upsertQuery = `INSERT INTO sites (url, name) VALUES (?, ?) +ON CONFLICT (url) DO UPDATE +SET + name = excluded.name +` + +func (s *Site) Save(db *sql.DB) { + db.Exec(upsertQuery, s.Url, s.Name) +} +``` + +## Pingu! Wait, no + +The next model I want to define is a log of all of the "pings" I've made to a site. The heading from this section comes from the fact that the file I've created is called `ping.go`[^2]. I figured a ping would need three key pieces of data: + +* The site it's pinging +* The time it occurs +* Something that tells me what the result was + +The struct definition for this was fairly straightforward: + +```go +// ping/ping.go +type Ping struct { + Site sites.Site + Timestamp string + Status ??? +} +``` + +## Enums in Go + +The `Status` field is the only one I wasn't certain about. In other languages, I might use an Enum for this, but Go doesn't have Enums! Instead, you can use grouped constants, which are immutable, and the [`iota` keyword](https://go.dev/wiki/Iota) to auto-increment a value. I've defined my `Status` enum using this: + +```go +type Status int16 + +const ( + Success Status = iota + Failure +) +``` + +If I were to print the values, I'd get something like: + +```go +fmt.Println("%d", Success) // 0 +fmt.Println("%d", Failure) // 1 +``` + +And now I can reference the `Status` type in my `Ping` struct: + +```go +type Ping struct { + Site sites.Site + Timestamp string + Status Status +} +``` + +## Creating the tables + +The enum workaround isn't ideal, but it actually maps really nicely to SQLite, which _also_ doesn't have enums. Instead, I have to create a table just for the status type, and then seed the values. + +```go +const createQuery = `CREATE TABLE IF NOT EXISTS statuses ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL +);` + +const seedStatusQuery = `INSERT INTO statuses (id, name) VALUES (?, ?) +ON CONFLICT (id) DO NOTHING;` + +func CreateTable(db *sql.DB) { + if _, err := db.Exec(createQuery); err != nil { + panic(err) + } + + seedStatuses(db) +} + +func seedStatuses(db *sql.DB) { + if _, err := db.Exec(seedStatusQuery, Success, "Success"); err != nil { + panic(err) + } + if _, err := db.Exec(seedStatusQuery, Failure, "Failure"); err != nil { + panic(err) + } +} +``` + +And then finally, I can create my table to store the `ping` log: + +```go +const createQuery = `CREATE TABLE IF NOT EXISTS statuses ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS ping ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + site TEXT NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + status INTEGER NOT NULL, + FOREIGN KEY (site) REFERENCES sites(url), + FOREIGN KEY (status) REFERENCES statuses(id) +);` +``` + +So to break that down, I've got 4 fields, two of which are auto-generated on inserting a row. The other two are just foreign key references to the statuses and sites tables. That makes saving a ping fairly easy: + +```go +const saveQuery = `INSERT INTO ping (site, status) VALUES (?, ?);` + +func (p *Ping) Save(db *sql.DB) { + db.Exec(saveQuery, p.Site.Url, p.Status) +} +``` + +Finally, because SQLite doesn't have foreign key constraints enabled by default, I need to turn them on using a PRAGMA statement when my application starts up: + +```go +// main.go + +func main() { + // ... + + db.Exec("PRAGMA foreign_keys = ON;") + sites.CreateTable(db) + ping.CreateTable(db) +} +``` + +## Why not use an ORM? + +I _could_ use an ORM[^3] and there are some fairly popular ones out there, like [GORM](https://gorm.io). I've decided not to for a couple of reasons. Partly because I'm doing this to learn, and while I'm already fairly comfortable with SQL, understanding the underlying connection is useful. The other reason is that I don't really like how much ORMs obfuscate the database, they're usually only good for the simplest of queries anyway - it's often much more efficient to write complex queries by hand. + +That's Day 10 wrapped up. Next up, I'll be working on actually populating some data in the database. + +[^1]: Maybe I'm alone here, but finding out that this was possible in SQL was such a game-changer for me +[^2]: Any similarities to mischievous claymation penguins, living or dead, is purely coincidental +[^3]: Object Relational Mapper, that sits between the software and the database and provides handy functions for easily creating data models, tables, and the relationships between them \ No newline at end of file