Guice, JDBC i zarządzanie połączeniami z bazami danych

Chcę stworzyć przykładowy projekt podczas nauki Guice, który używa JDBC do odczytu/zapisu do bazy danych SQL. Jednak po latach używania Springa i pozwalania mu na abstrakcyjną obsługę połączeń i transakcji, staram się pracować nad tym koncepcyjnie.

Chciałbym mieć usługę, która uruchamia i zatrzymuje transakcję i wywołuje wiele repozytoriów, które ponownie korzystają z tego samego połączenia i uczestniczą w tej samej transakcji. Moje pytania to:

  • Gdzie tworzę moje źródło danych?
  • Jak dać repozytoriom dostęp do połączenia? (ThreadLocal?)
  • najlepszy sposób zarządzania transakcją (Tworzenie Interceptora dla adnotacji?)

Poniższy kod pokazuje, jak zrobiłbym to wiosną. Jdbcoperations wstrzyknięte do każdego repozytorium będzie miał dostęp do połączenia związanego z aktywną transakcją.

Nie udało mi się znaleźć wielu tutoriali, które to omawiają, poza tymi, które pokazują tworzenie przechwytywaczy dla transakcji.

Cieszę się, że nadal używam Springa, ponieważ działa bardzo dobrze w moich projektach, ale chciałbym wiedzieć, jak to zrobić w czystym Guice i JBBC (bez JPA/Hibernate/Warp/Reusing Spring)

@Service
public class MyService implements MyInterface {

  @Autowired
  private RepositoryA repositoryA;
  @Autowired
  private RepositoryB repositoryB;
  @Autowired
  private RepositoryC repositoryC; 

  @Override
  @Transactional
  public void doSomeWork() {
    this.repositoryA.someInsert();
    this.repositoryB.someUpdate();
    this.repositoryC.someSelect();  
  }    
}

@Repository
public class MyRepositoryA implements RepositoryA {

  @Autowired
  private JdbcOperations jdbcOperations;

  @Override
  public void someInsert() {
    //use jdbcOperations to perform an insert
  }
}

@Repository
public class MyRepositoryB implements RepositoryB {

  @Autowired
  private JdbcOperations jdbcOperations;

  @Override
  public void someUpdate() {
    //use jdbcOperations to perform an update
  }
}

@Repository
public class MyRepositoryC implements RepositoryC {

  @Autowired
  private JdbcOperations jdbcOperations;

  @Override
  public String someSelect() {
    //use jdbcOperations to perform a select and use a RowMapper to produce results
    return "select result";
  }
}
Author: BalusC, 2010-02-27

4 answers

Jeśli twoja baza danych zmienia się rzadko, możesz użyć źródła danych dostarczonego ze sterownikiem JDBC bazy danych i wyizolować połączenia z biblioteką 3rd party u dostawcy (mój przykład używa tej dostarczonej przez H2 dataabse, ale wszyscy dostawcy JDBC powinni ją mieć). C3PO, Apache DBCP lub jedną dostarczoną przez kontener serwera aplikacji) można po prostu napisać nową implementację dostawcy, aby uzyskać źródło danych z odpowiedniego miejsce. Tutaj użyłem singleton scope, aby umożliwić instancję DataSource współdzielenie pomiędzy tymi klasami, które od niej zależą (konieczne do łączenia).

public class DataSourceModule extends AbstractModule {

    @Override
    protected void configure() {
        Names.bindProperties(binder(), loadProperties());

        bind(DataSource.class).toProvider(H2DataSourceProvider.class).in(Scopes.SINGLETON);
        bind(MyService.class);
    }

    static class H2DataSourceProvider implements Provider<DataSource> {

        private final String url;
        private final String username;
        private final String password;

        public H2DataSourceProvider(@Named("url") final String url,
                                    @Named("username") final String username,
                                    @Named("password") final String password) {
            this.url = url;
            this.username = username;
            this.password = password;
        }

        @Override
        public DataSource get() {
            final JdbcDataSource dataSource = new JdbcDataSource();
            dataSource.setURL(url);
            dataSource.setUser(username);
            dataSource.setPassword(password);
            return dataSource;
        }
    }

    static class MyService {
        private final DataSource dataSource;

        @Inject
        public MyService(final DataSource dataSource) {
            this.dataSource = dataSource;
        }

        public void singleUnitOfWork() {

            Connection cn = null;

            try {
                cn = dataSource.getConnection();
                // Use the connection
            } finally {
                try {
                    cn.close();
                } catch (Exception e) {}
            }
        }
    }

    private Properties loadProperties() {
        // Load properties from appropriate place...
        // should contain definitions for:
        // url=...
        // username=...
        // password=...
        return new Properties();
    }
}

Do obsługi transakcji należy użyć świadomego transakcji źródła danych. Nie polecam tego ręcznie. Używając czegoś w rodzaju WARP-persist lub kontenera dostarczonego do zarządzania transakcjami, wyglądałoby to jednak mniej więcej tak:

public class TxModule extends AbstractModule {

    @Override
    protected void configure() {
        Names.bindProperties(binder(), loadProperties());

        final TransactionManager tm = getTransactionManager();

        bind(DataSource.class).annotatedWith(Real.class).toProvider(H2DataSourceProvider.class).in(Scopes.SINGLETON);
        bind(DataSource.class).annotatedWith(TxAware.class).to(TxAwareDataSource.class).in(Scopes.SINGLETON);
        bind(TransactionManager.class).toInstance(tm);
        bindInterceptor(Matchers.any(), Matchers.annotatedWith(Transactional.class), new TxMethodInterceptor(tm));
        bind(MyService.class);
    }

