Rails y MySQL

Hoy vamos a ver como se crean, modifican y eliminan registros en una tabla de una base de datos Mysql en Rails, obviamente también todo el proceso para tener creada la base de datos.

Lo primero será crear el proyecto de Rails indicando que queremos mysql como motor de bases de datos, eso se hacía con la opción -d y luego mysql.

$ rails new railsymysql -d mysql
create
create README.rdoc
create Rakefile
create config.ru
create .gitignore
[...]
Use `bundle show [gemname]` to see where a bundled gem is installed.
run bundle exec spring binstub --all
* bin/rake: spring inserted
* bin/rails: spring inserted

El proyecto se llamará railsymysql y con esto ya tenemos creada la estructura de ficheros necesaria.

Ahora lo primero será generar un modelo, al que vamos a llamar Usuario (los modelos se generan en singular en Rails)

~/railsmysql$ rails generate model Usuario
Running via Spring preloader in process 465
invoke active_record
create db/migrate/20161030125505_create_usuarios.rb
create app/models/usuario.rb
invoke test_unit
create test/models/usuario_test.rb
create test/fixtures/usuarios.yml

Al crear el modelo creamos el fichero de migración inicial para definir nuestra base de datos y definimos el modelo en si como ActiveRecord

Antes de nada como tenemos vamos a usar MySQL tendremos que crear la base de datos y configurar el fichero /config/database.yml

[...]
default: &default
adapter: mysql2
encoding: utf8
pool: 5
username: edu
password: password
socket: /var/run/mysqld/mysqld.sock

development:
<<: *default
database: railsmysql_development
[...]

En mi caso voy a usar de usuario edu y como contraseña password, así que configuramos esto en el mysql

mysql> CREATE DATABASE railsmysql_development;
Query OK, 1 row affected (0,00 sec)

mysql> GRANT ALL PRIVILEGES ON railsmysql_development.* TO "edu"@"localhost" IDENTIFIED BY "password";
Query OK, 0 rows affected, 1 warning (0,00 sec)

En este punto procedemos a crear una tabla usuarios (plural del modelo) con sus campos, en este caso db/migrate/20161030125505_create_usuarios.rb

class CreateUsuarios < ActiveRecord::Migration
  def change
    create_table :usuarios do |t|
      t.column "nombre", :string, :limit => 25, :null => false
      t.string "apellidos", :limit => 50
      t.string "email", :limit => 40
      t.timestamps null: false
    end
  end
end

Y generaremos la migración

ubuntu@ronr:~/railsmysql$ rake db:migrate
== 20161030125505 CreateUsuarios: migrating ===================================
-- create_table(:usuarios)
-> 0.0474s
== 20161030125505 CreateUsuarios: migrated (0.0475s) ==========================

Y comprobamos en MySQL que se haya aplicado todo bien:

mysql> use railsmysql_development;
Database changed
mysql> show tables;
+----------------------------------+
| Tables_in_railsmysql_development |
+----------------------------------+
| schema_migrations                |
| usuarios                         |
+----------------------------------+
2 rows in set (0,00 sec)

mysql> show columns from usuarios;
+------------+-------------+------+-----+---------+----------------+
| Field      | Type        | Null | Key | Default | Extra          |
+------------+-------------+------+-----+---------+----------------+
| id         | int(11)     | NO   | PRI | NULL    | auto_increment |
| nombre     | varchar(25) | NO   |     | NULL    |                |
| apellidos  | varchar(50) | YES  |     | NULL    |                |
| email      | varchar(40) | YES  |     | NULL    |                |
| created_at | datetime    | NO   |     | NULL    |                |
| updated_at | datetime    | NO   |     | NULL    |                |
+------------+-------------+------+-----+---------+----------------+
6 rows in set (0,00 sec)

Ahora vamos a añadir una nueva columna, que se llame nickname y que vaya después de apellidos

ubuntu@ronr:~/railsmysql$ rails generate migration Nickname
Running via Spring preloader in process 753
invoke active_record
create db/migrate/20161030133824_nickname.rb

