Hibernate Efetivo - Erros Comuns e Soluções

Hibernate Efetivo - Erros Comuns e Soluções

(Palestra ministrada no TechDay da MDIAS em DEZ-2017)

Mesmo anos após o lançamento do Hibernate ainda é fácil encontrar projetos utilizando o framework de maneira ineficiente, podendo leva-lo a problemas sérios de performance ou até inviabilizar a aplicação.

O uso não efetivo do Hibernate está intimamente ligado a erros comuns e más práticas em sua utilização, que vão desde o pool de conexões, select n+1, configuração de cache, batch-size até o uso indevido do cache level 1 em processamentos batch e o tratamento de LazyInitializationException.

Nesta palestra você vai aprender dicas importantes de como melhorar a performance da sua camada de persistência e melhorar a escalabilidade da sua aplicação.

F853760c988228c4a153333407e64f09?s=128

Rafael Ponte

December 28, 2017
Tweet

Transcript

  1. Hibernate EFETIVO ERROS COMUNS E SOLUÇÕES Rafael Ponte MDias Tech

    Day 2017
  2. Eu estava me perguntando quando de fato o Hibernate foi

    criado...
  3. Eu estava me perguntando quando de fato o Hibernate foi

    criado... Luca Bastos O “Boom” foi em 2003!
  4. Mais de uma década! 2017 - 2003 = 14

  5. mas ainda hoje...

  6. mas ainda hoje... existem devs que subutilizam o framework

  7. mas ainda hoje... existem devs que subutilizam o framework problemas

    de perfomance e escalabilidade
  8. e pra piorar...

  9. culpam o Hibernate

  10. culpam o Hibernate A culpa é do Banco de Dados!

    Sérgio
  11. é fácil culpar o que não se conhece...

  12. um SGDB mal configurado pode ser o problema...

  13. um SGDB mal configurado pode ser o problema... MAS na

    maioria das vezes o problema está na SUA APLICAÇÃO
  14. tabelas sem índices <3

  15. tabelas sem índices consultas mal-feitas <3 <3

  16. tabelas sem índices consultas mal-feitas <3 <3 <3 muitos hits

    ao banco
  17. tabelas sem índices consultas mal-feitas muitos hits ao banco connection

    leaks <3 <3 <3
  18. tabelas sem índices consultas mal-feitas muitos hits ao banco connection

    leaks <3 <3 <3 memory leaks
  19. Não saber tirar proveito framework!

  20. Hibernate Efetivo 6 dicas para não deixar  sua app

    morrer
  21. @rponte

  22. None
  23. Fortaleza - Terra do Sol

  24. None
  25. None
  26. #1 POOL DE CONEXÕES

  27. com certeza todos aqui já ouviram falar...

  28. None
  29. 1 clientes 1 conexão

  30. 10 clientes 10 conexões

  31. 100 clientes 100 conexões

  32. 1000 clientes 1000 conexões

  33. 1000 clientes 0 conexões

  34. ???

  35. 100
 conexões

  36. 100
 conexões 1000 clientes 100 conexões

  37. None
  38. None
  39. mas nem todos dão a devida atenção...

  40. mas nem todos dão a devida atenção... até perceberem a

    app engasgando
  41. mas nem todos dão a devida atenção... até perceberem a

    app engasgando ou até receberem um
  42. mas nem todos dão a devida atenção... até perceberem a

    app engasgando ou até receberem um org.hibernate.exception.Generic JDBCException: Cannot open connection
  43. daí percebem que não configuraram o o pool do Hibernate

    
 hibernate.connection.driver_class=org.postgresql.Driver hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect hibernate.connection.url=jdbc:postgresql://localhost:5432/myapp hibernate.connection.username=postgres hibernate.connection.password=1234 hibernate.properties
  44. daí percebem que não configuraram o o pool do Hibernate

    
 hibernate.connection.driver_class=org.postgresql.Driver hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect hibernate.connection.url=jdbc:postgresql://localhost:5432/myapp hibernate.connection.username=postgres hibernate.connection.password=1234
 hibernate.connection.pool_size=30 hibernate.properties
  45. daí percebem que não configuraram o o pool do Hibernate

    
 hibernate.connection.driver_class=org.postgresql.Driver hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect hibernate.connection.url=jdbc:postgresql://localhost:5432/myapp hibernate.connection.username=postgres hibernate.connection.password=1234
 hibernate.connection.pool_size=30 hibernate.properties
  46. a app volta a funcionar bem por um tempo

  47. a app volta a funcionar bem por um tempo

  48. e vão alocando mais conexões com o tempo 
 hibernate.connection.pool_size=30

    40 55 70
 ... hibernate.properties
  49. o pool PADRÃO do Hibernate

  50. que possui uma impl. RUDIMENTAR

  51. INFO DriverManagerConnectionProvider:64 - Using Hibernate built-in connection pool (not for

    production use!) INFO DriverManagerConnectionProvider:65 - Hibernate connection pool size: 20
  52. INFO DriverManagerConnectionProvider:64 - Using Hibernate built-in connection pool (not for

    production use!) INFO DriverManagerConnectionProvider:65 - Hibernate connection pool size: 20 not for production use!
  53. qual pool utilizar?

  54. temos ótimas opções...

  55. pools como c3p0 ou commons-dbcp temos ótimas opções...

  56. o Hibernate já vem com c3p0

  57. configurando c3p0 
 hibernate.connection.driver_class=org.postgresql.Driver hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect hibernate.connection.url=jdbc:postgresql://localhost:5432/myapp hibernate.connection.username=postgres hibernate.connection.password=1234 hibernate.c3p0.min_size=5 hibernate.c3p0.max_size=20

    hibernate.c3p0.timeout=1800 hibernate.c3p0.max_statements=50 hibernate.properties
  58. ou melhor ainda...

  59. <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <!-- access configuration --> <property name="driverClass"

    value="${jdbc.driverclass}" /> <property name="jdbcUrl" value="${jdbc.url}" /> <property name="user" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> <!-- pool sizing --> <property name="initialPoolSize" value="3" /> <property name="minPoolSize" value="6" /> <property name="maxPoolSize" value="25" /> <property name="acquireIncrement" value="3" /> <property name="maxStatements" value="0" /> <!-- retries --> <property name="acquireRetryAttempts" value="30" /> <property name="acquireRetryDelay" value="1000" /> <!-- 1s --> <property name="breakAfterAcquireFailure" value="false" /> <!-- refreshing connections --> <property name="maxIdleTime" value="180" /> <!-- 3min --> <property name="maxConnectionAge" value="10" /> <!-- 1h --> <!-- timeouts e testing --> <property name="checkoutTimeout" value="5000" /> <!-- 5s --> <property name="idleConnectionTestPeriod" value="60" /> <!-- 60 --> <property name="testConnectionOnCheckout" value="true" /> <property name="preferredTestQuery" value="SELECT 1+1" /> </bean> podemos obter as conexões de um DataSource
  60. Pool traz melhoria de performance

  61. Pool traz melhoria de performance mas não faz milagres

  62. #2 lidando com LazyInitialization Exception

  63. quando e por que acontece?

  64. @Entity class NotaFiscal { … @OneToMany List<Item> itens; }

  65. @Entity class NotaFiscal { … @OneToMany List<Item> itens; }

  66. @Entity class NotaFiscal { … @OneToMany(fetch=FetchType.LAZY) List<Item> itens; }

  67. NotaFiscal nf = (NotaFiscal) session.load(NotaFiscal.class, 42); List<Item> itens = nf.getItens();

    Percorrendo os itens de uma nota
  68. NotaFiscal nf = (NotaFiscal) session.load(NotaFiscal.class, 42); List<Item> itens = nf.getItens();

    Percorrendo os itens de uma nota
  69. select nf.* from NotaFiscal nf where nf.id=42 select i.* from

    Item i where i.nota_fiscal_id=42 Hibernate executa 2 selects NotaFiscal nf = (NotaFiscal) session.load(NotaFiscal.class, 42); List<Item> itens = nf.getItens();
  70. Session session = sessionFactory.openSession(); NotaFiscal nf = (NotaFiscal) session.load(NotaFiscal.class, 42);

    
 session.close(); List<Item> itens = nf.getItens();
 System.out.println("numero de pedidos:" + itens.size()); a session do Hibernate foi fechada
  71. Session session = sessionFactory.openSession(); NotaFiscal nf = (NotaFiscal) session.load(NotaFiscal.class, 42);

    
 session.close(); List<Item> itens = nf.getItens();
 System.out.println("numero de pedidos:" + itens.size()); mas ao ler os itens da nota org.hibernate.LazyInitializationException: failed to lazily initialize a collection -
 no session or session was closed.
  72. resolver parece fácil, certo?

  73. Session session = sessionFactory.openSession(); NotaFiscal nf = (NotaFiscal) session.load(NotaFiscal.class, 42);

    List<Item> itens = nf.getItens();
 System.out.println("numero de pedidos:" + itens.size());
 
 session.close(); fechar a session ao término do trabalho
  74. Mas e quando estamos trabalhando na Web?

  75. @Get("/notas/{id}") public void view(Long id) {
 NotaFiscal nf = notaFiscalDao.carrega(id);

    result.include("nf", nf);
 result.forwardTo("/notas/view.jsp"); } view.jsp NotaFiscalController.java <c:forEach var="item" items="${nf.itens}"> ${item.produto.descricao}<br/> </c:forEach>
  76. @Get("/notas/{id}") public void view(Long id) {
 NotaFiscal nf = notaFiscalDao.carrega(id);

    result.include("nf", nf);
 result.forwardTo("/notas/view.jsp"); } view.jsp NotaFiscalController.java <c:forEach var="item" items="${nf.itens}"> ${item.produto.descricao}<br/> </c:forEach> session foi fechada!
  77. @Get("/notas/{id}") public void view(Long id) {
 NotaFiscal nf = notaFiscalDao.carrega(id);

    result.include("nf", nf);
 result.forwardTo("/notas/view.jsp"); } view.jsp NotaFiscalController.java <c:forEach var="item" items="${nf.itens}"> ${item.produto.descricao}<br/> </c:forEach> LazyInitializationException
  78. e agora? #comofas

  79. e agora? #comofas Sérgio passa tudo pra EAGER! ;D

  80. @Entity class NotaFiscal { … @OneToMany(fetch=FetchType.EAGER) List<Item> itens; }

  81. select nf.*, i.* from NotaFiscal nf left outer join Item

    i on nf.id = i.nota_fiscal_id
 where nf.id=42 Hibernate executa 1 select NotaFiscal nf = (NotaFiscal) session.load(NotaFiscal.class, 42);
  82. mas isso poderia gerar uma sobrecarga...

  83. mas isso poderia gerar uma sobrecarga... pois os itens da

    nota não são necessários em muitos lugares
  84. lembre-se que ter os relacionamentos como LAZY é uma boa

    prática
  85. como evitar LIE sem modificar o relacionamento para EAGER?

  86. Open Session In View

  87. @WebFilter(urlPatterns="/*") public class OpenSessionInViewFilter implements Filter { SessionFactory sessionFactory; @Override

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { Transaction transaction = null; try { Session session = sessionFactory.getCurrentSession(); transaction = session.beginTransaction(); chain.doFilter(req, res); transaction.commit(); } finally { if (transaction != null && transaction.isActive()) { transaction.rollback(); } session.close(); } } } Servlet Filter
  88. o OSIV só evita LIE no mesmo request!

  89. anti-pattern?

  90. #3 Second Level Cache

  91. NotaFiscal nf = (NotaFiscal) session.load(NotaFiscal.class, 42); Carregando uma nota por

    ID
  92. Banco de Dados Session

  93. Banco de Dados Session First Level Cache

  94. Banco de Dados Session Session Session Session

  95. Banco de Dados ???? Session Session Session Session

  96. Banco de Dados Second Level Cache Session Session Session Session

  97. Banco de Dados Second Level Cache Session Session Session First

    Level Cache Session
  98. Banco de Dados SessionFactory Session Session Session Session

  99. Configurar é simples

  100. #1 configuramos o Hibernate 
 hibernate.cache.use_second_level_cache=true hibernate.cache.region.factory_class=net.sf.ehcac he.hibernate.EhCacheRegionFactory hibernate.properties

  101. @Entity @Cache(usage=CacheConcurrencyStrategy.READ_WRITE) class Bug { @Id private Long id; private

    String descricao; private String status; @ManyToOne private Projeto projeto; @OneToMany private List<Comentario> comentarios; } #2 configuramos as entidades
  102. @Cache( usage=CacheConcurrencyStrategy.READ_WRITE) Caching Strategy READ_ONLY NONSTRICT_READ_WRITE READ_WRITE

  103. @Cache( usage=CacheConcurrencyStrategy.READ_WRITE) Caching Strategy READ_ONLY NONSTRICT_READ_WRITE READ_WRITE Melhor performance

  104. @Cache( usage=CacheConcurrencyStrategy.READ_WRITE) Caching Strategy READ_ONLY NONSTRICT_READ_WRITE READ_WRITE Dados não críticos

  105. @Cache( usage=CacheConcurrencyStrategy.READ_WRITE) Caching Strategy READ_ONLY NONSTRICT_READ_WRITE READ_WRITE Modificações frequentes

  106. <cache name="br.com.triadworks.model.Bug" maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="1800" timeToLiveSeconds="10000" overflowToDisk="true" memoryStoreEvictionPolicy="LRU" /> ehcache.xml

  107. Como 2nd Level Cache funciona?

  108. 2nd Level Cache não faz cache das instancias das entidades

  109. 2nd Level Cache não faz cache das instancias das entidades

    somente dos valores das propriedades
  110. @Entity @Cache(usage=CacheConcurrencyStrategy.READ_WRITE) class Bug { @Id private Long id; private

    String descricao; private String status; @ManyToOne private Projeto projeto; @OneToMany private List<Comentario> comentarios; }
  111. modelo conceitual do cache Bug Data Cache 17 -> [

    “Bug #1”, “ABERTA” , 1 ] 18 -> [ “Bug #2”, “FECHADA”, 2 ] 19 -> [ “Bug #3”, “ABERTA” , 1 ]
  112. modelo conceitual do cache Bug Data Cache 17 -> [

    “Bug #1”, “ABERTA” , 1 ] 18 -> [ “Bug #2”, “FECHADA”, 2 ] 19 -> [ “Bug #3”, “ABERTA” , 1 ] id descricao status id do projeto
  113. modelo conceitual do cache Bug Data Cache 17 -> [

    “Bug #1”, “ABERTA” , 1 ] 18 -> [ “Bug #2”, “FECHADA”, 2 ] 19 -> [ “Bug #3”, “ABERTA” , 1 ] não é uma árvore de objetos, mas sim um Map de Arrays
  114. 2nd Level Cache não faz cache das associações

  115. @Entity @Cache(usage=CacheConcurrencyStrategy.READ_WRITE) class Bug { @Id private Long id; private

    String descricao; private String status; @ManyToOne private Projeto projeto; @OneToMany private List<Comentario> comentarios; }
  116. modelo conceitual do cache Bug Data Cache 17 -> [

    “Bug #1”, “ABERTA” , 1, ??? ] 18 -> [ “Bug #2”, “FECHADA”, 2, ??? ] 19 -> [ “Bug #3”, “ABERTA” , 1, ??? ] comentarios??
  117. @Entity @Cache(usage=CacheConcurrencyStrategy.READ_WRITE) class Bug { @Id private Long id; private

    String descricao; private String status; @ManyToOne private Projeto projeto; @OneToMany @Cache(usage=CacheConcurrencyStrategy.READ_WRITE) private List<Comentario> comentarios; }
  118. modelo conceitual do cache Bug Data Cache 17 -> [

    “Bug #1”, “ABERTA” , 1, [1,2] ] 18 -> [ “Bug #2”, “FECHADA”, 2, [] ] 19 -> [ “Bug #3”, “ABERTA” , 1, [3] ]
  119. modelo conceitual do cache Bug Data Cache 17 -> [

    “Bug #1”, “ABERTA” , 1, [1,2] ] 18 -> [ “Bug #2”, “FECHADA”, 2, [] ] 19 -> [ “Bug #3”, “ABERTA” , 1, [3] ] ids dos comentarios
  120. E o que o Hibernate faz com todos estes IDs?

  121. E o que o Hibernate faz com todos estes IDs?

    vai no banco de novo! a não ser que você…
  122. @Entity @Cache(usage=CacheConcurrencyStrategy.READ_WRITE) class Projeto { ... } @Entity @Cache(usage=CacheConcurrencyStrategy.READ_ONLY) class

    Comentario { ... } configure o cache das entidades
  123. Devo cachear todas as minhas entidades?

  124. Com 2nd Level Cache tudo funciona bem enquanto buscamos por

    ID... session.load(Bug.class, 17);
  125. Com 2nd Level Cache tudo funciona bem enquanto buscamos por

    ID... ...mas e quando precisamos de uma consulta um pouco diferente? session.load(Bug.class, 17); session
 .createQuery("from Bug where status = ?") .setString(0,"ABERTO")
 .list();
  126. #4 Query Cache

  127. Query Cache faz cache do resultado de uma query

  128. Configurando Hibernate para usar Query Cache 
 hibernate.cache.use_query_cache=true hibernate.properties

  129. Query Cache session
 .createQuery("from Bug where status = ?") .setString(0,

    status)
 .setCacheable(true)
 .list();
  130. modelo conceitual do query cache Query Cache [“from Bug where

    status = ?”, [“ABERTO”]] -> [17, 19]
  131. modelo conceitual do query cache Query Cache [“from Bug where

    status = ?”, [“ABERTO”]] -> [17, 19] Query + Parâmetros IDs
  132. por isso Query Cache SEM 2nd Level Cache não é

    de muita ajuda
  133. Utilize somente em consultas que são executadas repetidas vezes com

    os mesmos parâmetros
  134. Utilize somente em consultas que são executadas repetidas vezes com

    os mesmos parâmetros
  135. #5 Select n+1

  136. o campeão em prejudicar a performance da aplicação

  137. @Entity class NotaFiscal { … @OneToMany List<Item> itens; }

  138. NotaFiscal nf = (NotaFiscal) session.load(NotaFiscal.class, 42); 
 processaItensDaNota(nf); Processando os

    itens de uma nota
  139. NotaFiscal nf = (NotaFiscal) session.load(NotaFiscal.class, 42); 
 processaItensDaNota(nf); Processando os

    itens de uma nota
  140. select nf.* from NotaFiscal nf where nf.id=42 select i.* from

    Item i where i.nota_fiscal_id=42 Hibernate executa 2 selects NotaFiscal nf = (NotaFiscal) session.load(NotaFiscal.class, 42); processaItensDaNota(nf);
  141. List<NotaFiscal> notas = dao.listaTudo(); for (NotaFiscal nf : notas) {

    processaItensDaNota(nf); } Processando os itens de varias notas
  142. List<NotaFiscal> notas = dao.listaTudo(); for (NotaFiscal nf : notas) {

    processaItensDaNota(nf); } Processando os itens de varias notas
  143. select nf.* from NotaFiscal nf select i.* from Item i

    where i.nota_fiscal_id=? select i.* from Item i where i.nota_fiscal_id=? select i.* from Item i where i.nota_fiscal_id=? select i.* from Item i where i.nota_fiscal_id=? select i.* from Item i where i.nota_fiscal_id=? ... Hibernate executa n+1 selects List<NotaFiscal> notas = dao.listaTudo(); for (NotaFiscal nf : notas) { processaItensDaNota(nf); }
  144. são muitos hits no banco de dados

  145. são muitos hits no banco de dados mas podemos resolver

    isso...
  146. 3 soluções

  147. #1 EAGER ou join- fetch

  148. @Entity class NotaFiscal { … @OneToMany(fetch=FetchType.EAGER) List<Item> itens; } Utilizando

    FetchMode=EAGER
  149. select nf.*, i.* from NotaFiscal nf left outer join Item

    i on nf.id = i.nota_fiscal_id
 Hibernate executa 1 select List<NotaFiscal> notas = dao.listaTudo();
  150. antes de definir um mapeamento global deste tipo você precisa

    se perguntar...
  151. SEMPRE que uma nota é necessária, todos seus itens também

    são necessários?
  152. não?

  153. session
 .createQuery("from NotaFiscal n left join fetch n.itens")
 .list(); Utilizando

    Join Fetch
  154. #2 batch-size nas associações

  155. É o meio termo entre EAGER e LAZY

  156. @Entity class NotaFiscal { … @OneToMany @BatchSize(size=10) List<Item> itens; }

    @BatchSize
  157. select nf.* from NotaFiscal nf select i.* from Item i

    where i.nota_fiscal_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) select i.* from Item i where i.nota_fiscal_id in (?, ?, ?, ?, ?) Hibernate executa n/10+1 selects List<NotaFiscal> notas = dao.listaTudo(); for (NotaFiscal nf : notas) { processaItensDaNota(nf); }
  158. select nf.* from NotaFiscal nf select i.* from Item i

    where i.nota_fiscal_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) select i.* from Item i where i.nota_fiscal_id in (?, ?, ?, ?, ?) Hibernate executa n/10+1 selects List<NotaFiscal> notas = dao.listaTudo(); for (NotaFiscal nf : notas) { processaItensDaNota(nf); }
  159. @BatchSize também é conhecido como:

  160. @BatchSize também é conhecido como: otimização de adivinhação cega
 (blind-guess

    optimization)
  161. @BatchSize também é conhecido como: otimização de adivinhação cega
 (blind-guess

    optimization) ou seja, é um palpite
  162. #3 FetchMode SUBSELECT

  163. @Entity class NotaFiscal { … @OneToMany @Fetch(FetchMode.SUBSELECT) List<Item> itens; }

    SUBSELECT
  164. select nf.* from NotaFiscal nf select i.* from Item i

    where i.nota_fiscal_id in (
 select nf.id from NotaFiscal nf ) Hibernate executa 2 selects List<NotaFiscal> notas = dao.listaTudo(); for (NotaFiscal nf : notas) { processaItensDaNota(nf); }
  165. select nf.* from NotaFiscal nf select i.* from Item i

    where i.nota_fiscal_id in (
 select nf.id from NotaFiscal nf ) Hibernate executa 2 selects List<NotaFiscal> notas = dao.listaTudo(); for (NotaFiscal nf : notas) { processaItensDaNota(nf); }
  166. Qual utilizar?

  167. Qual utilizar? depende

  168. #6 Processamento em lote

  169. Imagine que temos que importar 100k produtos para o banco

    de dados
  170. Session session = sf.openSession(); Transaction tx = session.beginTransaction(); for (

    int i=0; i < 100000; i++ ) { Produto produto = new Produto(...); session.save(produto); } tx.commit(); session.close();
  171. em ~50k produtos nós receberíamos um OutOfMemoryError

  172. por que?

  173. Hibernate faz cache de todas as instâncias dos Produtos inseridos

    na Session
  174. mas como melhorar?

  175. Session session = sf.openSession(); Transaction tx = session.beginTransaction(); for (

    int i=0; i < 100000; i++ ) { Produto produto = new Produto(...); session.save(produto); if (i % 100 == 0) { session.flush(); session.clear(); } } tx.commit(); session.close();
  176. evitamos o OutOfMemoryError

  177. evitamos o OutOfMemoryError Mas o processamento ainda continua lento!

  178. evitamos o OutOfMemoryError Mas o processamento ainda continua lento! Dá

    pra melhorar?
  179. JDBC puro?

  180. JDBC puro? código de maxu! Handerson Frota

  181. StatelessSession

  182. StatelessSession sem 1st Level Cache sem 2nd Level Cache sem

    dirty-checking sem cascade Collections são ignorados sem modelo de eventos sem interceptors próxima ao jdbc API mais baixo nível mapeamento básico
  183. StatelessSession session = sf.openStatelessSession(); Transaction tx = session.beginTransaction(); for (

    int i=0; i < 100000; i++ ) { Produto produto = new Produto(...); session.insert(produto); } tx.commit(); session.close();
  184. menos consumo de memória e mais rápida!

  185. menos consumo de memória e mais rápida! Yuri Adams dá

    pra melhorar?
  186. 
 hibernate.jdbc.batch_size=50 hibernate.properties

  187. CONCLUSÃO

  188. Foi apenas a ponta do iceberg!

  189. cada uma destas dicas são simples, mas requerem mais estudo

  190. cada uma destas dicas são simples, mas requerem mais estudo

    pois depende do projeto
  191. um DBA certamente pode te ajudar em muitos cenários

  192. Hibernate não é seu inimigo, deixem de #mimimi

  193. Rafael Ponte rponte@triadworks.com.br