loading...
Cover image for API Rest con Go (Golang) y PostgreSQL Parte 3

API Rest con Go (Golang) y PostgreSQL Parte 3

orlmonteverde profile image Orlando Monteverde ・11 min read

Requisitos:

  • Conocimientos básicos sobre el lenguaje go.
  • Conocimiento sobre HTTP y REST recomendable.
  • Go en su versión 1.11 o superior.
  • PostgreSQL en su versión 9.6 o superior.
  • Conocimientos básicos de SQL deseables.
  • Ganas de aprender.

Que vamos a desarrollar

Crearemos un API Rest para un microblog, estilo twitter en su versión mas básica. Con un modelo de usuarios y uno de publicaciones. Con un CRUD completo en cada caso, autenticación con JSON Web Tokens, creación, uso de middlewares y asegurar contraseña por con bcrypt. ¿Suena interesante? ¡Entonces vamos!

Meme

Esta es la tercera parte, recomiendo comenzar por el primer articulo. O si gustas revisa el articulo anterior. De cualquier forma, el código del que vamos a partir en este artículo se encuentra en este repositorio.

Repositorios

Ahora vamos a crear script de SQL para crear las tablas correspondientes a nuestros modelos: user y post. No me detendré a explicar detalladamente el código porque no es el propósito de esta publicación, si gustas puedes darte una vuelta por este tutorial.

Si ya conoces SQL seguro sera fácil de entender y en caso contrario simplemente estamos creando una tabla llamada users, con columnas que corresponden a los campos de nuestra estructura user. De igual forma, una tabla posts, que corresponde a nuestra estructura post, que se relaciona con los usuarios por medio del campo user_id, que corresponde a un id de usuario.

CREATE TABLE IF NOT EXISTS users (
    id serial NOT NULL,
    first_name VARCHAR(150) NOT NULL,
    last_name VARCHAR(150) NOT NULL,
    username VARCHAR(150) NOT NULL UNIQUE,
    password varchar(256) NOT NULL,
    email VARCHAR(150) NOT NULL UNIQUE,
    picture VARCHAR(256) NOT NULL,
    created_at timestamp DEFAULT now(),
    updated_at timestamp NOT NULL,
    CONSTRAINT pk_users PRIMARY KEY(id)
);

CREATE TABLE IF NOT EXISTS posts (
    id serial NOT NULL,
    user_id int NOT NULL,
    body text NOT NULL,
    created_at timestamp DEFAULT now(),
    updated_at timestamp NOT NULL,
    CONSTRAINT pk_notes PRIMARY KEY(id),
    CONSTRAINT fk_posts_users FOREIGN KEY(user_id) REFERENCES users(id)
);

Seria posible utilizar este código en nuestro motor de base de datos bien sea copiando y pegando o ejecutándolo directamente, pero vamos colocarlo dentro de un directorio llamado database en la raíz de nuestro proyecto y a darle el nombre de models.sql por un lado para que tengan acceso al archivo y por otro para que nuestra aplicación pueda leerlo y encargarse de ejecutarlo.

Regresamos al archivo postgres.sql y realizamos algunos cambios. Importamos el paquete ioutil de la biblioteca estándar, que nos permitirá leer el archivo y agregamos una nueva función a la que llamamos MakeMigration, que se encargara de leer nuestro archivo models.sql y de ejecutar las instrucciones que escribimos en el.

// internal/data/postgres.go
// […]

func MakeMigration(db *sql.DB) error {
    b, err := ioutil.ReadFile("./database/models.sql")
    if err != nil {
        return err
    }

    rows, err := db.Query(string(b))
    if err != nil {
        return err
    }

    return rows.Close()
}

Ahora debemos ejecutar esta función, vamos a hacerlo en la funcion initDB que creamos antes, en el archivo data.go

// internal/data/data.go
// […]
func initDB() {
    db, err := getConnection()
    if err != nil {
        log.Panic(err)
    }

    err = MakeMigration(db)
    if err != nil {
        log.Panic(err)
    }

    data = &Data{
        DB: db,
    }
}
[]

Hecho esto, vamos a compilar y ejecutar nuevamente nuestro programa para comprobar que, hasta este punto, todo este funcionando como esperamos.

go build ./cmd/microblog && ./microblog

Si no tenemos errores, el programa debería compilar, ejecutarse y mostrar el mensaje de que el servidor esta corriendo, similar al siguiente.

2020/05/19 17:28:21 Server running on http://localhost:9000

Y si revisamos la base de datos, deberíamos tener las tablas correctamente creadas.

 Schema |     Name     |   Type   | Owner  