Y editamos el fichero de la migración

class Nickname < ActiveRecord::Migration
 def change
 add_column("usuarios","nickname",:string, :limit => 40, :after => "email")
 end
end

Y ejecutamos la migración comprobando que termina, que vuelve al inicio (VERSION=0) y que vuelve al final:

ubuntu@ronr:~/railsmysql$ rake db:migrate
== 20161030133824 Nickname: migrating =========================================
-- add_column("usuarios", "nickname", :string, {:limit=>40, :after=>"email"})
 -> 0.0637s
== 20161030133824 Nickname: migrated (0.0638s) ================================

ubuntu@ronr:~/railsmysql$ rake db:migrate VERSION=0
== 20161030133824 Nickname: reverting =========================================
-- remove_column("usuarios", "nickname", :string, {:limit=>40, :after=>"email"})
 -> 0.0683s
== 20161030133824 Nickname: reverted (0.0705s) ================================

== 20161030125505 CreateUsuarios: reverting ===================================
-- drop_table(:usuarios)
 -> 0.0161s
== 20161030125505 CreateUsuarios: reverted (0.0163s) ==========================

ubuntu@ronr:~/railsmysql$ rake db:migrate
== 20161030125505 CreateUsuarios: migrating ===================================
-- create_table(:usuarios)
 -> 0.0410s
== 20161030125505 CreateUsuarios: migrated (0.0412s) ==========================

== 20161030133824 Nickname: migrating =========================================
-- add_column("usuarios", "nickname", :string, {:limit=>40, :after=>"email"})
 -> 0.0643s
== 20161030133824 Nickname: migrated (0.0644s) ================================

Ahora ya podemos ver como en MySQL se ha creado

mysql> show columns from usuarios;
+------------+-------------+------+-----+---------+----------------+
| Field      | Type        | Null | Key | Default | Extra          |
+------------+-------------+------+-----+---------+----------------+
| id         | int(11)     | NO   | PRI | NULL    | auto_increment |
| nombre     | varchar(25) | NO   |     | NULL    |                |
| apellidos  | varchar(50) | YES  |     | NULL    |                |
| email      | varchar(40) | YES  |     | NULL    |                |
| nickname   | varchar(40) | YES  |     | NULL    |                |
| created_at | datetime    | NO   |     | NULL    |                |
| updated_at | datetime    | NO   |     | NULL    |                |
+------------+-------------+------+-----+---------+----------------+
7 rows in set (0,00 sec)

Una vez creado vamos a editar el campo nickname para que no pueda ser NULL y que sea un índice. Podríamos hacerlo deshaciendo los cambio y volviendo a una versión anterior, nuestro caso podríamos ver el status y luego decidir ir a la versión 20161030125505 y luego editar el fichero de migración, pero sería muy fácil.

ubuntu@ronr:~/railsmysql$ rake db:migrate:status

database: railsmysql_development

Status Migration ID Migration Name
--------------------------------------------------
up 20161030125505 Create usuarios
up 20161030133824 Nickname

Lo que queremos es crear una nueva migración que no sea reversible, es decir, crear una tabla es reversible porque se borra, pero modificar un campo no es directamente reversible ya que hay que indicar el cambio que hay que hacer y ya no nos serviría el método change de la migración teniendo que definir el método up y el método down.

ubuntu@ronr:~/railsmysql$ rails generate migration ModificarNickname
Running via Spring preloader in process 1184
      invoke  active_record
      create    db/migrate/20161030135054_modificar_nickname.rb

Y modificaríamos el fichero de migración cambiando el método change por up y por down, muy importante que down se ejecute de forma simétria a up, primero lo último que hizo up y terminar por lo primero que se ejecutó en el método up.

class ModificarNickname < ActiveRecord::Migration
  def up
    change_column("usuarios","nickname",:string,:limited => 40, :null => false)
    add_index("usuarios","nickname")
  end
  def down
    remove_index("usuarios","nickname")
    change_column("usuarios","nickname",:string,:limited => 40, :null => true)
  end
