Mysql
 sql >> Base de données >  >> RDS >> Mysql

Comment configurer Hibernate pour lire/écrire sur différentes sources de données ?

Un exemple peut être trouvé ici :https://github.com/afedulov/routing-data- sources .

Spring fournit une variante de DataSource, appelée AbstractRoutingDatasource . Il peut être utilisé à la place des implémentations standard de DataSource et permet à un mécanisme de déterminer quel DataSource concret utiliser pour chaque opération au moment de l'exécution. Tout ce que vous avez à faire est de l'étendre et de fournir une implémentation d'un abstrait determineCurrentLookupKey méthode. C'est l'endroit où implémenter votre logique personnalisée pour déterminer la source de données concrète. L'objet renvoyé sert de clé de recherche. Il s'agit généralement d'une chaîne ou d'une enum, utilisée comme qualificatif dans la configuration Spring (les détails suivront).

package website.fedulov.routing.RoutingDataSource

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DbContextHolder.getDbType();
    }
}

Vous vous demandez peut-être quel est cet objet DbContextHolder et comment sait-il quel identifiant DataSource renvoyer ? Gardez à l'esprit que determineCurrentLookupKey sera appelée chaque fois que TransactionsManager demande une connexion. Il est important de se rappeler que chaque transaction est "associée" à un thread distinct. Plus précisément, TransactionsManager lie Connection au thread actuel. Par conséquent, afin de répartir différentes transactions vers différentes sources de données cibles, nous devons nous assurer que chaque thread peut identifier de manière fiable quelle source de données est destinée à être utilisée. Cela rend naturel l'utilisation de variables ThreadLocal pour lier un DataSource spécifique à un Thread et donc à une Transaction. Voici comment procéder :

public enum DbType {
   MASTER,
   REPLICA1,
}

public class DbContextHolder {

   private static final ThreadLocal<DbType> contextHolder = new ThreadLocal<DbType>();

   public static void setDbType(DbType dbType) {
       if(dbType == null){
           throw new NullPointerException();
       }
      contextHolder.set(dbType);
   }

   public static DbType getDbType() {
      return (DbType) contextHolder.get();
   }

   public static void clearDbType() {
      contextHolder.remove();
   }
}

Comme vous le voyez, vous pouvez également utiliser un enum comme clé et Spring se chargera de le résoudre correctement en fonction du nom. La configuration et les clés DataSource associées peuvent ressembler à ceci :

  ....
<bean id="dataSource" class="website.fedulov.routing.RoutingDataSource">
 <property name="targetDataSources">
   <map key-type="com.sabienzia.routing.DbType">
     <entry key="MASTER" value-ref="dataSourceMaster"/>
     <entry key="REPLICA1" value-ref="dataSourceReplica"/>
   </map>
 </property>
 <property name="defaultTargetDataSource" ref="dataSourceMaster"/>
</bean>

<bean id="dataSourceMaster" class="org.apache.commons.dbcp.BasicDataSource">
  <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="${db.master.url}"/>
  <property name="username" value="${db.username}"/>
  <property name="password" value="${db.password}"/>
</bean>
<bean id="dataSourceReplica" class="org.apache.commons.dbcp.BasicDataSource">
  <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
  <property name="url" value="${db.replica.url}"/>
  <property name="username" value="${db.username}"/>
  <property name="password" value="${db.password}"/>
</bean>

À ce stade, vous pourriez vous retrouver à faire quelque chose comme ceci :

@Service
public class BookService {

  private final BookRepository bookRepository;
  private final Mapper               mapper;

  @Inject
  public BookService(BookRepository bookRepository, Mapper mapper) {
    this.bookRepository = bookRepository;
    this.mapper = mapper;
  }

  @Transactional(readOnly = true)
  public Page<BookDTO> getBooks(Pageable p) {
    DbContextHolder.setDbType(DbType.REPLICA1);   // <----- set ThreadLocal DataSource lookup key
                                                  // all connection from here will go to REPLICA1
    Page<Book> booksPage = callActionRepo.findAll(p);
    List<BookDTO> pContent = CollectionMapper.map(mapper, callActionsPage.getContent(), BookDTO.class);
    DbContextHolder.clearDbType();               // <----- clear ThreadLocal setting
    return new PageImpl<BookDTO>(pContent, p, callActionsPage.getTotalElements());
  }

  ...//other methods

Nous pouvons maintenant contrôler quelle source de données sera utilisée et transmettre les demandes à notre guise. Cela semble bon!

...Ou le fait-il ? Tout d'abord, ces appels de méthode statique à un DbContextHolder magique ressortent vraiment. Ils semblent ne pas appartenir à la logique métier. Et ils ne le font pas. Non seulement ils ne communiquent pas le but, mais ils semblent fragiles et sujets aux erreurs (que diriez-vous d'oublier de nettoyer le dbType). Et que se passe-t-il si une exception est levée entre setDbType et cleanDbType ? Nous ne pouvons pas simplement l'ignorer. Nous devons être absolument sûrs que nous avons réinitialisé le dbType, sinon le Thread retourné au ThreadPool pourrait être dans un état "cassé", essayant d'écrire dans une réplique lors du prochain appel. Nous avons donc besoin de ceci :

  @Transactional(readOnly = true)
  public Page<BookDTO> getBooks(Pageable p) {
    try{
      DbContextHolder.setDbType(DbType.REPLICA1);   // <----- set ThreadLocal DataSource lookup key
                                                    // all connection from here will go to REPLICA1
      Page<Book> booksPage = callActionRepo.findAll(p);
      List<BookDTO> pContent = CollectionMapper.map(mapper, callActionsPage.getContent(), BookDTO.class);
       DbContextHolder.clearDbType();               // <----- clear ThreadLocal setting
    } catch (Exception e){
      throw new RuntimeException(e);
    } finally {
       DbContextHolder.clearDbType();               // <----- make sure ThreadLocal setting is cleared         
    }
    return new PageImpl<BookDTO>(pContent, p, callActionsPage.getTotalElements());
  }

Ouais >_< ! Cela ne ressemble certainement pas à quelque chose que j'aimerais mettre dans chaque méthode en lecture seule. Peut-on faire mieux ? Bien sûr! Ce modèle de "faire quelque chose au début d'une méthode, puis faire quelque chose à la fin" devrait vous dire quelque chose. Aspects à la rescousse !

Malheureusement, ce post est déjà devenu trop long pour couvrir le sujet des aspects personnalisés. Vous pouvez suivre les détails de l'utilisation des aspects en utilisant ce lien .