--------+--------------+----------+--------
 public | posts        | table    | gopher
 public | posts_id_seq | sequence | gopher
 public | users        | table    | gopher
 public | users_id_seq | sequence | gopher

Usuarios

Ahora que tenemos nuestras tablas creadas procederemos con las operaciones CRUD para nuestro usuario. Dentro del paquete data crearemos una estructura llamada UserRepository a la que agregaremos los métodos necesarios para satisfacer la interfaz Repository que creamos anteriormente dentro del paquete user.

Esta nueva estructura podemos colocarla donde nos parezca mas conveniente, mientras se encuentre dentro del paquete data. Vamos a crearla en un nuevo archivo llamado user_repository.go porque, en mi opinión, es mas cómodo y organizado.

// internal/data/user_repository.go
package data

type UserRepository struct {
    Data *Data
}

Obtener todos

Comencemos por el método GetAll, que nos permitirá obtener todos los usuarios en la tabla users.

// internal/data/user_repository.go
// […]
func (ur *UserRepository) GetAll(ctx context.Context) ([]user.User, error) {
    q := `
    SELECT id, first_name, last_name, username, email, picture,
        created_at, updated_at
        FROM users;
    `

    rows, err := ur.Data.DB.QueryContext(ctx, q)
    if err != nil {
        return nil, err
    }

    defer rows.Close()

    var users []user.User
    for rows.Next() {
        var u user.User
        rows.Scan(&u.ID, &u.FirstName, &u.LastName, &u.Username,
            &u.Email, &u.Picture, &u.CreatedAt, &u.UpdatedAt)
        users = append(users, u)
    }

    return users, nil
}

En primer lugar, definimos la consulta dentro de la variable que llamamos q, la ejecutamos por medio del método QueryContext, lo que nos retorna un valor de tipo puntero sql.Rows, el cual nos aseguramos de cerrar con su metodo Close gracias a defer. Entonces definimos una variable users que sera donde almacenaremos los usuarios y utilizamos un ciclo for en conjunto con el método Next, que retorna un Bool que sera true mientras existan filas por leer.

Dentro de este ciclo definimos una variable llama u de tipo user.User donde almacenaremos la información de cada usuario leído mediante el método Scan del tipo sql.Rows para posteriormente agregarlo a users. Finalmente, retornamos la colección de usuarios y y el valor nil para indicar que el proceso concluyo satisfactoriamente.

Obtener uno

Este método es muy similar al caso anterior, solo que retornamos un único usuario.

// internal/data/user_repository.go
// […]
func (ur *UserRepository) GetOne(ctx context.Context, id uint) (user.User, error) {
    q := `
    SELECT id, first_name, last_name, username, email, picture,
        created_at, updated_at
        FROM users WHERE id = $1;
    `

    row := ur.Data.DB.QueryRowContext(ctx, q, id)

    var u user.User
    err := row.Scan(&u.ID, &u.FirstName, &u.LastName, &u.Username, &u.Email,
        &u.Picture, &u.CreatedAt, &u.UpdatedAt)
    if err != nil {
        return user.User{}, err
    }

    return u, nil
}

No hay mucho que destacar en este fragmento de código, es bastante similar al anterior, con excepción de que, en este caso, vamos a pasar argumentos a la consulta, cosa que indicamos con la sintaxi ${numero correspondiente al orden en el que se pasa el argumento}, esto varia según el motor de base de datos que se este utilizando, en este caso es valido para PostgreSQL, pero en el caso de, por ejemplo, MySQL o SQLite se utiliza ? en su lugar.

Otro detalle a destacar es el uso del método QueryRowContext en lugar de QueryContext que utilizamos anteriormente, esto se debe a que esta consulta solo retornara una fila. Ademas, esta vez estamos pasando como tercer argumento el id del usuario, que tomara el lugar del $1 que definimos dentro del string que almacenamos en q.

Obtener por username

De manera casi idéntica al caso anterior, vamos a retornar un único usuario, solo que esta vez mediante su username en lugar de su id.

// internal/data/user_repository.go
// […]
func (ur *UserRepository) GetByUsername(ctx context.Context, username string) (user.User, error) {
    q := `
    SELECT id, first_name, last_name, username, email, picture,
        password, created_at, updated_at
        FROM users WHERE username = $1;
    `

    row := ur.Data.DB.QueryRowContext(ctx, q, username)

    var u user.User
    err := row.Scan(&u.ID, &u.FirstName, &u.LastName, &u.Username,
        &u.Email, &u.Picture, &u.PasswordHash, &u.CreatedAt, &u.UpdatedAt)
    if err != nil {
        return user.User{}, err
    }

    return u, nil
}