end

Y comprobamos que todo funciona perfectamente

ubuntu@ronr:~/railsmysql$ rake db:migrate
== 20161030135054 ModificarNickname: migrating ================================
-- change_column("usuarios", "nickname", :string, {:limited=>40, :null=>false})
   -> 0.0636s
-- add_index("usuarios", "nickname")
   -> 0.0381s
== 20161030135054 ModificarNickname: migrated (0.1019s) =======================

ubuntu@ronr:~/railsmysql$ rake db:migrate VERSION=0
== 20161030135054 ModificarNickname: reverting ================================
-- remove_index("usuarios", "nickname")
   -> 0.0228s
-- change_column("usuarios", "nickname", :string, {:limited=>40, :null=>true})
   -> 0.0779s
== 20161030135054 ModificarNickname: reverted (0.1009s) =======================

== 20161030133824 Nickname: reverting =========================================
-- remove_column("usuarios", "nickname", :string, {:limit=>40, :after=>"email"})
   -> 0.0619s
== 20161030133824 Nickname: reverted (0.0651s) ================================

== 20161030125505 CreateUsuarios: reverting ===================================
-- drop_table(:usuarios)
   -> 0.0242s
== 20161030125505 CreateUsuarios: reverted (0.0244s) ==========================

ubuntu@ronr:~/railsmysql$ rake db:migrate
== 20161030125505 CreateUsuarios: migrating ===================================
-- create_table(:usuarios)
   -> 0.0433s
== 20161030125505 CreateUsuarios: migrated (0.0434s) ==========================

== 20161030133824 Nickname: migrating =========================================
-- add_column("usuarios", "nickname", :string, {:limit=>40, :after=>"email"})
   -> 0.0628s
== 20161030133824 Nickname: migrated (0.0629s) ================================

== 20161030135054 ModificarNickname: migrating ================================
-- change_column("usuarios", "nickname", :string, {:limited=>40, :null=>false})
   -> 0.0714s
-- add_index("usuarios", "nickname")
   -> 0.0295s
== 20161030135054 ModificarNickname: migrated (0.1012s) =======================

Ahora ya tenemos la tabla creada en nuestra base de datos y lo que vamos a hacer es jugar con los datos es guardarlos, buscarlos, modificarlos y borrarlos.

Para introducir datos lo que vamos a hacer es crear un objeto instanciado con el modelo Usuario, tal y como lo hemos creado antes.

Para hacer todo esto lo haremos desde la consola de rails. Hay que fijarse que además nos muestra el comando SQL que ejecuta.

ubuntu@ronr:~/railsmysql$ rails console
Running via Spring preloader in process 1371
Loading development environment (Rails 4.2.6)
irb(main):001:0> datos = Usuario.new(:nombre => "Eduardo", :apellidos => "Collado Cabeza", :email => "asd@asd.com", :nickname => "ecollado")
=> #<Usuario id: nil, nombre: "Eduardo", apellidos: "Collado Cabeza", email: "asd@asd.com", nickname: "ecollado", created_at: nil, updated_at: nil>
irb(main):002:0> datos.save
   (0.3ms)  BEGIN
  SQL (0.6ms)  INSERT INTO `usuarios` (`nombre`, `apellidos`, `email`, `nickname`, `created_at`, `updated_at`) VALUES ('Eduardo', 'Collado Cabeza', 'asd@asd.com', 'ecollado', '2016-10-30 14:07:44', '2016-10-30 14:07:44')
   (8.3ms)  COMMIT
=> true

Y en MySQL veremos reflejada la inserción

mysql> mysql> select * from usuarios;
+----+---------+----------------+-------------+----------+---------------------+---------------------+
| id | nombre  | apellidos      | email       | nickname | created_at          | updated_at          |
+----+---------+----------------+-------------+----------+---------------------+---------------------+
|  1 | Eduardo | Collado Cabeza | asd@asd.com | ecollado | 2016-10-30 14:07:44 | 2016-10-30 14:07:44 |
+----+---------+----------------+-------------+----------+---------------------+---------------------+
1 row in set (0,00 sec)

