Frank Condezo

Implementación del patrón active record

Active record es un patrón de diseño que define una forma de acceder a los datos de una base de datos relacional y convertir las filas de una tabla en objetos.

Active Record fue descrito por Martin Fowler en su libro Patterns of Enterprise Application Architecture.

Un objeto transporta tanto datos como comportamiento. Gran parte de estos datos son persistentes y deben almacenarse en una base de datos. Active Record utiliza el enfoque más obvio, poniendo la lógica de acceso a los datos en el objeto de dominio.

En otras palabras, los objetos debe incluir funciones como por ejemplo insertar (CREATE), leer(READ), actualizar (UPDATE), eliminar (DELETE) y propiedades que correspondan de cierta manera directamente a las columnas de la base de datos asociada.

Este patrón de persistencia es quizás uno de los más habituales y es usado por frameworks como Rails(active record) o Laravel(eloquent).

En este post no vamos a usar una base de datos real, si no archivos JSON para mostrarte lo increíble que es el patron Active Record y para ello usaremos como lenguage de programación al gran y confiable ruby.

Iniciar el proyecto

Los archivos iniciales que vamos a necesitar para el proyecto son:

  • Movie.rb
  • main.rb
  • movies.json

Primero vamos a crear un archivo ruby Movie.rb y un archivo json movies.json en donde almacenaremos nuestras películas(este archivo inicialmente solo tiene un [] como contenido).

En Movie.rb creamos una clase(que se comportará como un modelo), los atributos propios de Movie y unos cuantos métodos que nos permitiran acceder a nuestro archivo movies.json. Tambien importamos la librería json para poder parsear archivos json en hash y guardar jsons a partir de hashs :

require 'json'

class Movie
  @@file = 'movies.json'

  attr_accessor :id, :name, :description, :date, :genders

  def initialize(id = "", name = "", description = "", date = "", genders = [] )
    @id = id
    @name = name
    @description = description
    @date = date
    @genders = genders
  end

  def self.read
    JSON.parse(File.read(@@file))
  end

  def self.save_data_to_json(data)
    File.write(@@file, data)
  end
end

Nota: Todos los métodos que comienzen con la palabra self son métodos estáticos

Ahora en nuestra clase Movie vamos a crear un metodo que guarde los atributos del objeto(CREATE)

  def save()
    movies = Movie.read_json
    movies << ({
      name: @name,
      description: @description,
      date: @date,
      genders: @genders,
      id: (Time.now.to_f.round(2) * 100).to_i # generate fake id
    })
    Movie.save_data_to_json(movies.to_json)
  end

Nota: Cuando queremos usar un método estático dentro de un metodo normal, debemos hacer referencia al metodo con el nombre de la clase Movie y no con self

Ahora necesitamos probar nuestro modelo, para ello creamos un archivo main.rb en la misma carpeta:

require_relative 'Movie'

movie = Movie.new
movie.name = "La chica que saltaba a traves del tiempo"
movie.description = "Una adolescente intenta usar a su favor su nueva capacidad para viajar en el tiempo"
movie.date = "09/12/2006"
movie.genders = ["drama", "seinen", "slice of life"]
movie.save()  # save object in movies.json

movie = Movie.new
movie.name = "El origen"
movie.description = "Esta película es una version corta de la película Paprika"
movie.date = "28/07/2010"
movie.genders = ["drama", "suspence", "acción"]
movie.save()

Si revisamos nuestro archivo movies.json debemos ver los objecto guardados.

Ahora en Movie.rb agregamos un método estático llamado all(READ) que nos retornará un array de objetos del tipo Movie:

  def self.all
    json_movies = self.read_json
    json_movies.map do |movie|
      self.new(
        movie["id"],
        movie["name"],
        movie["description"],
        movie["date"],
        movie["genders"]
      )
    end
  end

Usamos un metodo estático para poder llamarlo directamente desde la clase Movie y no crear un objecto de clase cada vez que necesitamos el metodo all

Ahora probamos el metodo all en main.rb para que nos liste todas las películas que han sido guardadas:

require_relative 'Movie'

movies = Movie.all
puts movies.inspect

