Hoy vamos a ver como replicar estas relaciones entre tablas, las de la imagen, en rails, vamos a crear las tablas, y establecer las relaciones entre ellas. Vamos a tener cuatro tablas:
- nombres
- administradores
- direcciones_ip
- servidores
El objetivo es poder definir las relaciones de forma que un administrador es una persona que tiene nombre y apellidos, ese administrador puede gestionar varios servidores, los cuales a su vez pueden ser gestionados por varios administradores, y cada servidor puede tener varias direcciones IP, pero cada dirección IP sólo puede estar en un servidor y en un interfaz.3
Si nos fijamos bien hay otra relación que no está definida entre tablas que es dirección IP e interfaz, es una relación uno a uno y como tal se puede incluir en una misma tabla, pero para nuestro ejemplo hemos separado administradores y el nombre de los mismos.
Lo primero que vamos a hacer como crear el proyecto, crear la base de datos, dar permisos a un usuario, configurar acceso al mysql en rails y crear las tablas, así que vamos a hacerlo todo seguido.
ubuntu@ronr:~$ rails new gestionips -d mysql ubuntu@ronr:~$ mysql -u root -p mysql> CREATE DATABASE gestionips_development; mysql> GRANT ALL PRIVILEGES ON gestionips_development.* TO 'edu'@'localhost' IDENTIFIED BY 'password'; ubuntu@ronr:~$ vim gestionips/config/database.yml default: &default adapter: mysql2 encoding: utf8 pool: 5 username: edu password: password socket: /var/run/mysqld/mysqld.sock ubuntu@ronr:~/gestionips$ rails generate model nombre ubuntu@ronr:~/gestionips$ rails generate model administrador ubuntu@ronr:~/gestionips$ rails generate model direccion_ip ubuntu@ronr:~/gestionips$ rails generate model servidor ubuntu@ronr:~/gestionips/db/migrate$ vim 20161101142603_create_nombres.rb class CreateNombres < ActiveRecord::Migration[5.0] def change create_table :nombres do |t| t.string("nombre", :limit => 25, :null => false) t.string("apellidos", :limit => 50, :null => false) t.timestamps end end end end ubuntu@ronr:~/gestionips/db/migrate$ vim 20161101142608_create_administradors.rb class CreateAdministradors < ActiveRecord::Migration[5.0] def change create_table :administradors do |t| t.timestamps end end end ubuntu@ronr:~/gestionips/db/migrate$ vim 20161101142620_create_direccion_ips.rb class CreateDireccionIps < ActiveRecord::Migration[5.0] def change create_table :direccion_ips do |t| t.string("ip",:limit => 15, :null => false) t.string("interfaz", :limit => 25, :null => false) t.timestamps end end end ubuntu@ronr:~/gestionips/db/migrate$ vim 20161101142626_create_servidors.rb class CreateServidors < ActiveRecord::Migration[5.0] def change create_table :servidors do |t| t.string("nombre", :limit => 50, :null => false) t.timestamps end end end ubuntu@ronr:~/gestionips$ rake db:migrate
Una vez hecho esto ya podemos concentrarnos en las relaciones, ya que lo único que hemos hecho ha sido crear las tablas vacías con los campos que se indicaban en el diagrama.
Ahora vamos a meter unos datos dummy en la base de datos para empezar a trabajar, para ello desde la consola de rails entrando como rails console desde el directorio raiz del proyecto:
nombre=Nombre.new(:nombre => "Eduardo", :apellidos => "Collado Cabeza") nombre.save nombre=Nombre.new(:nombre => "Maria", :apellidos => "Altamirano García") nombre.save nombre=Nombre.new(:nombre => "Norma", :apellidos => "Delgado Bugarín") nombre.save nombre=Nombre.new(:nombre => "Carlos", :apellidos => "Díaz Cruz") nombre.save direccion=DireccionIp.new(:ip => "12.12.43.12", :interfaz => "eth0") direccion.save direccion=DireccionIp.new(:ip => "56.21.123.14", :interfaz => "eth1") direccion.save direccion=DireccionIp.new(:ip => "121.32.5.7", :interfaz => "eth2") direccion.save direccion=DireccionIp.new(:ip => "173.11.53.122", :interfaz => "eth3") direccion.save servidor=Servidor.new(:nombre => "SRV-Rails1") servidor.save servidor=Servidor.new(:nombre => "SRV-Rails2") servidor.save servidor=Servidor.new(:nombre => "SRV-Rails3") servidor.save servidor=Servidor.new(:nombre => "SRV-Rails4") servidor.save
La tabla de administradores sólo tiene una columna que es ID, por eso no hemos rellenado nada después iremos creando según lo necesitemos, así que ya podemos empezar a trabajar.
Relación uno a uno
Entre las tablas nombres y administradores tenemos una relación uno a uno y vamos a decir que nombre tiene un administrador y que un administrador pertenece a un nombre, así que editamos ambos modelos:
ubuntu@ronr:~/gestionips/app/models$ vim nombre.rb class Nombre < ApplicationRecord has_one :administrador end ubuntu@ronr:~/gestionips/app/models$ vim administrador.rb class Administrador < ApplicationRecord belongs_to :nombre end
En la clase Administrador le hemos dicho que pertenece a nombre, es decir, es una llave foránea a la llave de la tabla nombre, así que tenemos que crear esa columna en la base de datos:
ubuntu@ronr:~/gestionips$ rails generate migration CrearForeignKeyEnAdministradoclass CrearForeignKeyEnAdministrador < ActiveRecord::Migration[5.0] def change add_column("administradors","nombre_id","string") end end
Y ejecutaremos el migrate
ubuntu@ronr:~/gestionips$ rake db:migrate
Si fueramos atrás en versiones y volvieramos tendríamos que volver a introducir los datos dmmy que hemos puesto antes, ya que se borrarían las tablas con drop y al recrearlas lo haría sin contenido.
En este momento entraríamos en la consola de rails para crear las cuatro relaciones uno a uno de nombre y administrador
ubuntu@ronr:~/gestionips$ rails console irb(main):001:0> nombre=Nombre.find(1) Nombre Load (0.4ms) SELECT `nombres`.* FROM `nombres` WHERE `nombres`.`id` = 1 LIMIT 1 => #<Nombre id: 1, nombre: "Eduardo", apellidos: "Collado Cabeza", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 16:54:09"> irb(main):002:0> administrador=Administrador.new() => #<Administrador id: nil, created_at: nil, updated_at: nil, nombre_id: nil> irb(main):003:0> nombre.administrador=administrador Administrador Load (0.4ms) SELECT `administradors`.* FROM `administradors` WHERE `administradors`.`nombre_id` = '1' LIMIT 1 (0.2ms) BEGIN SQL (0.4ms) INSERT INTO `administradors` (`created_at`, `updated_at`, `nombre_id`) VALUES ('2016-11-01 17:00:13', '2016-11-01 17:00:13', '1') (5.3ms) COMMIT => #<Administrador id: 5, created_at: "2016-11-01 17:00:13", updated_at: "2016-11-01 17:00:13", nombre_id: "1"> irb(main):004:0> nombre=Nombre.find(2) Nombre Load (0.5ms) SELECT `nombres`.* FROM `nombres` WHERE `nombres`.`id` = 2 LIMIT 1 => #<Nombre id: 2, nombre: "Maria", apellidos: "Altamirano García", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 16:54:09"> irb(main):005:0> administrador=Administrador.new() => #<Administrador id: nil, created_at: nil, updated_at: nil, nombre_id: nil> irb(main):006:0> nombre.administrador=administrador Administrador Load (0.3ms) SELECT `administradors`.* FROM `administradors` WHERE `administradors`.`nombre_id` = '2' LIMIT 1 (0.1ms) BEGIN SQL (0.2ms) INSERT INTO `administradors` (`created_at`, `updated_at`, `nombre_id`) VALUES ('2016-11-01 17:00:13', '2016-11-01 17:00:13', '2') (12.9ms) COMMIT => #<Administrador id: 6, created_at: "2016-11-01 17:00:13", updated_at: "2016-11-01 17:00:13", nombre_id: "2"> irb(main):007:0> nombre=Nombre.find(3) Nombre Load (0.7ms) SELECT `nombres`.* FROM `nombres` WHERE `nombres`.`id` = 3 LIMIT 1 => #<Nombre id: 3, nombre: "Norma", apellidos: "Delgado Bugarín", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 16:54:09"> irb(main):008:0> administrador=Administrador.new() => #<Administrador id: nil, created_at: nil, updated_at: nil, nombre_id: nil> irb(main):009:0> nombre.administrador=administrador Administrador Load (0.4ms) SELECT `administradors`.* FROM `administradors` WHERE `administradors`.`nombre_id` = '3' LIMIT 1 (0.1ms) BEGIN SQL (0.2ms) INSERT INTO `administradors` (`created_at`, `updated_at`, `nombre_id`) VALUES ('2016-11-01 17:00:13', '2016-11-01 17:00:13', '3') (5.6ms) COMMIT => #<Administrador id: 7, created_at: "2016-11-01 17:00:13", updated_at: "2016-11-01 17:00:13", nombre_id: "3"> irb(main):010:0> nombre=Nombre.find(4) Nombre Load (0.3ms) SELECT `nombres`.* FROM `nombres` WHERE `nombres`.`id` = 4 LIMIT 1 => #<Nombre id: 4, nombre: "Carlos", apellidos: "Díaz Cruz", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 16:54:09"> irb(main):011:0> administrador=Administrador.new() => #<Administrador id: nil, created_at: nil, updated_at: nil, nombre_id: nil> irb(main):012:0> nombre.administrador=administrador Administrador Load (0.5ms) SELECT `administradors`.* FROM `administradors` WHERE `administradors`.`nombre_id` = '4' LIMIT 1 (0.2ms) BEGIN SQL (0.5ms) INSERT INTO `administradors` (`created_at`, `updated_at`, `nombre_id`) VALUES ('2016-11-01 17:00:14', '2016-11-01 17:00:14', '4') (5.2ms) COMMIT => #<Administrador id: 8, created_at: "2016-11-01 17:00:14", updated_at: "2016-11-01 17:00:14", nombre_id: "4">
Se ve más claro si vemos la tabla directamente en MySQL como se ha rellenado la columna nombre_id
mysql> select * from administradors; +----+---------------------+---------------------+-----------+ | id | created_at | updated_at | nombre_id | +----+---------------------+---------------------+-----------+ | 5 | 2016-11-01 17:00:13 | 2016-11-01 17:00:13 | 1 | | 6 | 2016-11-01 17:00:13 | 2016-11-01 17:00:13 | 2 | | 7 | 2016-11-01 17:00:13 | 2016-11-01 17:00:13 | 3 | | 8 | 2016-11-01 17:00:14 | 2016-11-01 17:00:14 | 4 | +----+---------------------+---------------------+-----------+ 4 rows in set (0,00 sec) mysql> select * from nombres; +----+---------+--------------------+---------------------+---------------------+ | id | nombre | apellidos | created_at | updated_at | +----+---------+--------------------+---------------------+---------------------+ | 1 | Eduardo | Collado Cabeza | 2016-11-01 16:54:09 | 2016-11-01 16:54:09 | | 2 | Maria | Altamirano García | 2016-11-01 16:54:09 | 2016-11-01 16:54:09 | | 3 | Norma | Delgado Bugarín | 2016-11-01 16:54:09 | 2016-11-01 16:54:09 | | 4 | Carlos | Díaz Cruz | 2016-11-01 16:54:09 | 2016-11-01 16:54:09 | +----+---------+--------------------+---------------------+---------------------+ 4 rows in set (0,00 sec)
Relación uno a muchos
Ahora vamos a ver la relación entre servidores y direcciones IP. Un servidor puede tener varias direcciones IP, pero cada dirección IP sólo puede estar en un servidor.
Aquí hay que decir que los métodos disponibles son los siguientes:
- servidor.direccion_ip
- servidor.direccion_ip << direccion_ip (añadir al array)
- servidor.direccion_ip = direccion_ip, direccion_ip (introducimos todo el array)
- servidor.direccion_ip.delete(direccion_ip) (borramos el objeto)
- servidor.direccion_ip.destroy(direccion_ip) (borramos el objeto y de la base de datos)
- servidor.direccion_ip.clear (borramos todo)
- servidor.direccion_ip.empty? (preguntamos si está vacío el array)
- servidor.direccion_ip.size (preguntamos el tamaño del array)
Si lo trasladamos a lenguaje Rails tenemos que un servidor has_many :direccionesip
Así procedemos a configurar el fichero
ubuntu@ronr:~/gestionips/app/models$ vim servidor.rb class Servidor < ApplicationRecord has_many :direccion_ip end ubuntu@ronr:~/gestionips/app/models$ vim direccion_ip.rb class DireccionIp < ApplicationRecord belongs_to :servidor end
Y como tenemos belongs_to en direccion_ip significa que tenemos que crear una Foreign Key apuntando a servidor mediante una nueva migración
ubuntu@ronr:~/gestionips$ rails generate migration CrearForeignKeyEnDireccionIp ubuntu@ronr:~/gestionips$ vim db/migrate/20161101172341_crear_foreign_key_en_direccion_ip.rb class CrearForeignKeyEnDireccionIp < ActiveRecord::Migration[5.0] def change add_column("direccion_ips","servidor_id","string") end end ubuntu@ronr:~/gestionips$ rake db:migrate
Y entramos en la consola de rails. Vamos a asignar todas las IPs que tenemos en los datos dummy al primer servidor
ubuntu@ronr:~/gestionips$ rails console Running via Spring preloader in process 7156 Loading development environment (Rails 5.0.0.1) irb(main):001:0> servidor=Servidor.find(1) Servidor Load (0.4ms) SELECT `servidors`.* FROM `servidors` WHERE `servidors`.`id` = 1 LIMIT 1 => #<Servidor id: 1, nombre: "SRV-Rails1", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 16:54:09"> irb(main):002:0> direccionip = DireccionIp.find(1) DireccionIp Load (0.4ms) SELECT `direccion_ips`.* FROM `direccion_ips` WHERE `direccion_ips`.`id` = 1 LIMIT 1 => #<DireccionIp id: 1, ip: "12.12.43.12", interfaz: "eth0", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 16:54:09", servidor_id: nil> irb(main):003:0> servidor.direccion_ip << direccionip (0.3ms) BEGIN SQL (0.6ms) UPDATE `direccion_ips` SET `updated_at` = '2016-11-01 17:29:50', `servidor_id` = '1' WHERE `direccion_ips`.`id` = 1 (4.2ms) COMMIT DireccionIp Load (0.7ms) SELECT `direccion_ips`.* FROM `direccion_ips` WHERE `direccion_ips`.`servidor_id` = '1' => #<ActiveRecord::Associations::CollectionProxy [#<DireccionIp id: 1, ip: "12.12.43.12", interfaz: "eth0", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 17:29:50", servidor_id: "1">]> irb(main):004:0> direccionip = DireccionIp.find(2) DireccionIp Load (0.7ms) SELECT `direccion_ips`.* FROM `direccion_ips` WHERE `direccion_ips`.`id` = 2 LIMIT 1 => #<DireccionIp id: 2, ip: "56.21.123.14", interfaz: "eth1", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 16:54:09", servidor_id: nil> irb(main):005:0> servidor.direccion_ip << direccionip (0.4ms) BEGIN SQL (0.5ms) UPDATE `direccion_ips` SET `updated_at` = '2016-11-01 17:29:58', `servidor_id` = '1' WHERE `direccion_ips`.`id` = 2 (7.9ms) COMMIT => #<ActiveRecord::Associations::CollectionProxy [#<DireccionIp id: 1, ip: "12.12.43.12", interfaz: "eth0", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 17:29:50", servidor_id: "1">, #<DireccionIp id: 2, ip: "56.21.123.14", interfaz: "eth1", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 17:29:58", servidor_id: "1">]> irb(main):006:0> direccionip = DireccionIp.find(3) DireccionIp Load (0.5ms) SELECT `direccion_ips`.* FROM `direccion_ips` WHERE `direccion_ips`.`id` = 3 LIMIT 1 => #<DireccionIp id: 3, ip: "121.32.5.7", interfaz: "eth2", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 16:54:09", servidor_id: nil> irb(main):007:0> servidor.direccion_ip << direccionip (0.2ms) BEGIN SQL (0.6ms) UPDATE `direccion_ips` SET `updated_at` = '2016-11-01 17:30:03', `servidor_id` = '1' WHERE `direccion_ips`.`id` = 3 (7.4ms) COMMIT => #<ActiveRecord::Associations::CollectionProxy [#<DireccionIp id: 1, ip: "12.12.43.12", interfaz: "eth0", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 17:29:50", servidor_id: "1">, #<DireccionIp id: 2, ip: "56.21.123.14", interfaz: "eth1", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 17:29:58", servidor_id: "1">, #<DireccionIp id: 3, ip: "121.32.5.7", interfaz: "eth2", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 17:30:03", servidor_id: "1">]> irb(main):008:0> direccionip = DireccionIp.find(4) DireccionIp Load (0.4ms) SELECT `direccion_ips`.* FROM `direccion_ips` WHERE `direccion_ips`.`id` = 4 LIMIT 1 => #<DireccionIp id: 4, ip: "173.11.53.122", interfaz: "eth3", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 16:54:09", servidor_id: nil> irb(main):009:0> servidor.direccion_ip << direccionip (0.2ms) BEGIN SQL (0.5ms) UPDATE `direccion_ips` SET `updated_at` = '2016-11-01 17:30:08', `servidor_id` = '1' WHERE `direccion_ips`.`id` = 4 (7.6ms) COMMIT => #<ActiveRecord::Associations::CollectionProxy [#<DireccionIp id: 1, ip: "12.12.43.12", interfaz: "eth0", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 17:29:50", servidor_id: "1">, #<DireccionIp id: 2, ip: "56.21.123.14", interfaz: "eth1", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 17:29:58", servidor_id: "1">, #<DireccionIp id: 3, ip: "121.32.5.7", interfaz: "eth2", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 17:30:03", servidor_id: "1">, #<DireccionIp id: 4, ip: "173.11.53.122", interfaz: "eth3", created_at: "2016-11-01 16:54:09", updated_at: "2016-11-01 17:30:08", servidor_id: "1">]>
Y si vemos en MySQL vemos que todas las IPs se han asignado al servidor_id 1, es decir, al primero:
mysql> mysql> select * from direccion_ips; +----+---------------+----------+---------------------+---------------------+-------------+ | id | ip | interfaz | created_at | updated_at | servidor_id | +----+---------------+----------+---------------------+---------------------+-------------+ | 1 | 12.12.43.12 | eth0 | 2016-11-01 16:54:09 | 2016-11-01 17:29:50 | 1 | | 2 | 56.21.123.14 | eth1 | 2016-11-01 16:54:09 | 2016-11-01 17:29:58 | 1 | | 3 | 121.32.5.7 | eth2 | 2016-11-01 16:54:09 | 2016-11-01 17:30:03 | 1 | | 4 | 173.11.53.122 | eth3 | 2016-11-01 16:54:09 | 2016-11-01 17:30:08 | 1 | +----+---------------+----------+---------------------+---------------------+-------------+ 4 rows in set (0,00 sec)
Relación muchos a muchos
Para establecer esta relación y cumplir las formas normales es necesario crear una tabla join intermedia. Esta tabla estará compuesta únicamente por las dos claves foráneas y no tendrá tampoco la clave primaria (id) que crea por defecto Rails, así que habrá que quitársela con :id => false en la migración.
El nombre de la tabla será igual al nombre de las dos tablas unidas por guión y dispuestas por orden alfabético, en nuestro caso será administradors_servidors (el plural este tan raro es el que hace Rails añadiendo simplemente una s)
ubuntu@ronr:~/gestionips$ rails generate migration CreateAdministradors_ServidorsJoin ubuntu@ronr:~/gestionips$ ubuntu@ronr:~/gestionips$ vim db/migrate/20161101175712_create_administradors_servidors_join.rb class CreateAdministradorsServidorsJoin < ActiveRecord::Migration[5.0] def change create_table :administradors_servidors, :id => false do |t| t.integer("administrador_id") t.integer("servidor_id") end add_index :administradors_servidors, ["administrador_id","servidor_id"], :name => 'admin_index' end end ubuntu@ronr:~/gestionips$ rake db:migrate
En el index hemos tenido que añadir
, :name => 'admin_index'
porque nos daba este error en el rake db:migrate
Index name 'index_administradors_servidors_on_administrador_id_and_servidor_id' on table 'administradors_servidors' is too long; the limit is 64 characters /home/ubuntu/gestionips/db/migrate/20161101175712_create_administradors_servidors_join.rb:7:in `change'
Ahora en los dos modeles involucrados tendremos que poner has_and_belongs_to_many :el_otro_modelo.
has_and_belongs_to_many :el_otro_modelo
ubuntu@ronr:~/gestionips/app/models$ vim administrador.rb class Administrador < ApplicationRecord belongs_to :nombre has_and_belongs_to_many :servidor end ubuntu@ronr:~/gestionips/app/models$ vim servidor.rb class Servidor < ApplicationRecord has_many :direccion_ip has_and_belongs_to_many :administrador end
Y ahora vamos introduciendo datos a base de buscar un administrador, un servidor y enlazarlos
irb(main):032:0> administrador=Administrador.find_by_id(6) Administrador Load (0.9ms) SELECT `administradors`.* FROM `administradors` WHERE `administradors`.`id` = 6 LIMIT 1 => #<Administrador id: 6, created_at: "2016-11-01 17:00:13", updated_at: "2016-11-01 17:00:13", nombre_id: "2"> irb(main):033:0> servidor=Servidor.find(4) Servidor Load (0.8ms) SELECT `servidors`.* FROM `servidors` WHERE `servidors`.`id` = 4 LIMIT 1 => #<Servidor id: 4, nombre: "SRV-Rails4", created_at: "2016-11-01 16:54:11", updated_at: "2016-11-01 16:54:11"> irb(main):034:0> administrador.servidor << servidor (0.6ms) BEGIN SQL (0.6ms) INSERT INTO `administradors_servidors` (`administrador_id`, `servidor_id`) VALUES (6, 4) (9.0ms) COMMIT Servidor Load (0.9ms) SELECT `servidors`.* FROM `servidors` INNER JOIN `administradors_servidors` ON `servidors`.`id` = `administradors_servidors`.`servidor_id` WHERE `administradors_servidors`.`administrador_id` = 6 => #<ActiveRecord::Associations::CollectionProxy [#<Servidor id: 4, nombre: "SRV-Rails4", created_at: "2016-11-01 16:54:11", updated_at: "2016-11-01 16:54:11">, #<Servidor id: 4, nombre: "SRV-Rails4", created_at: "2016-11-01 16:54:11", updated_at: "2016-11-01 16:54:11">]>
En MySQL se vería así:
mysql> select * from administradors_servidors; +------------------+-------------+ | administrador_id | servidor_id | +------------------+-------------+ | 5 | 1 | | 5 | 2 | | 5 | 3 | | 6 | 4 | | 7 | 2 | | 7 | 3 | | 7 | 4 | | 8 | 4 | +------------------+-------------+ 8 rows in set (0,00 sec)