Ahora ya sabemos insertar datos en la base de datos ahora tenemos que buscar, para ello podemos hacerlo con el campo clave, el ID que automáticamente nos ha generado rails o por cualquier campo.

Para bucar por campo ID o usando search_by_xxxx

irb(main):019:0> Usuario.find(4)
  Usuario Load (0.7ms)  SELECT  `usuarios`.* FROM `usuarios` WHERE `usuarios`.`id` = 4 LIMIT 1
=> #<Usuario id: 4, nombre: "Björk", apellidos: "Guðmundsdóttir", email: "bguomindsdf@islandia.com", nickname: "sugarcubes", created_at: "2016-10-30 14:20:00", updated_at: "2016-10-30 14:20:00">
irb(main):020:0> Usuario.find_by_apellidos("Gato")
  Usuario Load (0.8ms)  SELECT  `usuarios`.* FROM `usuarios` WHERE `usuarios`.`apellidos` = 'Gato' LIMIT 1
=> #<Usuario id: 5, nombre: "Isidoro", apellidos: "Gato", email: "elgatoisidoro@gately.com", nickname: "gatelycat", created_at: "2016-10-30 14:21:14", updated_at: "2016-10-30 14:21:14">

Vamos a moficar el apellido a Isidoro Gato, con el id número 5 y le vamos a poner de apellido Gato con Botas en vez de Gato

irb(main):001:0> a_modificar = Usuario.find_by_apellidos("Gato")
  Usuario Load (0.5ms)  SELECT  `usuarios`.* FROM `usuarios` WHERE `usuarios`.`apellidos` = 'Gato' LIMIT 1
=> #<Usuario id: 5, nombre: "Isidoro", apellidos: "Gato", email: "elgatoisidoro@gately.com", nickname: "gatelycat", created_at: "2016-10-30 14:21:14", updated_at: "2016-10-30 14:21:14">
irb(main):002:0> a_modificar.apellidos="Gato con Botas"
=> "Gato con Botas
irb(main):003:0> a_modificar.save
   (0.4ms)  BEGIN
  SQL (1.1ms)  UPDATE `usuarios` SET `apellidos` = 'Gato con Botas', `updated_at` = '2016-10-30 14:29:07' WHERE `usuarios`.`id` = 5
   (6.6ms)  COMMIT
=> true

Y MySQL vemos el cambio

mysql> select * from usuarios;
+----+---------+------------------+--------------------------+------------+---------------------+---------------------+
| id | nombre  | apellidos        | email                    | nickname   | created_at          | updated_at          |
+----+---------+------------------+--------------------------+------------+---------------------+---------------------+
|  1 | Eduardo | Collado Cabeza   | asd@asd.com              | ecollado   | 2016-10-30 14:07:44 | 2016-10-30 14:07:44 |
|  2 | Maria   | Peña Hernandez   | mph@asd.com              | mpena      | 2016-10-30 14:17:38 | 2016-10-30 14:17:38 |
|  3 | Alberto | Smith Garcia     | cholo@jotmeil.com        | cholo      | 2016-10-30 14:18:20 | 2016-10-30 14:18:20 |
|  4 | Björk   | Guðmundsdóttir   | bguomindsdf@islandia.com | sugarcubes | 2016-10-30 14:20:00 | 2016-10-30 14:20:00 |
|  5 | Isidoro | Gato con Botas   | elgatoisidoro@gately.com | gatelycat  | 2016-10-30 14:21:14 | 2016-10-30 14:29:07 |
+----+---------+------------------+--------------------------+------------+---------------------+---------------------+
5 rows in set (0,00 sec)

Aquí el problema es que hemos tenido que hacer tres cosas

  1. Buscar
  2. Modificar
  3. Guardar

Y lo podemos hacer juntando los pasos 2 y 3 mediante update_attributes, vamos a volver a ponerle de apellidos simplemente Gato

