Hoy tengo uno de esos casos algo peculiares para los cuales no hay una solución buena o quizás todas sean buenas, nunca se sabe. De lo que estoy seguro es de que hay otras soluciones, y seguro que muchas de ellas mejores, sobre todo, porque yo estoy empezando a trabajar ahora con MyBatis y mis conocimientos sobre este framework no son muy extensos aún. Pero bueno, como ya sabéis el motivo del blog es aprender, y una forma de hacer esto es escribir lo que uno aprende y, posteriormente, revisar las cosas cuando uno adquiere más conocimiento.
Por ponernos en situación, el caso es este: Imaginemos que tenemos una aplicación con tres clases, las cuales extienden unas de otras, de forma que:
- La clase A no extiende de ninguna.
- La clase B extiende a A.
- La clase C extiende a B.
En esta aplicación se entiende como un objeto completo a la clase C, pero a lo largo de la aplicación en determinadas ocasiones solo se requieren los datos que posee A o B, no siendo necesario tener el objeto al completo.
Además de este esquema de clases, en la BBDD, se decidió representar esta jerarquía de clases de igual modo, de forma que tenemos una tabla por cada clase y todas ellas comparten el mismo identificador.
Tras todo esto, se decidió implementar en MyBatis cumpliendo todas las restricciones y necesidades.
Antes de seguir leyendo, este artículo no es un tutorial de MyBatis (probablemente escriba uno, pero no hoy). Este artículo es solo la resolución de un caso muy concreto, otra de estas cosas que surgen y hay que solucionar sin poderse replantear el diseño de la aplicación.
Para ver como resolver esto, y repito, habrá muchas y mejores soluciones, vamos a hacer un pequeño proyecto donde:
- La clase A será la clase “Mammal”. Con una propiedad “age” y un “id”.
- La clase B será la clase “Human”. Con una propiedad “name” y un “id”.
- La clase C será la clase “Gender”. Con una propiedad “gender” y un “id”.
No hagáis mucho caso a los nombre de las clases y centrados en las relaciones.
Para el ejemplo, vamos a utilizar Java y una BBDD PostgreSQL.
Lo primero, es crea un pequeño proyecto maven y añadirle las siguientes dependencias:
<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>org.example</groupId>
<artifactId>MyBatis</artifactId>
<version>1.0</version>
<packaging>war</packaging>
<name>MyBatis</name>
<properties>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.2.7</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.3-1101-jdbc41</version>
</dependency>
</dependencies>
<build>
<finalName>mybatis</finalName>
</build>
</project>
El siguiente paso, si no lo hemos hecho ya, es crear una BBDD para nuestro ejemplo:
drop table if exists mammal;
create table mammal (
id integer primary key,
age integer
);
drop table if exists human;
create table human (
id integer primary key,
name varchar(50)
);
drop table if exists gender;
create table gender (
id integer primary key,
gender varchar(50)
);
Tras esto, vamos a hacer una configuración básica de MyBatis para que conecte con nuestra BBDD:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- Alias generation -->
<typeAliases>
<package name="org.example.model" />
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="UNPOOLED">
<property name="driver" value="org.postgresql.Driver" />
<property name="url" value="jdbc:postgresql:earthlife" />
<property name="username" value="postgres" />
<property name="password" value="toor" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/example/model/MammalMapper.xml" />
<mapper resource="org/example/model/HumanMapper.xml" />
<mapper resource="org/example/model/GenderMapper.xml" />
</mappers>
</configuration>
Y ya nos lanzamos a la implementación.
En primer lugar nuestros objectos:
package org.example.model;
public class Mammal {
private int id;
private int age;
public int getId() {
return this.id;
}
public void setId(int id) {
this.id = id;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
}
package org.example.model;
public class Human extends Mammal {
private int id;
private String name;
@Override
public int getId() {
return this.id;
}
@Override
public void setId(int id) {
this.id = id;
super.setId(id);
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
package org.example.model;
public class Gender extends Human {
private int id;
private String gender;
@Override
public int getId() {
return this.id;
}
@Override
public void setId(int id) {
this.id = id;
super.setId(id);
}
public String getGender() {
return this.gender;
}
public void setGender(String gender) {
this.gender = gender;
}
}
En segundo lugar, los interfaces de los Mappers:
package org.example.mappers;
import java.util.List;
import org.example.model.Mammal;
public interface MammalMapper {
Mammal getMammalById(int id);
List<Mammal> getAllMammals();
int insertMammal(Mammal mammal);
int updateMammal(Mammal mammal);
int deleteMammal(int id);
}
package org.example.mappers;
import java.util.List;
import org.example.model.Human;
public interface HumanMapper {
Human getHumanById(int id);
List<Human> getAllHumans();
int insertHuman(Human human);
int updateHuman(Human human);
int deleteHuman(int id);
Human getHumanMammalById(int id);
List<Human> getAllHumanMammals();
int insertHumanMammal(Human human);
int updateHumanMammal(Human human);
int deleteHumanMammal(int id);
}
package org.example.mappers;
import java.util.List;
import org.example.model.Gender;
public interface GenderMapper {
Gender getGenderById(int id);
List<Gender> getAllGenders();
int insertGender(Gender gender);
int updateGender(Gender gender);
int deleteGender(int id);
Gender getGenderHumanById(int id);
List<Gender> getAllGenderHumans();
int insertGenderHuman(Gender gender);
int updateGenderHuman(Gender gender);
int deleteGenderHuman(int id);
Gender getGenderHumanMammalById(int id);
List<Gender> getAllGenderHumanMammals();
int insertGenderHumanMammal(Gender gender);
int updateGenderHumanMammal(Gender gender);
int deleteGenderHumanMammal(int id);
}
Y, por último, los Mappers en XML:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.example.mappers.MammalMapper" >
<resultMap type="Mammal" id="MammalResultMap">
<id column="id" property="id" jdbcType="INTEGER" />
<result column="age" property="age" jdbcType="INTEGER" />
</resultMap>
<select id="getMammalById" parameterType="int" resultMap="MammalResultMap">
select id,
age
from mammal
where id = #{id}
</select>
<select id="getAllMammals" parameterType="int" resultMap="MammalResultMap">
select id,
age
from mammal
</select>
<insert id="insertMammal" parameterType="Mammal">
insert into mammal
(id, age)
values (#{id, jdbcType=INTEGER}, #{age, jdbcType=INTEGER})
</insert>
<update id="updateMammal" parameterType="Mammal">
update mammal
set age = #{age, jdbcType=INTEGER}
where id = #{id, jdbcType=INTEGER}
</update>
<delete id="deleteMammal" parameterType="int">
delete from mammal
where id = #{id, jdbcType=INTEGER}
</delete>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.example.mappers.HumanMapper" >
<resultMap type="Human" id="HumanResultMap">
<id column="id" property="id" jdbcType="INTEGER" />
<result column="name" property="name" jdbcType="VARCHAR" />
</resultMap>
<resultMap type="Human" id="HumanMammalResultMap">
<id column="id" property="id" jdbcType="INTEGER" />
<result column="age" property="age" jdbcType="INTEGER" />
<result column="name" property="name" jdbcType="VARCHAR" />
</resultMap>
<!-- Human -->
<select id="getHumanById" parameterType="int" resultMap="HumanResultMap">
select id,
name
from human
where id = #{id}
</select>
<select id="getAllHumans" parameterType="int" resultMap="HumanResultMap">
select id,
name
from human
</select>
<insert id="insertHuman" parameterType="Human">
insert into human
(id, name)
values (#{id, jdbcType=INTEGER}, #{name, jdbcType=VARCHAR})
</insert>
<update id="updateHuman" parameterType="Human">
update human
set name = #{name, jdbcType=VARCHAR}
where id = #{id, jdbcType=INTEGER}
</update>
<delete id="deleteHuman" parameterType="int">
delete from human
where id = #{id, jdbcType=INTEGER}
</delete>
<!-- Human + Mammal -->
<select id="getHumanMammalById" parameterType="int" resultMap="HumanMammalResultMap">
select h.id,
m.age,
h.name
from human h
left outer join mammal m on h.id = m.id
where h.id = #{id}
</select>
<select id="getAllHumanMammals" parameterType="int" resultMap="HumanMammalResultMap">
select h.id,
m.age,
h.name
from human
left outer join mammal m on h.id = m.id
</select>
<insert id="insertHumanMammal" parameterType="Human">
insert into mammal
(id, age)
values (#{id, jdbcType=INTEGER}, #{age, jdbcType=INTEGER});
insert into human
(id, name)
values (#{id, jdbcType=INTEGER}, #{name, jdbcType=VARCHAR})
</insert>
<update id="updateHumanMammal" parameterType="Human">
update mammal
set age = #{age, jdbcType=INTEGER}
where id = #{id, jdbcType=INTEGER};
update human
set name = #{name, jdbcType=VARCHAR}
where id = #{id, jdbcType=INTEGER}
</update>
<delete id="deleteHumanMammal" parameterType="int">
delete from human
where id = #{id, jdbcType=INTEGER};
delete from mammal
where id = #{id, jdbcType=INTEGER}
</delete>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.example.mappers.GenderMapper" >
<resultMap type="Gender" id="GenderResultMap">
<id column="id" property="id" jdbcType="INTEGER" />
<result column="gender" property="gender" jdbcType="VARCHAR" />
</resultMap>
<resultMap type="Gender" id="GenderHumanResultMap">
<id column="id" property="id" jdbcType="INTEGER" />
<result column="name" property="name" jdbcType="VARCHAR" />
<result column="gender" property="gender" jdbcType="VARCHAR" />
</resultMap>
<resultMap type="Gender" id="GenderHumanMammalResultMap">
<id column="id" property="id" jdbcType="INTEGER" />
<result column="age" property="age" jdbcType="INTEGER" />
<result column="name" property="name" jdbcType="VARCHAR" />
<result column="gender" property="gender" jdbcType="VARCHAR" />
</resultMap>
<!-- Gender -->
<select id="getGenderById" parameterType="int" resultMap="GenderResultMap">
select id,
gender
from gender
where id = #{id}
</select>
<select id="getAllGenders" parameterType="int" resultMap="GenderResultMap">
select id,
gender
from gender
</select>
<insert id="insertGender" parameterType="Human">
insert into gender
(id, gender)
values (#{id, jdbcType=INTEGER}, #{gender, jdbcType=VARCHAR})
</insert>
<update id="updateGender" parameterType="Human">
update gender
set gender = #{gender, jdbcType=VARCHAR}
where id = #{id, jdbcType=INTEGER}
</update>
<delete id="deleteGender" parameterType="int">
delete from gender
where id = #{id, jdbcType=INTEGER}
</delete>
<!-- Gender + Human -->
<select id="getGenderHumanById" parameterType="int" resultMap="GenderHumanResultMap">
select g.id,
h.name,
g.gender
from gender g
left outer join human h on g.id = h.id
where g.id = #{id}
</select>
<select id="getAllGenderHumans" parameterType="int" resultMap="GenderHumanResultMap">
select g.id,
h.name,
g.gender
from gender g
left outer join human h on g.id = h.id
</select>
<insert id="insertGenderHuman" parameterType="Gender">
insert into human
(id, name)
values (#{id, jdbcType=INTEGER}, #{name, jdbcType=VARCHAR});
insert into gender
(id, gender)
values (#{id, jdbcType=INTEGER}, #{gender, jdbcType=VARCHAR})
</insert>
<update id="updateGenderHuman" parameterType="Gender">
update human
set name = #{name, jdbcType=VARCHAR}
where id = #{id, jdbcType=INTEGER};
update gender
set gender = #{gender, jdbcType=VARCHAR}
where id = #{id, jdbcType=INTEGER}
</update>
<delete id="deleteGenderHuman" parameterType="int">
delete from gender
where id = #{id, jdbcType=INTEGER};
delete from human
where id = #{id, jdbcType=INTEGER}
</delete>
<!-- Gender + Human + Mammal -->
<select id="getGenderHumanMammalById" parameterType="int" resultMap="GenderHumanMammalResultMap">
select g.id,
m.age,
h.name,
g.gender
from gender g
left outer join human h on g.id = h.id
left outer join mammal m on g.id = m.id
where g.id = #{id}
</select>
<select id="getAllGenderHumanMammals" parameterType="int" resultMap="GenderHumanMammalResultMap">
select g.id,
m.age,
h.name,
g.gender
from gender g
left outer join human h on g.id = h.id
left outer join mammal m on g.id = m.id
</select>
<insert id="insertGenderHumanMammal" parameterType="Gender">
insert into mammal
(id, age)
values (#{id, jdbcType=INTEGER}, #{age, jdbcType=INTEGER});
insert into human
(id, name)
values (#{id, jdbcType=INTEGER}, #{name, jdbcType=VARCHAR});
insert into gender
(id, gender)
values (#{id, jdbcType=INTEGER}, #{gender, jdbcType=VARCHAR});
</insert>
<update id="updateGenderHumanMammal" parameterType="Gender">
update mammal
set age = #{age, jdbcType=INTEGER}
where id = #{id, jdbcType=INTEGER};
update human
set name = #{name, jdbcType=VARCHAR}
where id = #{id, jdbcType=INTEGER};
update gender
set gender = #{gender, jdbcType=VARCHAR}
where id = #{id, jdbcType=INTEGER}
</update>
<delete id="deleteGenderHumanMammal" parameterType="int">
delete from gender
where id = #{id, jdbcType=INTEGER};
delete from human
where id = #{id, jdbcType=INTEGER};
delete from mammal
where id = #{id, jdbcType=INTEGER}
</delete>
</mapper>
Como podéis ver, todo está preparado para poder trabajar con los tres objetos a lo largo de todas la aplicación, y que para aquellos que heredan se pueda obtener el objeto entero con todas sus propiedades, o solo partes de dicho objeto.
La única cosa digna de mención, ya que el código es fácilmente entendible, es la utilización del objeto “super” en los constructores, para poder hacer una asignación encadenada de dicho “id” ya que este es compartido por los tres objetos en las tres tablas. Si no lo añadiéramos, tendríamos en muchas ocasiones los ids de los objetos padres a null.
Aunque otra cosa que debería de nombrar es que el código no es independiente de la BBDD que utilicemos. Quiero decir, PostgreSQL soporte la ejecución consecutiva de varias instrucciones tal y como están en los Mappers, pero si utilizáramos una BBDD Oracle, por ejemplo, tendríamos que encerrar esas sentencias consecutivas entre un “begin” y un “end;”:
begin
select ...;
select ...;
...
end;
Estoy casi seguro de que se puede acortar mucho más el código trabajando con herencia de los objetos resultado en MyBatis o quizás reutilizando las sentencias SQL de unos mappers a otros, pero de momento, eso, está aún por investigar.
Nos vemos.