¿Pueden encontrar las diferencias con el método GetOne? 😁. Sino, no se preocupen, son realmente parecidas, la diferencia mas destacable seria el uso del username en lugar del id, otro, quizás menos visible, es que ahora estamos solicitando la columna password en la consulta y, a su vez, ese valor lo estamos almacenando en el campo PasswordHash del usuario.

La razón por la que estamos solicitando el la contraseña esta vez es porque el método GetByUsername sera utilizado mas adelante para manejar el ingreso de usuarios y sera necesario comparar la contraseña con la que tenemos almacenada en la base de datos.

Insertar

Ahora es momento de agregar nuevos usuarios a la tabla, para ese fin preparamos la consulta correspondiente, esta vez utilizando insert into.

// internal/data/user_repository.go
// […]
func (ur *UserRepository) Create(ctx context.Context, u *user.User) error {
    q := `
    INSERT INTO users (first_name, last_name, username, email, picture, password, created_at, updated_at)
        VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
        RETURNING id;
    `

    if u.Picture == "" {
        u.Picture = "https://placekitten.com/g/300/300"
    }

    if err := u.HashPassword(); err != nil {
        return err
    }

    row := ur.Data.DB.QueryRowContext(
        ctx, q, u.FirstName, u.LastName, u.Username, u.Email,
        u.Picture, u.PasswordHash, time.Now(), time.Now(),
    )

    err := row.Scan(&u.ID)
    if err != nil {
        return err
    }

    return nil
}

Realizamos una pequeña validación para agregar una imagen por defecto si el usuario no incluye una, en este caso un simpático gato desde placekitten. Generamos el PasswordHash gracias al método HashPassword que creamos en el primer articulo y ejecutamos la sentencia para, finalmente, leer el valor del id que le fue asignado al nuevo usuario ya que lo usaremos mas adelante.

Actualizar

Ahora que podemos obtener y crear usuarios vamos a agregar un método que nos permita actualizar un usuario existente.

// internal/data/user_repository.go
// […]
func (ur *UserRepository) Update(ctx context.Context, id uint, u user.User) error {
    q := `
    UPDATE users set first_name=$1, last_name=$2, email=$3, picture=$4, updated_at=$5
        WHERE id=$6;
    `

    stmt, err := ur.Data.DB.PrepareContext(ctx, q)
    if err != nil {
        return err
    }

    defer stmt.Close()

    _, err = stmt.ExecContext(
        ctx, u.FirstName, u.LastName, u.Email,
        u.Picture, time.Now(), id,
    )
    if err != nil {
        return err
    }

    return nil
}

Para actualizar utilizamos la sentencia SQL update y, de esta forma, actualizamos todos los campos del usuarios (con excepción de id, username, password y created_at) que corresponda con el id dado.

Borrar

Ahora es momento de borrar un usuario, la operación restante para completar nuestro CRUD.

// internal/data/user_repository.go
// […]
func (ur *UserRepository) Delete(ctx context.Context, id uint) error {
    q := `DELETE FROM users WHERE id=$1;`

    stmt, err := ur.Data.DB.PrepareContext(ctx, q)
    if err != nil {
        return err
    }

    defer stmt.Close()

    _, err = stmt.ExecContext(ctx, id)
    if err != nil {
        return err
    }

    return nil
}

Este método es uno de los mas sencillos, simplemente usamos la sentencia SQL delete para eliminar al usuario cuyo id corresponda con el dado. La clausula where es muy importante para esto ultimo, de otra forma eliminaríamos todos los usuarios en la tabla y probablemente no es lo que buscamos 😜.

Publicaciones

Ahora es momento del CRUD para las publicaciones. De manera similar al como lo hicimos para user crearemos un archivo llamado post_repository.go dentro del paquete data y lo primero que haremos sera agregar una estructura que llamaremos PostRepository.

// internal/data/post_repository.go
package data

type PostRepository struct {
    Data *Data
}

Para esta estructura, como podrán suponer por el caso anterior, agregaremos los métodos correspondientes para satisfacer la interfaz Repository en el paquete post que definimos en el primer articulo.

Debido a que todo el proceso es prácticamente igual al que realizamos para el caso de los usuarios solo me detendré a explicar el método para obtener todos las publicaciones de un usuario que considero ligeramente diferente a los ya vistos y para los demás casos voy a compartir el código ¿todos a favor?

thumbs up

Obtener todos