irb(main):004:0> a_modificar = Usuario.find_by_apellidos("Gato con Botas")
  Usuario Load (0.7ms)  SELECT  `usuarios`.* FROM `usuarios` WHERE `usuarios`.`apellidos` = 'Gato con Botas' LIMIT 1
=> #<Usuario id: 5, nombre: "Isidoro", apellidos: "Gato con Botas", email: "elgatoisidoro@gately.com", nickname: "gatelycat", created_at: "2016-10-30 14:21:14", updated_at: "2016-10-30 14:29:07">
irb(main):005:0> a_modificar.update_attributes(:apellidos => "Gato")
   (0.3ms)  BEGIN
  SQL (0.7ms)  UPDATE `usuarios` SET `apellidos` = 'Gato', `updated_at` = '2016-10-30 14:31:50' WHERE `usuarios`.`id` = 5
   (8.4ms)  COMMIT
=> true

El resultado en ambos casos sería el mismo, sólo que con update_attributes nos ahorramos el guardar.

Ya sólo nos quedaría borrar un registro, eso lo haremos con destroy, que no es lo mismo que delete, en nuestro caso usaremos siempre destroy.

irb(main):008:0> a_modificar = Usuario.find_by_apellidos("Gato")
  Usuario Load (0.8ms)  SELECT  `usuarios`.* FROM `usuarios` WHERE `usuarios`.`apellidos` = 'Gato' LIMIT 1
=> #<Usuario id: 5, nombre: "Isidoro", apellidos: "Gato", email: "elgatoisidoro@gately.com", nickname: "gatelycat", created_at: "2016-10-30 14:21:14", updated_at: "2016-10-30 14:31:50">
irb(main):009:0> a_modificar.destroy
   (0.1ms)  BEGIN
  SQL (0.4ms)  DELETE FROM `usuarios` WHERE `usuarios`.`id` = 5
   (11.8ms)  COMMIT
=> #<Usuario id: 5, nombre: "Isidoro", apellidos: "Gato", email: "elgatoisidoro@gately.com", nickname: "gatelycat", created_at: "2016-10-30 14:21:14", updated_at: "2016-10-30 14:31:50">

Ya sólo nos quedaría algo para bordar este post, que serían los named scopes, que vendría a ser algo parecido a una macro de SQL que nos permitirá reutilizar código y hacer la lectura y escritura de nuestro código más sencilla.

Los named scopes se llaman como si fueran métodos de ActiveRelation y requieren sintaxis lambda.

   scope :active, lambda {where(:active => true)}

O con parámetros

   scope :with_content_type, lambda {|ctype| where(:content_type => true)}

Se definen en el directorio /app/models dentro de los modelos correspondientes, en nuestro caso definiremos una nueva columna en la base de datos y crearemos un scope para sacar todos aquellos visibles e invisbles:

ubuntu@ronr:~/railsmysql$ rails generate migration Visible
Running via Spring preloader in process 1506
      invoke  active_record
      create    db/migrate/20161030144303_visible.rb
ubuntu@ronr:~/railsmysql$ vim db/migrate/20161030144303_visible.rb

Y configuraremos el fichero de esta manera

class Visible < ActiveRecord::Migration
  def change
    add_column("usuarios","visible", :boolean, :default =>true, :after => "nickname")
  end
end

Luego haremos la migración

ubuntu@ronr:~/railsmysql$ rake db:migrate
== 20161030144303 Visible: migrating ==========================================
-- add_column("usuarios", "visible", :boolean, {:default=>true, :after=>"nickname"})
   -> 0.1167s
== 20161030144303 Visible: migrated (0.1168s) =================================

Y configuraremos en el fichero del modelo, en nuestro caso app/models/usuario.rb

class Usuario < ActiveRecord::Base
        scope :visible, lambda {where(:visible => true)}
        scope :invisible, lambda {where(:visible => false)}
        scope :ordenado, lambda {order("usuarios.id ASC")}
        scope :nuevos_primero, lambda {order("usuarios.id DESC")}
end

Así tendremos por ejemplo:

