En la entrada Mejora al tutorial ‘Building a Web App with Go and SQLite’: limitar el número de registros devueltos por defecto modificamos el código original del tutorial de Jeremy Morgan Building a Web App with Go and SQLite para que el usuario pudiera especificar el número de resultados devueltos al consultar la API a través del parámetro count
: http://<URL>/api/v1/person?count=15
.
El problema es que siempre se devuelven las entradas empezando por la de Id más bajo (habitualmente, 1
).
En esta entrada, modificamos la función getPersonById
para introducir también el parámetro count
y que se devuelvan count
resultados a partir del Id especificado.
Como comentaba en la entrada Mejora al tutorial ‘Building a Web App with Go and SQLite’: limitar el número de registros devueltos por defecto, en el mundo real las APIs devuelven un determinado número de resultados y un índice (o un paginador) que permite solicitar el siguiente conjunto de resultados… Podemos repetir el proceso hasta que la API nos informe que no hay más resultados disponibles.
El problema es que models.GetPersons(count int)
sólo acepta el número de resultados a retornar, y no desde qué entrada, por lo que siempre devuelve el mismo conjunto de resultados.
Una forma de solucionar este problema sería modificando la función GetPersons(count int)
para que admita un parámetro adicional: el número de registro desde el que empezar a retornar los resultados… Pero eso es precisamente lo que hace la función GetPersonById
(aunque sólo devuelve 1 registro).
Modificaremos el comportamiento de GetPersonById
para aceptar también un parámetro count
, que será el número de registros a devolver (incluyendo al especificado por el Id
).
El resultado final es que el usuario pueda solicitar mediante http://<URL>/api/v1/person/5?count=10
los siguientes 10 registros al indicado por el Id=5
.
En cierto modo, con esta modificación, la función
GetPersons
sería un caso particular deGetPersonById
, en el que siempre especificamosId=1
.
Modificación del código
Como antes, definimos unos valores por defecto; en este caso, el valor por defecto de count
es 1
(devolver sólo el registro con el Id
solicitado):
// Limit the number of records returned
var max_count int = 10
// Default value
var count int = 1
El resto del código es igual al de la entrada anterior; en primer lugar, validamos si el usuario ha especificado un valor para el parámetro count
; si es así, lo convertimos de string
en int
(mediante strconv.Atoi
).
// If no "count" parameter is provided, we return the count value
if c.Query("count") != "" {
var conv_err error
count, conv_err = strconv.Atoi(c.Query("count"))
if conv_err != nil {
log.Printf("[error] error getting count from queryString (default to %d). error: %s", count, conv_err.Error())
return
}
}
El siguiente paso es validar que el valor de count
no sea superior al máximo que hemos especificado:
// Return max_count (at most)
if count > max_count {
log.Printf("[warning] requested %d records (returning max: %d)", count, max_count)
count = max_count
}
El resto del código de la función es igual al original.
El resultado final es:
func GetPersonById(c *gin.Context) {
// Limit the number of records returned
var max_count int = 10
// Default value
var count int = 1
// If no "count" parameter is provided, we return the count value
if c.Query("count") != "" {
var conv_err error
count, conv_err = strconv.Atoi(c.Query("count"))
if conv_err != nil {
log.Printf("[error] error getting count from queryString (default to %d). error: %s", count, conv_err.Error())
return
}
}
// Return max_count (at most)
if count > max_count {
log.Printf("[warning] requested %d records (returning max: %d)", count, max_count)
count = max_count
}
id := c.Param("id")
results, err := models.GetPersonById(id, count)
if err != nil {
log.Printf("[error] retrieving record from database: %s", err.Error())
}
if results[0].FirstName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "not records found"})
return
} else {
c.JSON(http.StatusOK, gin.H{"data": results})
}
}
Resultado del cambio
Por defecto, si no se especifica el parámetro count
en la petición, sólo se devuelve un resultado:
$ curl "localhost:8080/api/v1/person/3"
{"data":[
{"id":3,"first_name":"Gaven","last_name":"Allanby","email":"gallanby2@cloudflare.com","ip_address":"33.223.203.230"}
]}
Este es el mismo comportamiento que la función del tutorial.
Sin embargo ahora, podemos especificar count
para devolver resultados adicionales:
$ curl "localhost:8080/api/v1/person/3?count=5"
{"data":[
{"id":3,"first_name":"Gaven","last_name":"Allanby","email":"gallanby2@cloudflare.com","ip_address":"33.223.203.230"},
{"id":4,"first_name":"Clara","last_name":"Vince","email":"cvince3@cloudflare.com","ip_address":"179.132.154.90"},
{"id":5,"first_name":"Rozalie","last_name":"Stein","email":"rstein4@sitemeter.com","ip_address":"43.208.221.109"},
{"id":6,"first_name":"Britni","last_name":"Pacquet","email":"bpacquet5@paypal.com","ip_address":"232.111.115.116"},
{"id":7,"first_name":"Roselin","last_name":"Aleso","email":"raleso6@a8.net","ip_address":"0.219.112.14"}
]}
Como vemos, se devuelven el número de resultados especificados en el parámetro count
(5
) a partir del Id=3
.
Si especificamos un valor para count
superior al máximo definido, sólo se devuelven max_count
resultados:
curl "localhost:8080/api/v1/person/3?count=500"
{"data":[
{"id":3,"first_name":"Gaven","last_name":"Allanby","email":"gallanby2@cloudflare.com","ip_address":"33.223.203.230"},
{"id":4,"first_name":"Clara","last_name":"Vince","email":"cvince3@cloudflare.com","ip_address":"179.132.154.90"},
{"id":5,"first_name":"Rozalie","last_name":"Stein","email":"rstein4@sitemeter.com","ip_address":"43.208.221.109"},
{"id":6,"first_name":"Britni","last_name":"Pacquet","email":"bpacquet5@paypal.com","ip_address":"232.111.115.116"},
{"id":7,"first_name":"Roselin","last_name":"Aleso","email":"raleso6@a8.net","ip_address":"0.219.112.14"},
{"id":8,"first_name":"Parrnell","last_name":"Castellaccio","email":"pcastellaccio7@woothemes.com","ip_address":"156.168.236.20"},
{"id":9,"first_name":"Jobie","last_name":"McGiveen","email":"jmcgiveen8@tinyurl.com","ip_address":"124.84.5.0"},
{"id":10,"first_name":"Lyn","last_name":"Kupper","email":"lkupper9@adobe.com","ip_address":"10.173.222.145"},
{"id":11,"first_name":"Flossy","last_name":"Wareham","email":"fwarehama@google.ru","ip_address":"44.136.94.10"},
{"id":12,"first_name":"Clemens","last_name":"Vail","email":"cvailb@admin.ch","ip_address":"77.176.26.76"}
]}
Y se registra en los logs del servidor:
2022/08/12 12:42:18 [warning] requested 500 records (returning max: 10)
[GIN] 2022/08/12 - 12:42:18 | 200 | 486.665µs | ::1 | GET "/api/v1/person/3?count=500
Casos límite
Sabiendo que en nuestra base de datos de pruebas tenemos mil registros, podemos observar qué pasa cuando se llega al final de los registros existentes:
$ curl "localhost:8080/api/v1/person/999?count=5"
{"data":[
{"id":999,"first_name":"Maxim","last_name":"Heake","email":"mheakerq@seesaa.net","ip_address":"13.228.167.253"},
{"id":1001,"first_name":"Xavier","last_name":"Aznar","email":"onthedock@example.cat","ip_address":"127.0.0.1"}
]}
Si solicita un registro con un Id
que no existe en la base de datos, no se devuelve nada:
$ curl "localhost:8080/api/v1/person/2000?count=5"
$
En el servidor, se genera un panic, aunque el framework Gin se recupera y el servidor continua funcionando con normalidad:
runtime error: index out of range [0] with length 0
Autocrítica
La función GetPersons
deja de tener mucho sentido, ya que obtenemos los mismos resultados con http://<URL>/api/v1/person/?count=10
que con http://<URL>/api/v1/person/1?count=10
, por lo que quizás sería mejor usar un enfoque diferente basado en páginas: http://<URL>/api/v1/person/?page=12
. Estableciendo un valor de resultados por página (por ejemplo, 10), esta petición devolvería los resultados con Id
desde 12*10=120 hasta 129.
Otra posible mejora sería devolver algún mensaje indicando que no se han encontrado resultados, y si es posible, evitar el panic en el lado del servidor.
Conclusión
Al introducir el parámetro count
para GetPersonById
es posible retornar un conjunto de resultados al realizar la petición a la API desde cualquier registro dado. De esta forma, el usuario podría iterar sobre los registros en la base de datos de forma parecida a como sucede en las APIs reales.