问题
I'm using JPA, by way of EclipseLink. In my unit tests, I'd like to test how many SQL queries were performed during an operation. That way, if a later modification causes the query count to explode (if lazy loading is triggered, for instance), the unit test will flag it as potentially needing optimization.
I'm striking out in finding the correct API to do this. A pure-JPA solution would be ideal, but I'm fine with using EclipseLink-specific APIs in my unit tests. I looked at the EclipseLink profiler, but it doesn't seem to give me a way to count the number of SQL queries.
Thanks in advance for the help!
回答1:
I didn't find a proper tool for such validation and created my own. It is called sniffy and available under MIT license.
You can assert the number of generated queries like shown below:
// Integrate Sniffy to your test using @Rule annotation and a QueryCounter field
@Rule
public final QueryCounter queryCounter = new QueryCounter();
// Now just add @Expectation or @Expectations annotations to define number of queries allowed for given method
@Test
@Expectation(1)
public void testJUnitIntegration() throws SQLException {
// Just add sniffer: in front of your JDBC connection URL in order to enable sniffer
final Connection connection = DriverManager.getConnection("sniffer:jdbc:h2:mem:", "sa", "sa");
// Do not make any changes in your code - just add the @Rule QueryCounter and put annotations on your test method
connection.createStatement().execute("SELECT 1 FROM DUAL");
}
More information about integration with JUnit available in project wiki
回答2:
Most databases have built-in statistics, you might consider using those.
E.g. MySQL has SHOW STATUS LIKE 'Queries' command which dumps total amount of queries run.
回答3:
Sniffy looks like a nice tool and maybe I will give it try in a future.
Below you can find how to handle profiling using only eclipseLink. Whole code can be found here
import io.github.ssledz.domain.{Employee, Project}
import javax.persistence.{EntityManagerFactory, Persistence, PersistenceUnitUtil}
import org.eclipse.persistence.annotations.BatchFetchType
import org.eclipse.persistence.config.QueryHints
import org.eclipse.persistence.internal.jpa.EntityManagerFactoryDelegate
import org.eclipse.persistence.queries.LoadGroup
import org.eclipse.persistence.sessions.SessionProfiler
import org.eclipse.persistence.tools.profiler.PerformanceMonitor
import scala.jdk.CollectionConverters._
/**
* This example requires that all entities are properly waved by eclipseLink
*
* In order to make eclipseLink happy download java agent
* wget -O /tmp/eclipselink.jar https://repo1.maven.org/maven2/org/eclipse/persistence/eclipselink/2.7.7/eclipselink-2.7.7.jar
* pass following parameter -javaagent:/tmp/eclipselink.jar to jvm
* and set 'eclipselink.weaving' to true in persistance.xml
*
*/
object JpaQueryProfilingExample extends App {
val emf: EntityManagerFactory = Persistence.createEntityManagerFactory("default")
val pUtil: PersistenceUnitUtil = emf.getPersistenceUnitUtil
val emfd: EntityManagerFactoryDelegate = emf.unwrap(classOf[EntityManagerFactoryDelegate])
val profiler: PerformanceMonitor = emfd.getServerSession.getProfiler.asInstanceOf[PerformanceMonitor]
val em = emf.createEntityManager()
clearDb()
val johny = createEmployee("Johny", "Bravo", "eclipse-link-playground")
val tom = createEmployee("Tom", "Tip", "eclipse-link-profiling")
em.clear()
emf.getCache.evictAll()
val stats = QueryStatistics(profiler)
val employees = findAll()
def numberOfDbQueries: Int = QueryStatistics(profiler).diff(stats).numberOfDbQueries
assert(employees.size == 2)
assert(numberOfDbQueries == 1)
assertNotLoaded(employees.head, "projects")
employees.foreach(_.getProjects) // trigger lazy loading
assert(numberOfDbQueries == 3, "+2 for lazy loaded project")
private def clearDb(): Unit = {
val tx = em.getTransaction
tx.begin()
em.createQuery("delete from Employee").executeUpdate()
em.createQuery("delete from Project").executeUpdate()
tx.commit()
}
private def findAll(): List[Employee] =
em.createQuery("select e from Employee e", classOf[Employee]).getResultList.asScala.toList
private def findAllWithProjects(): List[Employee] = {
val query = em.createQuery("select e from Employee e", classOf[Employee])
query.setHint(QueryHints.LOAD_GROUP, loadGroup("projects"))
query.setHint(QueryHints.BATCH_TYPE, BatchFetchType.EXISTS)
query.setHint(QueryHints.BATCH, "e.projects")
query.getResultList.asScala.toList
}
private def createEmployee(firstName: String, lastName: String, projectName: String): Employee = {
val employee = new Employee
employee.setFirstName(firstName)
employee.setLastName(lastName)
val project = employee.addProject(Project(projectName))
val tx = em.getTransaction
tx.begin()
em.persist(employee)
em.persist(project)
tx.commit()
employee
}
private def loadGroup(attributes: String*): LoadGroup = {
val lg = new LoadGroup()
attributes.foreach(lg.addAttribute)
lg
}
private def assertLoaded(obj: AnyRef, attributeName: String): Unit = assertLoadedOrNot(obj, attributeName, true)
private def assertNotLoaded(obj: AnyRef, attributeName: String): Unit = assertLoadedOrNot(obj, attributeName, false)
private def assertLoadedOrNot(obj: AnyRef, attributeName: String, loaded: Boolean): Unit = {
val message = s"$attributeName property should be ${if (loaded) "eagerly" else "lazy"} loaded"
assert(pUtil.isLoaded(obj, attributeName) == loaded, s"because $message")
}
}
case class QueryStatistics(readAllQuery: Int, readObjectQuery: Int, cacheHits: Int) {
def numberOfDbQueries: Int = (readObjectQuery + readAllQuery) - cacheHits
def diff(other: QueryStatistics): QueryStatistics =
QueryStatistics(
readAllQuery - other.readAllQuery,
readObjectQuery - other.readObjectQuery,
cacheHits - other.cacheHits
)
}
object QueryStatistics {
def nullToZero(a: Any): Int = Option(a).map(_.toString.toInt).getOrElse(0)
def apply(pm: PerformanceMonitor): QueryStatistics =
new QueryStatistics(
nullToZero(pm.getOperationTimings.get("Counter:ReadAllQuery")),
nullToZero(pm.getOperationTimings.get("Counter:ReadObjectQuery")),
nullToZero(pm.getOperationTimings.get(SessionProfiler.CacheHits))
)
}
persistance.xml
<persistence>
<persistence-unit name="default" transaction-type="RESOURCE_LOCAL">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<class>io.github.ssledz.domain.Employee</class>
<class>io.github.ssledz.domain.Project</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<!-- https://www.eclipse.org/eclipselink/documentation/2.6/jpa/extensions/persistenceproperties_ref.htm#CACDCCEG2 -->
<properties>
<property name="eclipselink.profiler" value="PerformanceMonitor"/>
<property name="eclipselink.weaving" value="true"/>
<property name="eclipselink.logging.thread" value="true"/>
<property name="eclipselink.logging.session" value="true"/>
<property name="eclipselink.logging.timestamp" value="true"/>
<property name="eclipselink.logging.level" value="ALL"/>
<property name="eclipselink.logging.level.sql" value="ALL"/>
<property name="eclipselink.logging.parameters" value="true"/>
<!-- <property name="eclipselink.logging.logger" value="org.eclipse.persistence.logging.slf4j.SLF4JLogger"/>-->
<property name="eclipselink.ddl-generation" value="create-tables"/>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/eclipse_link_test?serverTimezone=UTC"/>
<property name="javax.persistence.jdbc.user" value="test"/>
<property name="javax.persistence.jdbc.password" value="test"/>
</properties>
</persistence-unit>
</persistence>
来源:https://stackoverflow.com/questions/22662833/eclipselink-jpa-how-to-programmatically-get-the-number-of-sql-queries-that-ha