irb(main):009:0* Usuario.visible
  Usuario Load (0.8ms)  SELECT `usuarios`.* FROM `usuarios` WHERE `usuarios`.`visible` = 1
=> #<ActiveRecord::Relation [#<Usuario id: 1, nombre: "Eduardo", apellidos: "Collado Cabeza", email: "asd@asd.com", nickname: "ecollado", visible: true, created_at: "2016-10-30 14:07:44", updated_at: "2016-10-30 14:07:44">, #<Usuario id: 2, nombre: "Maria", apellidos: "Peña Hernandez", email: "mph@asd.com", nickname: "mpena", visible: true, created_at: "2016-10-30 14:17:38", updated_at: "2016-10-30 14:17:38">, #<Usuario id: 3, nombre: "Alberto", apellidos: "Smith Garcia", email: "cholo@jotmeil.com", nickname: "cholo", visible: true, created_at: "2016-10-30 14:18:20", updated_at: "2016-10-30 14:18:20">, #<Usuario id: 4, nombre: "Björk", apellidos: "Guðmundsdóttir", email: "bguomindsdf@islandia.com", nickname: "sugarcubes", visible: true, created_at: "2016-10-30 14:20:00", updated_at: "2016-10-30 14:20:00">]>
irb(main):010:0> Usuario.invisible
  Usuario Load (0.7ms)  SELECT `usuarios`.* FROM `usuarios` WHERE `usuarios`.`visible` = 0
=> #<ActiveRecord::Relation []>
irb(main):011:0> Usuario.ordenado
  Usuario Load (0.7ms)  SELECT `usuarios`.* FROM `usuarios`  ORDER BY usuarios.id ASC
=> #<ActiveRecord::Relation [#<Usuario id: 1, nombre: "Eduardo", apellidos: "Collado Cabeza", email: "asd@asd.com", nickname: "ecollado", visible: true, created_at: "2016-10-30 14:07:44", updated_at: "2016-10-30 14:07:44">, #<Usuario id: 2, nombre: "Maria", apellidos: "Peña Hernandez", email: "mph@asd.com", nickname: "mpena", visible: true, created_at: "2016-10-30 14:17:38", updated_at: "2016-10-30 14:17:38">, #<Usuario id: 3, nombre: "Alberto", apellidos: "Smith Garcia", email: "cholo@jotmeil.com", nickname: "cholo", visible: true, created_at: "2016-10-30 14:18:20", updated_at: "2016-10-30 14:18:20">, #<Usuario id: 4, nombre: "Björk", apellidos: "Guðmundsdóttir", email: "bguomindsdf@islandia.com", nickname: "sugarcubes", visible: true, created_at: "2016-10-30 14:20:00", updated_at: "2016-10-30 14:20:00">]>
irb(main):012:0> Usuario.nuevos_primero
  Usuario Load (0.7ms)  SELECT `usuarios`.* FROM `usuarios`  ORDER BY usuarios.id DESC
=> #<ActiveRecord::Relation [#<Usuario id: 4, nombre: "Björk", apellidos: "Guðmundsdóttir", email: "bguomindsdf@islandia.com", nickname: "sugarcubes", visible: true, created_at: "2016-10-30 14:20:00", updated_at: "2016-10-30 14:20:00">, #<Usuario id: 3, nombre: "Alberto", apellidos: "Smith Garcia", email: "cholo@jotmeil.com", nickname: "cholo", visible: true, created_at: "2016-10-30 14:18:20", updated_at: "2016-10-30 14:18:20">, #<Usuario id: 2, nombre: "Maria", apellidos: "Peña Hernandez", email: "mph@asd.com", nickname: "mpena", visible: true, created_at: "2016-10-30 14:17:38", updated_at: "2016-10-30 14:17:38">, #<Usuario id: 1, nombre: "Eduardo", apellidos: "Collado Cabeza", email: "asd@asd.com", nickname: "ecollado", visible: true, created_at: "2016-10-30 14:07:44", updated_at: "2016-10-30 14:07:44">]>