Ahora vamos a crear un metodo estático que nos devuelva una película por un identificador unico(id) de nuestro archivo movies.json

  def self.find(id)
    movies = self.read_json
    movie = movies.detect{ |movie| movie["id"] == id}
    return nil if movie.nil?
    self.new(
      movie["id"],
      movie["name"],
      movie["description"],
      movie["date"],
      movie["genders"]
    )
  end

Ahora probamos el metodo find en main.rb, usamos un id de cualquier película que ha sido guardada:

require_relative 'Movie'

movie = Movie.find(155395102921)
puts movie.name
puts movie.description

Ahora procedemos a crear un metodo que nos permita actualizar a los registros guardados, para esto solo modificaremos nuestro metodo save:

  def save
    movies = Movie.read_json
    if @id == ""
      movies << ({
        name: @name,
        description: @description,
        date: @date,
        genders: @genders,
        id: (Time.now.to_f.round(2) * 100).to_i # generate fake unique id
      })
    else
      movies = movies.map do |movie|
        movie["id"] == @id ?
          {
            name: @name,
            description: @description,
            date: @date,
            genders: @genders,
            id: @id
          } : movie
      end
    end
    Movie.save_data_to_json(movies.to_json)
  end

Ahora probamos la actualizacion de un registro, primero usamos un id de cualquier película para encontrar a la película y después actualizamos sus atributos:

require_relative 'Movie'

movie = Movie.find(155395102921)
movie.name = "The best title"
movie.description = "Description update"
movie.save() # update movie

Por último vamos a implementar un metodo que nos permita eliminar(DELETE) registros:

  def self.delete(id)
    movies = self.read_json
    movies = movies.select{ |movie| movie["id"] != id}
    Movie.save_data_to_json(movies.to_json)
  end

Ahora probamos el metodo delete en main.rb, usamos un id de cualquier película que ha sido guardada:

require_relative 'Movie'

Movie.delete(155395357010)

puts Movie.all.inspect

Ahora somos capaces de eliminar cualquier película de nuestros registros.

Antes de terminar vamos a refactorizar todo nuestro modelo Movie aplicando el principio DRY:

require 'json'

class Movie
  @@file = 'movies.json'

  attr_accessor :id, :name, :description, :date, :genders

  def initialize(id = "", name = "", description = "", date = "", genders = [] )
    @id = id
    @name = name
    @description = description
    @date = date
    @genders = genders
  end

  def self.read_json
    JSON.parse(File.read(@@file))
  end

  def self.save_data_to_json(data)
    File.write(@@file, data)
  end

  def save
    movies = Movie.read_json
    if @id == ""
      @id = (Time.now.to_f.round(2) * 100).to_i # generate fake id
      movies << Movie.to_hash(self)
    else
      movies = movies.map do |movie|
        movie["id"] == @id ? Movie.to_hash(self) : movie
      end
    end
    Movie.save_data_to_json(movies.to_json)
  end

  def self.all
    json_movies = self.read_json
    json_movies.map do |movie|
      self.to_class(movie)
    end
  end

  def self.find(id)
    movies = self.read_json
    movie = movies.detect{ |movie| movie["id"] == id}
    return nil if movie.nil?
    self.to_class(movie)
  end

  def self.delete(id)
    movies = self.read_json
    movies = movies.select{ |movie| movie["id"] != id}
    Movie.save_data_to_json(movies.to_json)
  end

  def self.to_class(hash = {})
    self.new(hash["id"], hash["name"], hash["description"], hash["date"], hash["genders"])
  end

  def self.to_hash(movie)
    { id: movie.id, name: movie.name, description: movie.description, date: movie.date, genders: movie.genders}
  end

end

Como vemos agregamos dos métodos staticos to_class y to_hash porque ambas funcionalidades se repetian mas de unas vez en nuestro código, ahora tenemos un código reducido y mucho mas simple de leer.

Palabras finales

Existen varios patrones para la persistencia de datos, pero active record destaca por su facilidad de uso. Al usar active record nos aseguramos que nuestra aplicación quede completamente aislada del trabajo con SQL o como en este caso a la manipulación directa de los archivos de nuestra data.