web-dev-qa-db-fra.com

Comment créer une relation un-à-plusieurs avec l'API objet JDBI SQL?

Je crée une simple application REST avec dropwizard à l'aide de JDBI. L'étape suivante consiste à intégrer une nouvelle ressource qui a une relation un-à-plusieurs avec une autre. Jusqu'à présent, je ne pouvais pas comprendre comment créer une méthode dans mon DAO qui récupère un seul objet qui contient une liste d'objets d'une autre table.

Les représentations POJO ressembleraient à ceci:

public class User {

    private int id;
    private String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

public class Account {

    private int id;
    private String name;
    private List<User> users;

    public Account(int id, String name, List<User> users) {
        this.id = id;
        this.name = name;
        this.users = users;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<User> getUsers() {
        return name;
    }

    public void setUsers(List<Users> users) {
        this.users = users;
    }

}

Le DAO devrait ressembler à ceci

public interface AccountDAO {

    @Mapper(AccountMapper.class)
    @SqlQuery("SELECT Account.id, Account.name, User.name as u_name FROM Account LEFT JOIN User ON User.accountId = Account.id WHERE Account.id = :id")
    public Account getAccountById(@Bind("id") int id);

}

Mais lorsque la méthode a un seul objet comme valeur de retour ( Account au lieu de List <Account> ) il semble qu'il n'y ait aucun moyen d'accéder à plus d'une ligne du resultSet dans la classe Mapper. La seule solution qui se rapproche que j'ai pu trouver est décrite à https://groups.google.com/d/msg/jdbi/4e4EP-gVwEQ/02CRStgYGtgJ mais que l'on ne retourne également qu'un ensemble avec un objet unique qui ne semble pas très élégant. (Et ne peut pas être correctement utilisé par les classes de ressources.)

Il semble y avoir un moyen d'utiliser un Folder2 dans l'API fluide. Mais je ne sais pas comment intégrer cela correctement avec dropwizard et je préfère m'en tenir à l'API objet SQL de JDBI comme recommandé dans la documentation de dropwizard.

N'y a-t-il vraiment aucun moyen d'obtenir un mappage un-à-plusieurs à l'aide de l'API objet SQL dans JDBI? C'est un cas d'utilisation tellement basique pour une base de données que je pense que je dois manquer quelque chose.

Toute aide est grandement appréciée,
Tilman

28
Tilman

OK, après beaucoup de recherches, je vois deux façons de gérer cela:

La première option consiste à récupérer un objet pour chaque colonne et à le fusionner dans le code Java à la ressource (c'est-à-dire faire la jointure dans le code au lieu de le faire) par la base de données).

@GET
@Path("/{accountId}")
public Response getAccount(@PathParam("accountId") Integer accountId) {
    Account account = accountDao.getAccount(accountId);
    account.setUsers(userDao.getUsersForAccount(accountId));
    return Response.ok(account).build();
}

Ceci est possible pour les opérations de jointure plus petites mais ne me semble pas très élégant, car c'est quelque chose que la base de données est censée faire. Cependant, j'ai décidé de prendre ce chemin car mon application est plutôt petite et je ne voulais pas écrire beaucoup de code de mappeur.

La deuxième option consiste à écrire un mappeur, qui récupère le résultat de la requête de jointure et le mappe à l'objet comme ceci:

public class AccountMapper implements ResultSetMapper<Account> {

    private Account account;

    // this mapping method will get called for every row in the result set
    public Account map(int index, ResultSet rs, StatementContext ctx) throws SQLException {

        // for the first row of the result set, we create the wrapper object
        if (index == 0) {
            account = new Account(rs.getInt("id"), rs.getString("name"), new LinkedList<User>());
        }

        // ...and with every line we add one of the joined users
        User user = new User(rs.getInt("u_id"), rs.getString("u_name"));
        if (user.getId() > 0) {
            account.getUsers().add(user);
        }

        return account;
    }
}

L'interface DAO aura alors une méthode comme celle-ci:

public interface AccountDAO {

    @Mapper(AccountMapper.class)
    @SqlQuery("SELECT Account.id, Account.name, User.id as u_id, User.name as u_name FROM Account LEFT JOIN User ON User.accountId = Account.id WHERE Account.id = :id")
    public List<Account> getAccountById(@Bind("id") int id);

}

Remarque: Votre classe DAO abstraite se compilera tranquillement si vous utilisez un type de retour non-collection, par ex. public Account getAccountById(...);. Cependant, votre mappeur ne recevra qu'un ensemble de résultats avec une seule ligne même si la requête SQL aurait trouvé plusieurs lignes, que votre mappeur transformera avec plaisir en un seul compte avec un seul utilisateur. JDBI semble imposer un LIMIT 1 Pour les requêtes SELECT qui ont un type de retour non-collection. Il est possible de mettre des méthodes concrètes dans votre DAO si vous le déclarez comme une classe abstraite, donc une option est de conclure la logique avec une paire de méthodes publique/protégée, comme ceci:

public abstract class AccountDAO {

    @Mapper(AccountMapper.class)
    @SqlQuery("SELECT Account.id, Account.name, User.id as u_id, User.name as u_name FROM Account LEFT JOIN User ON User.accountId = Account.id WHERE Account.id = :id")
    protected abstract List<Account> _getAccountById(@Bind("id") int id);

    public Account getAccountById(int id) {
        List<Account> accountList = _getAccountById(id);
        if (accountList == null || accountList.size() < 1) {
            // Log it or report error if needed
            return null;
        }
        // The mapper will have given a reference to the same value for every entry in the list
        return accountList.get(accountList.size() - 1);
    }
}

Cela me semble encore un peu lourd et de bas niveau, car il y a généralement beaucoup de jointures dans le travail avec les données relationnelles. Je serais ravi de voir une meilleure façon ou d'avoir JDBI prenant en charge une opération abstraite pour cela avec l'API objet SQL.

30
Tilman

J'ai une petite bibliothèque qui sera très utile pour maintenir une relation un à plusieurs et un à un. Il fournit également plus de fonctionnalités pour les mappeurs par défaut.

https://github.com/Manikandan-K/jdbi-folder

4
Manikandan

Dans JDBI v3, vous pouvez utiliser @ UseRowReducer pour y parvenir. Le réducteur de ligne est appelé sur chaque ligne du résultat joint que vous pouvez "accumuler" en un seul objet. Une implémentation simple dans votre cas ressemblerait à:

public class AccountUserReducer implements LinkedHashMapRowReducer<Integer, Account> {

    @Override
    public void accumulate(final Map<Integer, Account> map, final RowView rowView) {
        final Account account = map.computeIfAbsent(rowView.getColumn("a_id", Integer.class),
            id -> rowView.getRow(Account.class));
        if (rowView.getColumn("u_id", Integer.class) != null) {
            account.addUser(rowView.getRow(User.class));
        }
    }
}

Vous pouvez maintenant appliquer ce réducteur sur une requête qui renvoie la jointure:

@RegisterBeanMapper(value = Account.class, prefix = "a")
@RegisterBeanMapper(value = User.class, prefix = "u")
@SqlQuery("SELECT a.id a_id, a.name a_name, u.id u_id, u.name u_name FROM " +
    "Account a LEFT JOIN User u ON u.accountId = a.id WHERE " +
    "a.id = :id")
@UseRowReducer(AccountUserReducer.class)
Account getAccount(@Bind("id") int id);

Notez que vos mappeurs de ligne/bean User et Account peuvent rester inchangés; ils savent simplement comment mapper une ligne individuelle des tables d'utilisateurs et de comptes respectivement. Votre classe Account aura besoin d'une méthode addUser() qui est appelée chaque fois que le réducteur de ligne est appelé.

4
spinlok

Il y a un ancien article de Google Groupes où Brian McAllistair (l'un des auteurs de JDBI) fait cela en mappant chaque ligne jointe à un objet intermédiaire, puis en repliant les lignes dans l'objet cible.

Voir la discussion ici . Il y a du code de test ici .

Personnellement, cela semble un peu insatisfaisant car cela signifie écrire un objet DBO supplémentaire et un mappeur pour la structure intermédiaire. Je pense quand même que cette réponse devrait être incluse pour être complète!

1
Matthew