    private TransactionManager getTransactionManager() {
        // Get the transaction manager
        return null;
    }

    static class TxMethodInterceptor implements MethodInterceptor {

        private final TransactionManager tm;

        public TxMethodInterceptor(final TransactionManager tm) {
            this.tm = tm;
        }

        @Override
        public Object invoke(final MethodInvocation invocation) throws Throwable {
            // Start tx if necessary
            return invocation.proceed();
            // Commit tx if started here.
        }
    }

    static class TxAwareDataSource implements DataSource {

        static ThreadLocal<Connection> txConnection = new ThreadLocal<Connection>();
        private final DataSource ds;
        private final TransactionManager tm;

        @Inject
        public TxAwareDataSource(@Real final DataSource ds, final TransactionManager tm) {
            this.ds = ds;
            this.tm = tm;
        }

        public Connection getConnection() throws SQLException {
            try {
                final Transaction transaction = tm.getTransaction();
                if (transaction != null && transaction.getStatus() == Status.STATUS_ACTIVE) {

                    Connection cn = txConnection.get();
                    if (cn == null) {
                        cn = new TxAwareConnection(ds.getConnection());
                        txConnection.set(cn);
                    }

                    return cn;

                } else {
                    return ds.getConnection();
                }
            } catch (final SystemException e) {
                throw new SQLException(e);
            }
        }

        // Omitted delegate methods.
    }

    static class TxAwareConnection implements Connection {

        private final Connection cn;

        public TxAwareConnection(final Connection cn) {
            this.cn = cn;
        }

        public void close() throws SQLException {
            try {
                cn.close();
            } finally {
                TxAwareDataSource.txConnection.set(null);
            }
        }

        // Omitted delegate methods.
    }

    static class MyService {
        private final DataSource dataSource;

        @Inject
        public MyService(@TxAware final DataSource dataSource) {
            this.dataSource = dataSource;
        }

        @Transactional
        public void singleUnitOfWork() {
            Connection cn = null;

            try {
                cn = dataSource.getConnection();
                // Use the connection
            } catch (final SQLException e) {
                throw new RuntimeException(e);
            } finally {
                try {
                    cn.close();
                } catch (final Exception e) {}
            }
        }
    }
}
 28
Author: Michael Barker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-03-01 19:48:01

Użyłbym czegoś takiego jak c3po do bezpośredniego tworzenia źródeł danych. Jeśli używasz ComboPooledDataSource potrzebujesz tylko instancji (pooling odbywa się pod pokrywami), które można powiązać bezpośrednio lub za pośrednictwem Dostawcy.

Wtedy stworzyłbym interceptor, który np. pobiera @ Transactional, zarządza połączeniem I commit/ rollback. Możesz również wstrzykiwać połączenie, ale musisz upewnić się, że gdzieś zamykasz połączenia, aby umożliwić ich sprawdzenie w znowu basen.

 2
Author: Eelco,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-02-28 09:12:49
  1. Aby wprowadzić źródło danych, prawdopodobnie nie musisz być związany z pojedynczą instancją źródła danych, ponieważ baza danych, którą łączysz z funkcjami w adresie url. Za pomocą Guice można wymusić na programistach powiązanie z implementacją DataSource (link ) . To źródło danych może być wstrzyknięte do ConnectionProvider, aby zwrócić źródło danych.

  2. Połączenie musi znajdować się w zasięgu lokalnym wątku. Możesz nawet zaimplementować swój lokalny wątek scope ale wszystkie połączenia lokalne wątku muszą być zamknięte i usunięte z obiektu ThreadLocal po operacjach commit lub rollback, aby zapobiec wyciekowi pamięci. Po hackingu odkryłem, że trzeba mieć hak do obiektu wtryskiwacza, aby usunąć elementy ThreadLocal. Wtryskiwacz można łatwo wstrzyknąć do Guice AOP interceptor, coś takiego:

    protected  void visitThreadLocalScope(Injector injector, 
                        DefaultBindingScopingVisitor visitor) {
        if (injector == null) {
            return;
        }

        for (Map.Entry, Binding> entry : 
                injector.getBindings().entrySet()) {
            final Binding binding = entry.getValue();
            // Not interested in the return value as yet.
            binding.acceptScopingVisitor(visitor);
        }        
    }

    /**
     * Default implementation that exits the thread local scope. This is 
     * essential to clean up and prevent any memory leakage.
     * 
     * 

The scope is only visited iff the scope is an sub class of or is an * instance of {@link ThreadLocalScope}. */ private static final class ExitingThreadLocalScopeVisitor extends DefaultBindingScopingVisitor { @Override public Void visitScope(Scope scope) { // ThreadLocalScope is the custom scope. if (ThreadLocalScope.class.isAssignableFrom(scope.getClass())) { ThreadLocalScope threadLocalScope = (ThreadLocalScope) scope; threadLocalScope.exit(); } return null; } }

Upewnij się, że wywołujesz to po wywołaniu metody i zamknięciu połączenia. Spróbuj to sprawdzić, czy to działa.

 0
Author: Kartik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-06-05 14:30:51

Proszę sprawdzić rozwiązanie, które podałem: transakcje z Guice i JDBC - dyskusja rozwiązania

To tylko bardzo podstawowa wersja i proste podejście. ale działa dobrze, aby obsługiwać transakcje z Guice i JDBC.

 0
Author: honghai,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:02:02