Skip to content

Commit 516e6d4

Browse files
committed
sync impl/server-database README
1 parent c615c0f commit 516e6d4

File tree

1 file changed

+312
-0
lines changed

1 file changed

+312
-0
lines changed

server-database/README.md

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,315 @@ Learning objectives:
1010
- Understand how a server and a database work together
1111
- Use SQL to read data from a database
1212
- Accept data over a POST request and write it to the database
13+
14+
## Steps
15+
16+
`go mod init server-database`
17+
18+
[intro to Go and JSON](https://go.dev/blog/json)
19+
20+
Create a struct that represents the data:
21+
22+
```go
23+
type Image struct {
24+
Title string
25+
AltText string
26+
Url string
27+
}
28+
```
29+
30+
Initialise some data:
31+
32+
```go
33+
images := []Image{
34+
{"Sunset", "Clouds at sunset", "https://images.unsplash.com/photo-1506815444479-bfdb1e96c566?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1000&q=80"},
35+
{"Mountain", "A mountain at sunset", "https://images.unsplash.com/photo-1540979388789-6cee28a1cdc9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1000&q=80"},
36+
}
37+
```
38+
39+
Import `"encoding/json"` and `Marshal`:
40+
41+
```go
42+
b, err := json.Marshal(images)
43+
```
44+
45+
Write this back as the response:
46+
47+
```
48+
> curl 'http://localhost:8080/images.json' -i
49+
HTTP/1.1 200 OK
50+
Content-Type: text/json
51+
Date: Wed, 03 Aug 2022 18:06:34 GMT
52+
Content-Length: 487
53+
54+
[{"Title":"Sunset","AltText":"Clouds at sunset","Url":"https://images.unsplash.com/photo-1506815444479-bfdb1e96c566?ixlib=rb-1.2.1\u0026ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8\u0026auto=format\u0026fit=crop\u0026w=1000\u0026q=80"},{"Title":"Mountain","AltText":"A mountain at sunset","Url":"https://images.unsplash.com/photo-1540979388789-6cee28a1cdc9?ixlib=rb-1.2.1\u0026ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8\u0026auto=format\u0026fit=crop\u0026w=1000\u0026q=80"}]
55+
```
56+
57+
Add a query param `indent` which uses `MarshalIndent` instead (https://pkg.go.dev/encoding/json#MarshalIndent) such that the following snippet works. The indent value should increase the amount of indentation: `?indent=4` should have 4 spaces, but `?indent=2` should have 2.
58+
59+
To do this, you'll need to investigate the `strconv` and `strings` packages in the Go standard library.
60+
61+
```
62+
> curl 'http://localhost:8080/images.json?indent=2' -i
63+
HTTP/1.1 200 OK
64+
Content-Type: text/json
65+
Date: Mon, 08 Aug 2022 19:57:51 GMT
66+
Content-Length: 536
67+
68+
[
69+
{
70+
"Title": "Sunset",
71+
"AltText": "Clouds at sunset",
72+
"Url": "https://images.unsplash.com/photo-1506815444479-bfdb1e96c566?ixlib=rb-1.2.1\u0026ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8\u0026auto=format\u0026fit=crop\u0026w=1000\u0026q=80"
73+
},
74+
{
75+
"Title": "Mountain",
76+
"AltText": "A mountain at sunset",
77+
"Url": "https://images.unsplash.com/photo-1540979388789-6cee28a1cdc9?ixlib=rb-1.2.1\u0026ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8\u0026auto=format\u0026fit=crop\u0026w=1000\u0026q=80"
78+
}
79+
]
80+
```
81+
82+
We've now got a working server that responds to requests with data in JSON format, and can format it.
83+
84+
---
85+
86+
Next we're going to set up a database to store data that our server will use.
87+
88+
We'll use [Postgres](https://www.postgresql.org/), which is an open source relational database. Don't worry if that doesn't mean anything right now. Read the Postgres website to find out the core ideas.
89+
90+
First, install Postgres. You may have been provided with access to Amazon Web Services, which provides Postgres for you via their Relational Database Service. If not, you can also run it on your computer: follow the [instructions on the Postgres website](https://www.postgresql.org/download/).
91+
92+
Your goal is to have a database running that you can connect to using a connection string, which will look something like this: `postgres://user:secret@localhost:5432/mydatabasename`
93+
94+
For easy demoing, we'll assume you have Postgres running locally and connect with `postgresql://localhost`.
95+
96+
Next install [pgAdmin](https://www.pgadmin.org/) (if you haven't already). This is a useful user interface that we'll use to set up the Postgres database.
97+
98+
Open up pgAdmin and add a server that connects to your instance of Postgres.
99+
100+
Then add a database by right-clicking on Databases.
101+
102+
![](./readme-assets/create-database.png)
103+
104+
The SQL generated (see the SQL tab) should read as follows:
105+
106+
```sql
107+
CREATE DATABASE "go-server-database"
108+
WITH
109+
OWNER = postgres
110+
ENCODING = 'UTF8'
111+
LC_COLLATE = 'en_US.UTF-8'
112+
LC_CTYPE = 'en_US.UTF-8'
113+
CONNECTION LIMIT = -1
114+
IS_TEMPLATE = False;
115+
```
116+
117+
TODO: note here introducing SQL.
118+
119+
Data in Postgres is arranged in tables with columns, like a spreadsheet.
120+
121+
Next create a table that will store our image data. Within the `go-server-database` database, open `schemas`, `public`, and then create a new table.
122+
123+
![](./readme-assets/create-table.png)
124+
125+
Add four columns:
126+
127+
- `id`: type `serial` with "Primary key?" and "Not null?" turned on
128+
- `title`: type `text` with "Not null?" turned on
129+
- `url`: type `text` with "Not null?" turned on
130+
- `alt_text`: type `text`
131+
132+
The SQL tab should look like the following:
133+
134+
```sql
135+
CREATE TABLE public.images
136+
(
137+
id serial NOT NULL,
138+
title text NOT NULL,
139+
url text NOT NULL,
140+
alt_text text,
141+
PRIMARY KEY (id)
142+
);
143+
144+
ALTER TABLE IF EXISTS public.images
145+
OWNER to postgres;
146+
```
147+
148+
Once the table is created, we can insert some data.
149+
150+
Right click on the table and open `Scripts > INSERT script`.
151+
152+
![](./readme-assets/insert-script.png)
153+
154+
This will open the Query tool, which will allow us to add some data, and give some basic SQL to start with.
155+
156+
```sql
157+
INSERT INTO public.images(
158+
id, title, url, alt_text)
159+
VALUES (?, ?, ?, ?);
160+
```
161+
162+
If you run this ("play" button at the top) you will get an error, because we haven't provided any data.
163+
164+
Update the SQL to look like this. We don't need to specify an ID: Postgres will do this.
165+
166+
```sql
167+
INSERT INTO public.images(title, url, alt_text)
168+
VALUES ('Sunset', 'https://images.unsplash.com/photo-1506815444479-bfdb1e96c566?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1000&q=80', 'Clouds at sunset');
169+
```
170+
171+
This should insert some data:
172+
173+
```
174+
INSERT 0 1
175+
176+
Query returned successfully in 132 msec.
177+
```
178+
179+
We can check by deleting this query and running a different one:
180+
181+
```sql
182+
SELECT * from images;
183+
```
184+
185+
You should see the row you added, and a value in the ID column.
186+
187+
Now write a new `INSERT` query with the other image file from our code.
188+
189+
When this is done, the `SELECT` query should return two rows with different IDs and data. Nice.
190+
191+
---
192+
193+
We can now connect our server to the database.
194+
195+
The server and the database are two separate systems that communicate over a network, like our client (`curl`) and server did before. This time, the server will be acting as the "client" by making requests to the database, and the database will responding with data it has stored.
196+
197+
We will be sending requests from our server to the database using SQL: from now on we'll call them queries. At the most basic level, queries can read, write, update and delete data.
198+
199+
Let's get going.
200+
201+
Install pgx: `go get github.com/jackc/pgx/v4`
202+
203+
`pgx` is the library we will use to connect to and query the database.
204+
205+
The first step is to connect. Our server needs to know which database to connect to, and we'll do that by running the server like this:
206+
207+
```
208+
> DATABASE_URL='postgres://localhost:5432/go-server-database' go run .
209+
```
210+
211+
`DATABASE_URL` is an environment variable, and our server can access the value of this variable like this:
212+
213+
```go
214+
os.Getenv("DATABASE_URL")
215+
```
216+
217+
Before going any further, write some code that checks if `DATABASE_URL` is set. If it's not: write a helpful error to `os.Stderr` and exit the process with a non-zero code, to indicate failure (`os.Exit(1)`).
218+
219+
Next, create a connection using `pgx.Connect`. You will need to supply a `context`, which can simply be `context.Background()`. You can [find out about contexts in the Go documentation](https://pkg.go.dev/context), but they are not an important concept for now.
220+
221+
Remember to handle errors in connecting to the database gracefully: handle the error and output a useful error message. You can check this is working by turning off Postgres and starting your server:
222+
223+
```
224+
> DATABASE_URL='postgres://localhost:5432/go-server-database' go run .
225+
unable to connect to database: failed to connect to `host=localhost user=tom database=go-server-database`: dial error (dial tcp 127.0.0.1:5432: connect: connection refused)
226+
exit status 1
227+
```
228+
229+
Once connected to the database, you can look in pgAdmin at the Dashboard for your server: it will show an active session.
230+
231+
It's important to gracefully close our connection to the database, so we need to add `defer conn.Close(context.Background())` immediately after we successfully connect. The `defer` keyword means that this line of code is delayed until the nearby function returns. [Defer is really useful!](https://go.dev/blog/defer-panic-and-recover)
232+
233+
Now we're connected we can fetch some data.
234+
235+
To get data we'll use `conn.Query`, which is a [method](https://gobyexample.com/methods) defined for the connection (`pgx.Conn`) to the Postgres database.
236+
237+
The SQL will be the same as above, to fetch all the images:
238+
239+
```sql
240+
"SELECT title, url, alt_text FROM public.images"
241+
```
242+
243+
Remember to handle the error from `conn.Query` gracefully.
244+
245+
`conn.Query` returns some `Rows` which we can use to `Scan` for the actual data:
246+
247+
We need a container [slice](https://gobyexample.com/slices) for the image: `var images []Image`
248+
249+
We can iterate over the rows: `for rows.Next() { ... }`
250+
251+
Using `Scan` to extract the data from each row means [passing a pointer to the variables](https://gobyexample.com/pointers) you want to fill, so they can be modified:
252+
253+
```go
254+
var title, url, altText string
255+
err = rows.Scan(&title, &url, &altText)
256+
```
257+
258+
After handling the possible error, you can add an image to the list:
259+
260+
```go
261+
images = append(images, Image{Title: title, Url: url, AltText: altText})
262+
```
263+
264+
And that's it! We've not got all images from the database, and our server can now return them as JSON.
265+
266+
The structure of your overall solution now should look something like:
267+
268+
```go
269+
func main() {
270+
// Check that DATABASE_URL is set
271+
272+
// Connect to the database
273+
274+
// Handle a possible connection error
275+
276+
// Defer closing the connection to when main function exits
277+
278+
// Fetch images from the database
279+
280+
// Send a query to the database, returning raw rows
281+
282+
// Handle query errors
283+
284+
// Create slice to contain the images
285+
286+
// Iterate through each row to extract the data
287+
// Extract the data, passing pointers so the data can be updated in place
288+
// Append this as a new Image to the images slice
289+
290+
// Create the handler function that serves the images JSON
291+
// Handle the indent query parameter
292+
// Convert images to a byte-array for writing back in a response
293+
// Write a `Content-Type` header
294+
// Write the data back to the client
295+
296+
// Listen & serve on port 8080
297+
}
298+
```
299+
300+
Don't forget error handling!
301+
302+
---
303+
304+
There's now quite a bit of logic in this `main` function and it's getting a bit big.
305+
306+
Extract the image fetching log (not the database connection) to its own function with this signature:
307+
308+
```go
309+
func fetchImages(conn *pgx.Conn) ([]Image, error) {
310+
// Fetch images from the database
311+
312+
// Send a query to the database, returning raw rows
313+
314+
// Handle query errors
315+
316+
// Create slice to contain the images
317+
318+
// Iterate through each row to extract the data
319+
// Extract the data, passing pointers so the data can be updated in place
320+
// Append this as a new Image to the images slice
321+
322+
// Return the images
323+
}
324+
```

0 commit comments

Comments
 (0)