// internal/data/post_repository.go
// […]
func (pr *PostRepository) GetAll(ctx context.Context) ([]post.Post, error) {
    q := `
    SELECT id, body, user_id, created_at, updated_at
        FROM posts;
    `

    rows, err := pr.Data.DB.QueryContext(ctx, q)
    if err != nil {
        return nil, err
    }

    defer rows.Close()

    var posts []post.Post
    for rows.Next() {
        var p post.Post
        rows.Scan(&p.ID, &p.Body, &p.UserID, &p.CreatedAt, &p.UpdatedAt)
        posts = append(posts, p)
    }

    return posts, nil
}

Obtener uno

// internal/data/post_repository.go
// […]
func (pr *PostRepository) GetOne(ctx context.Context, id uint) (post.Post, error) {
    q := `
    SELECT id, body, user_id, created_at, updated_at
        FROM posts WHERE id = $1;
    `

    row := pr.Data.DB.QueryRowContext(ctx, q, id)

    var p post.Post
    err := row.Scan(&p.ID, &p.Body, &p.UserID, &p.CreatedAt, &p.UpdatedAt)
    if err != nil {
        return post.Post{}, err
    }

    return p, nil
}

Obtener todas las publicaciones de un usuario

Este método es bastante parecido al de GetAll, con la diferencia de que no obtendremos todas las publicaciones en la tabla sino que las filtraremos por el id de un usuario, agregando la clausula SQL where al final de la consulta.

// internal/data/post_repository.go
// […]
func (pr *PostRepository) GetByUser(ctx context.Context, userID uint) ([]post.Post, error) {
    q := `
    SELECT id, body, user_id, created_at, updated_at
        FROM posts
        WHERE user_id = $1;
    `

    rows, err := pr.Data.DB.QueryContext(ctx, q, userID)
    if err != nil {
        return nil, err
    }

    defer rows.Close()

    var posts []post.Post
    for rows.Next() {
        var p post.Post
        rows.Scan(&p.ID, &p.Body, &p.UserID, &p.CreatedAt, &p.UpdatedAt)
        posts = append(posts, p)
    }

    return posts, nil
}

Insertar

// internal/data/post_repository.go
// […]
func (pr *PostRepository) Create(ctx context.Context, p *post.Post) error {
    q := `
    INSERT INTO posts (body, user_id, created_at, updated_at)
        VALUES ($1, $2, $3, $4)
        RETURNING id;
    `

    stmt, err := pr.Data.DB.PrepareContext(ctx, q)
    if err != nil {
        return err
    }

    defer stmt.Close()

    row := stmt.QueryRowContext(ctx, p.Body, p.UserID, time.Now(), time.Now())

    err = row.Scan(&p.ID)
    if err != nil {
        return err
    }

    return nil
}

Actualizar

// internal/data/post_repository.go
// […]
func (pr *PostRepository) Update(ctx context.Context, id uint, p post.Post) error {
    q := `
    UPDATE posts set body=$1, updated_at=$2
        WHERE id=$3;
    `

    stmt, err := pr.Data.DB.PrepareContext(ctx, q)
    if err != nil {
        return err
    }

    defer stmt.Close()

    _, err = stmt.ExecContext(
        ctx, p.Body, time.Now(), id,
    )
    if err != nil {
        return err
    }

    return nil
}

Borrar

// internal/data/post_repository.go
// […]
func (pr *PostRepository) Delete(ctx context.Context, id uint) error {
    q := `DELETE FROM posts WHERE id=$1;`

    stmt, err := pr.Data.DB.PrepareContext(ctx, q)
    if err != nil {
        return err
    }

    defer stmt.Close()

    _, err = stmt.ExecContext(ctx, id)
    if err != nil {
        return err
    }

    return nil
}

Por ahora lo dejaremos hasta aquí, en el siguiente articulo continuaremos con el desarrollo de nuestra API. Les comparto el repositorio con el código del proyecto hasta el momento.

Realmente espero que el contenido les sea de utilidad, y cualquier duda o sugerencia, por favor, déjenla en los comentarios y les responderé tan pronto como me resulte posible. Saludos y gracias por leer.

konosuba

Posted on by:

orlmonteverde profile

Orlando Monteverde

@orlmonteverde

Web developer, gopher, blogger, teacher, open source enthusiast and professional coffee drinker ☕️.

Discussion

markdown guide
 

Buenas, para cuando la parte No. 4? Gracias por tan excelente recurso.

 

Ya disponible la parte 4, mas vale tarde que nunca 🥺.

 

Hola. En primer lugar, gracias a ti por leer este articulo, me alegra que sea de tu agrado. Para este fin de semana si no hay mas inconvenientes, una semana agitada 😅

 

Gracias por compartir tu conocimiento, estaba buscando ejemplos de una api en go, pero no encontraba información concreta.