Prerrequisito: Conocimiento básico del marco de hibernación , conocimiento sobre bases de datos, Java
Hibernate es un marco que proporciona una capa de abstracción, lo que significa que el programador no tiene que preocuparse por las implementaciones, Hibernate hace las implementaciones por usted internamente, como establecer una conexión con la base de datos, escribir consultas para realizar operaciones CRUD, etc. Java Framework que se utiliza para desarrollar la lógica de persistencia. La lógica de persistencia significa almacenar y procesar los datos para un uso prolongado. Más precisamente, Hibernate es un marco Java ORM (Mapeo relacional de objetos) de código abierto, no invasivo y liviano para desarrollar objetos que son independientes del software de la base de datos y hacer una lógica de persistencia independiente en todo JAVA, JEE.
¿Qué es el mapeo uno a uno?
Uno a uno representa que una sola entidad está asociada con una sola instancia de la otra entidad. Una instancia de una entidad de origen se puede asignar como máximo a una instancia de la entidad de destino. Tenemos muchos ejemplos a nuestro alrededor que demuestran este mapeo uno a uno.
- Una persona tiene un pasaporte, un pasaporte está asociado con una sola persona.
- Los leopardos tienen manchas únicas, un patrón de manchas está asociado con un solo leopardo.
- Tenemos una identificación universitaria, una identificación universitaria está asociada de manera única con una persona.
Puedes encontrar más ejemplos de la vida cotidiana si observas. En los sistemas de gestión de bases de datos, el mapeo uno a uno es de dos tipos:
- Unidireccional uno a uno
- Bidireccional uno a uno
Unidireccional uno a uno
En este tipo de asignación, una entidad tiene una propiedad o una columna que hace referencia a una propiedad o columna en la entidad de destino. Veamos esto con la ayuda de un ejemplo:
En este ejemplo, la tabla de estudiantes se asocia con student_gfg_detail con la ayuda de una clave externa student_gfg_detail_id que hace referencia a student_gfg_detail.id . La entidad de destino ( student_gfg_detail ) no tiene forma de asociarse con la tabla de estudiantes , pero la tabla de estudiantes puede acceder a student_gfg_table con la ayuda de una clave externa. La relación anterior se puede generar con la ayuda del siguiente script SQL.
DROP SCHEMA IF EXISTS `hb-one-to-one-mapping`; CREATE SCHEMA `hb-one-to-one-mapping`; use `hb-one-to-one-mapping`; SET FOREIGN_KEY_CHECKS = 0; DROP TABLE IF EXISTS `student_gfg_detail`; -- ----------------------------------------------------- -- Table `hb-one-to-one-mapping`.`student_gfg_detail` -- ----------------------------------------------------- CREATE TABLE `student_gfg_detail` ( `id` INT NOT NULL AUTO_INCREMENT, `college` varchar(128) DEFAULT NULL, `no_of_problems_solved` INT DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; DROP TABLE IF EXISTS `instructor`; -- ----------------------------------------------------- -- Table `hb-one-to-one-mapping`.`student` -- ----------------------------------------------------- CREATE TABLE `hb-one-to-one-mapping`.`student` ( `id` INT NOT NULL AUTO_INCREMENT, `first_name` VARCHAR(45) NULL DEFAULT NULL, `last_name` VARCHAR(45) NULL DEFAULT NULL, `email` VARCHAR(45) NULL DEFAULT NULL, `student_gfg_detail_id` INT UNIQUE, PRIMARY KEY (`id`), FOREIGN KEY (`student_gfg_detail_id`) REFERENCES `hb-one-to-one-mapping`.`student_gfg_detail` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION) ENGINE = InnoDB DEFAULT CHARACTER SET = latin1; SET FOREIGN_KEY_CHECKS = 1;
Al crear la tabla de estudiantes, hacemos referencia a la clave principal en la tabla student_gfg_detail , es decir, student_gfg_detail.id. Hemos establecido ON DELETE NO ACTION y ON DELETE NO ACTION deliberadamente, ya que estableceremos estos valores dentro de Hibernate. Ahora agreguemos entradas a la base de datos con la ayuda de Hibernate. Primero, definamos nuestro archivo de configuración de hibernación.
XML
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <!-- JDBC Database connection settings --> <property name="connection.driver_class">com.mysql.cj.jdbc.Driver</property> <property name="connection.url"> jdbc:mysql://localhost:3306/hb-one-to-one-mapping?useSSL=false&allowPublicKeyRetrieval=true </property> <property name="connection.username">your_username</property> <property name="connection.password">your_password</property> <!-- JDBC connection pool settings ... using built-in test pool --> <property name="connection.pool_size">1</property> <!-- Select our SQL dialect --> <property name="dialect">org.hibernate.dialect.MySQLDialect</property> <!-- Echo the SQL to stdout --> <property name="show_sql">true</property> <!-- Set the current session context --> <property name="current_session_context_class">thread</property> </session-factory> </hibernate-configuration>
Usaremos MySQL como controlador JDBC. También puede utilizar la implementación de cualquier otro proveedor. En el archivo de configuración, configuramos la fábrica de sesiones con la configuración de conexión de la base de datos, la configuración del grupo de conexiones, el dialecto SQL, etc. Una propiedad importante a tener en cuenta aquí es current_session_context_class . La mayoría de las aplicaciones que utilizan Hibernate necesitan algún tipo de sesión «contextual», en la que una sesión determinada está en vigor en todo el ámbito de un contexto determinado. Aquí, esta propiedad especifica que la sesión actual está en el contexto de un solo hilo. Además, no olvide cambiar your_username con el nombre de usuario de su base de datos y your_password con su contraseña de base de datos.
Ahora necesitamos agregar el archivo JAR de hibernación y el conector de la base de datos MySQL a nuestras bibliotecas externas. Usaré maven para agregar esos archivos JAR, pero también puede descargarlos de las fuentes oficiales y agregarlos manualmente a sus bibliotecas externas. Definamos nuestro archivo pom.xml
XML
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.geeksforgeeks</groupId> <artifactId>Hibernate-One-to-One-Mapping</artifactId> <version>1.0</version> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-vibur</artifactId> <version>5.6.5.Final</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> </dependencies> </project>
Ahora que hemos agregado las dependencias relacionadas, comencemos con la definición de nuestras entidades.
Java
package com.geeksforgeeks.entity; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.OneToOne; import javax.persistence.Table; @Entity @Table(name = "student") public class Student { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private int id; @Column(name = "first_name") private String firstName; @Column(name = "last_name") private String lastName; @Column(name = "email") private String email; @OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "student_gfg_detail_id") private StudentGfgDetail studentGfgDetail; public Student() {} public Student(String firstName, String lastName, String email) { this.firstName = firstName; this.lastName = lastName; this.email = email; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public StudentGfgDetail getStudentGfgDetail() { return studentGfgDetail; } public void setStudentGfgDetail(StudentGfgDetail studentGfgDetail) { this.studentGfgDetail = studentGfgDetail; } @Override public String toString() { return "Student{" + "id=" + id + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", email='" + email + '\'' + ", studentGfgDetail=" + studentGfgDetail + '}'; } }
Java
package com.geeksforgeeks.entity; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "student_gfg_detail") public class StudentGfgDetail { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private int id; @Column(name = "college") private String college; @Column(name = "no_of_problems_solved") private int noOfProblemsSolved; public StudentGfgDetail() {} public StudentGfgDetail(String college, int noOfProblemsSolved) { this.college = college; this.noOfProblemsSolved = noOfProblemsSolved; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getCollege() { return college; } public void setCollege(String college) { this.college = college; } public int getNoOfProblemsSolved() { return noOfProblemsSolved; } public void setNoOfProblemsSolved(int noOfProblemsSolved) { this.noOfProblemsSolved = noOfProblemsSolved; } @Override public String toString() { return "StudentGfgDetail{" + "id=" + id + ", college='" + college + '\'' + ", noOfProblemsSolved=" + noOfProblemsSolved + '}'; } }
Todos los marcos de trabajo más recientes en estos días dependen en gran medida de las anotaciones, ya que hacen que el código sea más corto y más fácil de entender, comprendamos lo que está sucediendo paso a paso. A medida que almacenamos el objeto Student y los objetos StudentGfgDetail en nuestra base de datos, los anotamos con @Entity . La anotación @Table especifica la tabla principal asociada con la entidad, pasamos el nombre de la tabla dentro de la anotación.
- @Id : especifica que el campo dado es la clave principal de la entidad.
- @GeneratedValue : esto define la estrategia para generar la clave principal, aquí usamos GenerationType.IDENTITY , que incrementa automáticamente el valor de Id, si no queremos usar esto, tendríamos que especificar la clave principal explícitamente cada vez que creamos un objeto.
- @Column : esto simplemente asigna un campo dado a una columna dentro de la base de datos.
La clave aquí a tener en cuenta es la siguiente parte:
@OneToOne(cascade = CascadeType.ALL) @JoinColumn(name = "student_gfg_detail_id") private StudentGfgDetail studentGfgDetail;
Agregamos un solo objeto de StudentGfgDetail dentro de la clase Student que se anota con la anotación @OneToOne que especifica la asignación uno a uno. Esta anotación contiene un elemento llamado cascada que especifica la estrategia de cascada. La conexión en cascada es una función que se utiliza para administrar el estado de la entidad de destino cada vez que cambia el estado de la entidad principal. Los tipos básicos de cascada de Hibernate son:
- CascadeType.ALL : propaga todas las operaciones de la entidad principal a la de destino.
- CascadeType.PERSIST : propaga la persistencia de la entidad principal a la de destino.
- CascadeType.MERGE : propaga la fusión de la entidad principal a la de destino.
- CascadeType.REMOVE : propaga la eliminación de la entidad principal a la de destino.
- CascadeType.REFRESH : propaga la actualización de la entidad principal a la de destino.
- CascadeType.DETACH : propaga la separación de la entidad principal a la de destino.
Por ejemplo, si cascade = CascadeType.REMOVE , si la entidad principal se elimina de la base de datos, la entidad de destino también se eliminará de la base de datos, es decir, si se elimina una entidad de Estudiante de la base de datos, el StudentGfgDetail relacionado se eliminará automáticamente en el mismo operación. La anotación @JoinColumn especifica la columna para unir la entidad de destino. Aquí se especifica que StudentGfgDetail debe unirse a la columna student_gfg_detail_id , lo cual es correcto ya que student_gfg_detail_id actúa como nuestra clave externa.
Esto establece una relación unidireccional de uno a uno, ya que el estudiante puede acceder a la entidad StudentGfgDetail pero no es así al revés. Ahora, una vez que hayamos terminado de definir nuestras entidades, entremos en acción real, modifiquemos nuestra base de datos con la ayuda de hibernate. Agreguemos una entrada a nuestra base de datos-
Java
package com.geeksforgeeks.application; import com.geeksforgeeks.entity.Student; import com.geeksforgeeks.entity.StudentGfgDetail; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; public class AddingEntryDemo { public static void main(String[] args) { // Create session factory SessionFactory factory = new Configuration() .configure("hibernate.cfg.xml") .addAnnotatedClass(Student.class) .addAnnotatedClass(StudentGfgDetail.class) .buildSessionFactory(); // Create session try (factory; Session session = factory.getCurrentSession()) { // Get the current session // Create relevant object. Student student = new Student("Vyom", "Yadav", "vyom@gmail.com"); StudentGfgDetail studentGfgDetail = new StudentGfgDetail("GFG College", 20); student.setStudentGfgDetail(studentGfgDetail); // Begin the transaction session.beginTransaction(); // Save the student object. // This will also save the StudentGfgDetail // object as we have used CascadeType.ALL session.save(student); // Commit the transaction session.getTransaction().commit(); System.out.println( "Transaction Successfully Completed!"); } catch (Exception e) { e.printStackTrace(); } } }
Ahora analicemos esto paso a paso.
- Creamos la fábrica de sesiones con la ayuda del archivo de configuración, ya que habíamos configurado los detalles en el archivo de configuración. Luego agregamos clases anotadas, básicamente clases que anotamos en los pasos anteriores, y luego simplemente construimos la fábrica de sesiones.
- Obtenga la sesión actual de la fábrica de sesiones.
- Cree objetos relevantes y métodos de establecimiento de llamadas.
- Comience la transacción.
- Guarde el objeto Student , esto también guarda el StudentGfgDetail asociado como usamos CascadeType.ALL en los pasos anteriores.
- Obtenga la transacción y confirme los cambios.
- Cierra la sesión actual.
- Cierra la fábrica de sesiones ya que no queremos crear más sesiones.
Si ejecutamos el código anterior con la configuración adecuada, deberíamos ver un resultado similar:
Es posible que la salida no sea 100% igual, ya que depende de la versión de hibernación que esté utilizando, pero debería ser muy similar a la salida anterior. Verifiquemos dos veces si esos valores se insertaron en la base de datos o no.
Ahora que hemos verificado que pudimos agregar una entrada a la base de datos y que nuestras operaciones estaban funcionando, intentemos actualizar estas entradas.
Java
package com.geeksforgeeks.application; import com.geeksforgeeks.entity.Student; import com.geeksforgeeks.entity.StudentGfgDetail; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; public class UpdateEntryDemo { public static void main(String[] args) { // Create session factory SessionFactory factory = new Configuration() .configure("hibernate.cfg.xml") .addAnnotatedClass(Student.class) .addAnnotatedClass(StudentGfgDetail.class) .buildSessionFactory(); // Create session try (factory; Session session = factory.getCurrentSession()) { // Begin the transaction session.beginTransaction(); // Get object with id = 1 int id = 1; Student student = session.get(Student.class, id); StudentGfgDetail studentGfgDetail = student.getStudentGfgDetail(); // modify the student and its details student.setEmail("vyom@geeksforgeeks.com"); studentGfgDetail.setNoOfProblemsSolved(40); // Update the student object. // This will also update the StudentGfgDetail // object as we have used CascadeType.ALL session.update(student); // Commit the transaction session.getTransaction().commit(); System.out.println( "Transaction Successfully Completed!"); } catch (Exception e) { e.printStackTrace(); } } }
La única pieza de código que necesitamos entender es-
// Get object with id = 1 int id = 1; Student student = session.get(Student.class, id); StudentGfgDetail studentGfgDetail = student.getStudentGfgDetail(); // modify the student and its details student.setEmail("vyom@geeksforgeeks.com"); studentGfgDetail.setNoOfProblemsSolved(40); // Update the student object. // This will also update the StudentGfgDetail object as we have used CascadeType.ALL session.update(student);
- Obtenemos el estudiante con id = 1.
- Modificar los detalles.
- Actualice el valor.
Una cosa a tener en cuenta aquí es que debemos estar adentro para usar el método get (Student.class, id) , es por eso que este fragmento de código viene después de que comenzamos la transacción. La salida si ejecutamos el código con la configuración adecuada-
Nuevamente, la salida depende de la versión de hibernación, pero debería ser similar. También podemos verificar estos cambios dentro de la base de datos.
La lectura de un valor de una base de datos también se puede hacer muy fácilmente, simplemente modifique la sección debajo de comenzar la transacción como-
// Get object with id = 1 int id = 1; Student student = session.get(Student.class, id); StudentGfgDetail studentGfgDetail = student.getStudentGfgDetail(); System.out.println(student); System.out.println(studentGfgDetail); // Commit the transaction session.getTransaction().commit(); // close the session session.close();
Podríamos imprimir los datos en el flujo de salida estándar o en un archivo, según nuestras necesidades. Tenga en cuenta que no es necesario guardar, ya que no estamos modificando los datos, simplemente los estamos leyendo. La eliminación también se puede hacer de manera similar-
// Get object with id = 1 int id = 1; Student student = session.get(Student.class, id); // Delete the student, this will also delete the associated entity // as we use CascadeType.ALL session.delete(student); // Commit the transaction session.getTransaction().commit(); // close the session session.close();
Tenga en cuenta que la entidad StudentGfgDetail asociada también se eliminará ya que estamos usando CascadeType.ALL
Bidireccional uno a uno
Hasta ahora la relación era unidireccional, es decir, podíamos acceder a StudentGfgDetail desde la entidad Student pero no al revés, pero ¿por qué querríamos tener una relación bidireccional y cuál es el problema con una relación unidireccional ?
Problema:
Si de alguna manera simplemente eliminamos la entidad StudentGfgDetail y dejamos la entidad Student tal como está, entonces la entidad Student tendrá una clave externa que se refiere a un objeto inexistente que presenta el problema de la clave externa colgante que es, por supuesto, una mala práctica. . La opción de eliminar la entidad Student cuando se elimina la entidad StudentGfgDetail depende del diseño de la base de datos, es posible que queramos mantener la entidad Student como un registro de los usuarios que abandonaron la comunidad o simplemente eliminarla. Afortunadamente, podemos lograr las dos cosas mencionadas anteriormente sin modificar nuestra base de datos y solo con la ayuda de Hibernate, simplemente modifique StudentGfgDetail.java-
Java
package com.geeksforgeeks.entity; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToOne; import javax.persistence.Table; @Entity @Table(name = "student_gfg_detail") public class StudentGfgDetail { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private int id; @Column(name = "college") private String college; @Column(name = "no_of_problems_solved") private int noOfProblemsSolved; @OneToOne(mappedBy = "studentGfgDetail", cascade = CascadeType.ALL) private Student student; public StudentGfgDetail() {} public StudentGfgDetail(String college, int noOfProblemsSolved) { this.college = college; this.noOfProblemsSolved = noOfProblemsSolved; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getCollege() { return college; } public void setCollege(String college) { this.college = college; } public int getNoOfProblemsSolved() { return noOfProblemsSolved; } public void setNoOfProblemsSolved(int noOfProblemsSolved) { this.noOfProblemsSolved = noOfProblemsSolved; } public Student getStudent() { return student; } public void setStudent(Student student) { this.student = student; } @Override public String toString() { return "StudentGfgDetail{" + "id=" + id + ", college='" + college + '\'' + ", noOfProblemsSolved=" + noOfProblemsSolved + ", student=" + student + '}'; } }
Lo único que necesitamos cubrir aquí es-
@OneToOne(mappedBy = "studentGfgDetail", cascade = CascadeType.ALL) private Student student;
Ahora que no tenemos una columna que haga referencia a la tabla de estudiantes en nuestra tabla student_gfg_detail , entonces, ¿qué es este fragmento de código?
Aquí es donde Hibernate nos ayuda, mappedBy = “ studentGfgDetail” le dice a Hibernate que busque un campo llamado studentGfgDetail en la clase Student y vincule esa instancia particular al objeto estudiante actual . Ahora que hemos entendido el vínculo, intentemos agregar una entrada a la base de datos, pero esta vez guardaremos el objeto StudentGfgDetail explícitamente, lo que implícitamente también guardará el objeto Student relacionado debido a CascadeType.ALL.
Java
package com.geeksforgeeks.application; import com.geeksforgeeks.entity.Student; import com.geeksforgeeks.entity.StudentGfgDetail; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; public class AddEntryBidirectionalDemo { public static void main(String[] args) { // Create session factory SessionFactory factory = new Configuration() .configure("hibernate.cfg.xml") .addAnnotatedClass(Student.class) .addAnnotatedClass(StudentGfgDetail.class) .buildSessionFactory(); // Create session try (factory; Session session = factory.getCurrentSession()) { // Create relevant object. Student student = new Student("JJ", "Olatunji", "jj@gmail.com"); StudentGfgDetail studentGfgDetail = new StudentGfgDetail("GFG College", 0); student.setStudentGfgDetail(studentGfgDetail); studentGfgDetail.setStudent(student); // Begin the transaction session.beginTransaction(); // Save the studentGfgDetail object. // This will also save the student object as we // have used CascadeType.ALL session.save(studentGfgDetail); // Commit the transaction session.getTransaction().commit(); System.out.println( "Transaction Successfully Completed!"); } catch (Exception e) { e.printStackTrace(); } } }
Tenga en cuenta que todavía no tenemos ninguna clave que haga referencia al estudiante en la tabla student_gfg_detail , podemos guardarla a través del objeto StudentGfgDetail .
Producción:
Podemos ver que hibernate hizo dos inserciones, por lo que sabemos que ambos valores se insertaron con éxito, ahora intentemos leer estos valores simplemente recuperando el objeto StudentGfgDetail .
Java
package com.geeksforgeeks.application; import com.geeksforgeeks.entity.Student; import com.geeksforgeeks.entity.StudentGfgDetail; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; public class ReadEntryBidirectionalDemo { public static void main(String[] args) { // Create session factory SessionFactory factory = new Configuration() .configure("hibernate.cfg.xml") .addAnnotatedClass(Student.class) .addAnnotatedClass(StudentGfgDetail.class) .buildSessionFactory(); // Create session try (factory; Session session = factory.getCurrentSession()) { // Begin the transaction session.beginTransaction(); int theId = 5; StudentGfgDetail studentGfgDetail = session.get( StudentGfgDetail.class, theId); System.out.println( studentGfgDetail.getStudent()); System.out.println(studentGfgDetail); // Commit the transaction session.getTransaction().commit(); System.out.println( "Transaction Successfully Completed!"); } catch (Exception e) { e.printStackTrace(); } } }
Como podemos ver, acabamos de recuperar el objeto StudentGfgDetail , y ese objeto a su vez recupera implícitamente el objeto Student .
Producción:
Nota: Antes de recuperar cualquier elemento de la base de datos, verifique dos veces la ID del estudiante o StudentGfgDetail, ya que puede variar en diferentes sistemas debido a la cantidad de ejecuciones.
Podemos actualizar y eliminar los elementos, de la misma manera que lo hicimos en unidireccional, pero ¡cuidado, ahora actualizar o eliminar cualquier entidad también eliminará la entidad asociada!
Publicación traducida automáticamente
Artículo escrito por jackhammervyom y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA