killbill-memoizeit

Details

diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
index 3acf35f..f4a0491 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
@@ -87,9 +87,11 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Function;
+import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Iterators;
 import com.google.common.collect.Lists;
@@ -241,17 +243,45 @@ public class PaymentProcessor extends ProcessorBase {
 
     public Pagination<Payment> getPayments(final Long offset, final Long limit, final boolean withPluginInfo, final boolean withAttempts,
                                            final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) {
-        return getEntityPaginationFromPlugins(true,
-                                              getAvailablePlugins(),
-                                              offset,
-                                              limit,
-                                              new EntityPaginationBuilder<Payment, PaymentApiException>() {
-                                                  @Override
-                                                  public Pagination<Payment> build(final Long offset, final Long limit, final String pluginName) throws PaymentApiException {
-                                                      return getPayments(offset, limit, pluginName, withPluginInfo, withAttempts, properties, tenantContext, internalTenantContext);
-                                                  }
-                                              }
-                                             );
+        final Map<UUID, Optional<PaymentPluginApi>> paymentMethodIdToPaymentPluginApi = new HashMap<UUID, Optional<PaymentPluginApi>>();
+
+        try {
+            return getEntityPagination(limit,
+                                       new SourcePaginationBuilder<PaymentModelDao, PaymentApiException>() {
+                                           @Override
+                                           public Pagination<PaymentModelDao> build() {
+                                               // Find all payments for all accounts
+                                               return paymentDao.get(offset, limit, internalTenantContext);
+                                           }
+                                       },
+                                       new Function<PaymentModelDao, Payment>() {
+                                           @Override
+                                           public Payment apply(final PaymentModelDao paymentModelDao) {
+                                               final PaymentPluginApi pluginApi;
+                                               if (!withPluginInfo) {
+                                                   pluginApi = null;
+                                               } else {
+                                                   if (paymentMethodIdToPaymentPluginApi.get(paymentModelDao.getPaymentMethodId()) == null) {
+                                                       try {
+                                                           final PaymentPluginApi paymentProviderPlugin = getPaymentProviderPlugin(paymentModelDao.getPaymentMethodId(), internalTenantContext);
+                                                           paymentMethodIdToPaymentPluginApi.put(paymentModelDao.getPaymentMethodId(), Optional.<PaymentPluginApi>of(paymentProviderPlugin));
+                                                       } catch (final PaymentApiException e) {
+                                                           log.warn("Unable to retrieve PaymentPluginApi for paymentMethodId='{}'", paymentModelDao.getPaymentMethodId(), e);
+                                                           // We use Optional to avoid printing the log line for each result
+                                                           paymentMethodIdToPaymentPluginApi.put(paymentModelDao.getPaymentMethodId(), Optional.<PaymentPluginApi>absent());
+                                                       }
+                                                   }
+                                                   pluginApi = paymentMethodIdToPaymentPluginApi.get(paymentModelDao.getPaymentMethodId()).orNull();
+                                               }
+                                               final List<PaymentTransactionInfoPlugin> pluginInfo = getPaymentTransactionInfoPluginsIfNeeded(pluginApi, paymentModelDao, tenantContext);
+                                               return toPayment(paymentModelDao.getId(), pluginInfo, withAttempts, internalTenantContext);
+                                           }
+                                       }
+                                      );
+        } catch (final PaymentApiException e) {
+            log.warn("Unable to get payments", e);
+            return new DefaultPagination<Payment>(offset, limit, null, null, ImmutableSet.<Payment>of().iterator());
+        }
     }
 
     public Pagination<Payment> getPayments(final Long offset, final Long limit, final String pluginName, final boolean withPluginInfo, final boolean withAttempts, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java
index 5ed95a9..7e010b7 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PaymentStateMachineHelper.java
@@ -78,6 +78,35 @@ public class PaymentStateMachineHelper {
 
     private final StateMachineConfigCache stateMachineConfigCache;
 
+    public static final String[] STATE_NAMES = {AUTH_ERRORED,
+                                                AUTHORIZE_FAILED,
+                                                AUTHORIZE_PENDING,
+                                                AUTHORIZE_SUCCESS,
+                                                CAPTURE_ERRORED,
+                                                CAPTURE_FAILED,
+                                                CAPTURE_PENDING,
+                                                CAPTURE_SUCCESS,
+                                                CHARGEBACK_ERRORED,
+                                                CHARGEBACK_FAILED,
+                                                CHARGEBACK_PENDING,
+                                                CHARGEBACK_SUCCESS,
+                                                CREDIT_ERRORED,
+                                                CREDIT_FAILED,
+                                                CREDIT_PENDING,
+                                                CREDIT_SUCCESS,
+                                                PURCHASE_ERRORED,
+                                                PURCHASE_FAILED,
+                                                PURCHASE_PENDING,
+                                                PURCHASE_SUCCESS,
+                                                REFUND_ERRORED,
+                                                REFUND_FAILED,
+                                                REFUND_PENDING,
+                                                REFUND_SUCCESS,
+                                                VOID_ERRORED,
+                                                VOID_FAILED,
+                                                VOID_PENDING,
+                                                VOID_SUCCESS};
+
     @Inject
     public PaymentStateMachineHelper(final StateMachineConfigCache stateMachineConfigCache) {
         this.stateMachineConfigCache = stateMachineConfigCache;
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
index 0e67704..bed7b5b 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/DefaultPaymentDao.java
@@ -19,16 +19,19 @@
 package org.killbill.billing.payment.dao;
 
 import java.math.BigDecimal;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.List;
 import java.util.UUID;
+import java.util.regex.Pattern;
 
 import javax.annotation.Nullable;
 import javax.inject.Inject;
 
 import org.joda.time.DateTime;
+import org.killbill.billing.ErrorCode;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.Currency;
@@ -38,10 +41,12 @@ import org.killbill.billing.payment.api.DefaultPaymentErrorEvent;
 import org.killbill.billing.payment.api.DefaultPaymentInfoEvent;
 import org.killbill.billing.payment.api.DefaultPaymentPluginErrorEvent;
 import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentApiException;
 import org.killbill.billing.payment.api.PaymentMethod;
 import org.killbill.billing.payment.api.PaymentTransaction;
 import org.killbill.billing.payment.api.TransactionStatus;
 import org.killbill.billing.payment.api.TransactionType;
+import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
 import org.killbill.billing.util.cache.CacheControllerDispatcher;
 import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.dao.NonEntityDao;
@@ -49,6 +54,7 @@ import org.killbill.billing.util.entity.Entity;
 import org.killbill.billing.util.entity.Pagination;
 import org.killbill.billing.util.entity.dao.DefaultPaginationSqlDaoHelper;
 import org.killbill.billing.util.entity.dao.DefaultPaginationSqlDaoHelper.PaginationIteratorBuilder;
+import org.killbill.billing.util.entity.dao.EntityDaoBase;
 import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
 import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
 import org.killbill.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
@@ -66,11 +72,10 @@ import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 
-public class DefaultPaymentDao implements PaymentDao {
+public class DefaultPaymentDao extends EntityDaoBase<PaymentModelDao, Payment, PaymentApiException> implements PaymentDao {
 
     private static final Logger log = LoggerFactory.getLogger(DefaultPaymentDao.class);
 
-    private final EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao;
     private final DefaultPaginationSqlDaoHelper paginationHelper;
     private final PersistentBus eventBus;
     private final Clock clock;
@@ -78,7 +83,7 @@ public class DefaultPaymentDao implements PaymentDao {
     @Inject
     public DefaultPaymentDao(final IDBI dbi, final Clock clock, final CacheControllerDispatcher cacheControllerDispatcher,
                              final NonEntityDao nonEntityDao, final InternalCallContextFactory internalCallContextFactory, final PersistentBus eventBus) {
-        this.transactionalSqlDao = new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao, internalCallContextFactory);
+        super(new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao, internalCallContextFactory), PaymentSqlDao.class);
         this.paginationHelper = new DefaultPaginationSqlDaoHelper(transactionalSqlDao);
         this.eventBus = eventBus;
         this.clock = clock;
@@ -248,16 +253,20 @@ public class DefaultPaymentDao implements PaymentDao {
 
     @Override
     public Pagination<PaymentModelDao> searchPayments(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
+        // Optimization: if the search key looks like a state name (e.g. _ERRORED), assume the user is searching by state only
+        final List<String> paymentStates = shouldSearchByStateNameOnly(searchKey);
+
+        final String likeSearchKey = String.format("%%%s%%", searchKey);
         return paginationHelper.getPagination(PaymentSqlDao.class,
                                               new PaginationIteratorBuilder<PaymentModelDao, Payment, PaymentSqlDao>() {
                                                   @Override
                                                   public Long getCount(final PaymentSqlDao paymentSqlDao, final InternalTenantContext context) {
-                                                      return paymentSqlDao.getSearchCount(searchKey, String.format("%%%s%%", searchKey), context);
+                                                      return !paymentStates.isEmpty() ? paymentSqlDao.getSearchByStateCount(paymentStates, context) : paymentSqlDao.getSearchCount(searchKey, likeSearchKey, context);
                                                   }
 
                                                   @Override
                                                   public Iterator<PaymentModelDao> build(final PaymentSqlDao paymentSqlDao, final Long limit, final InternalTenantContext context) {
-                                                      return paymentSqlDao.search(searchKey, String.format("%%%s%%", searchKey), offset, limit, context);
+                                                      return !paymentStates.isEmpty() ? paymentSqlDao.searchByState(paymentStates, offset, limit, context) : paymentSqlDao.search(searchKey, likeSearchKey, offset, limit, context);
                                                   }
                                               },
                                               offset,
@@ -265,6 +274,20 @@ public class DefaultPaymentDao implements PaymentDao {
                                               context);
     }
 
+    private List<String> shouldSearchByStateNameOnly(final String searchKey) {
+        final Pattern pattern = Pattern.compile(".*" + searchKey + ".*");
+
+        // Note that technically, we should look at all of the available state names in the database instead since the state machine is configurable. The common use-case
+        // is to override transitions though, not to introduce new states, and since some of it is already hardcoded in PaymentStateMachineHelper anyways, it's probably good enough for now.
+        final List<String> stateNames = new ArrayList<String>();
+        for (final String stateName : PaymentStateMachineHelper.STATE_NAMES) {
+            if (pattern.matcher(stateName).matches()) {
+                stateNames.add(stateName);
+            }
+        }
+        return stateNames;
+    }
+
     @Override
     public PaymentModelDao insertPaymentWithFirstTransaction(final PaymentModelDao payment, final PaymentTransactionModelDao paymentTransaction, final InternalCallContext context) {
 
@@ -650,4 +673,9 @@ public class DefaultPaymentDao implements PaymentDao {
     private InternalCallContext contextWithUpdatedDate(final InternalCallContext input) {
         return new InternalCallContext(input, clock.getUTCNow());
     }
+
+    @Override
+    protected PaymentApiException generateAlreadyExistsException(final PaymentModelDao entity, final InternalCallContext context) {
+        return new PaymentApiException(ErrorCode.PAYMENT_INTERNAL_ERROR, "Payment already exists");
+    }
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
index dc1f8f7..2c46e15 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentDao.java
@@ -24,11 +24,14 @@ import org.joda.time.DateTime;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentApiException;
 import org.killbill.billing.payment.api.TransactionStatus;
 import org.killbill.billing.payment.api.TransactionType;
 import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.EntityDao;
 
-public interface PaymentDao {
+public interface PaymentDao extends EntityDao<PaymentModelDao, Payment, PaymentApiException> {
 
     public Pagination<PaymentTransactionModelDao> getByTransactionStatusAcrossTenants(final Iterable<TransactionStatus> transactionStatuses, DateTime createdBeforeDate, DateTime createdAfterDate, final Long offset, final Long limit);
 
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentSqlDao.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentSqlDao.java
index 0f5475b..23dcd3f 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentSqlDao.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentSqlDao.java
@@ -33,7 +33,6 @@ import org.skife.jdbi.v2.sqlobject.Bind;
 import org.skife.jdbi.v2.sqlobject.BindBean;
 import org.skife.jdbi.v2.sqlobject.SqlQuery;
 import org.skife.jdbi.v2.sqlobject.SqlUpdate;
-import org.skife.jdbi.v2.sqlobject.customizers.Define;
 
 @EntitySqlDaoStringTemplate
 public interface PaymentSqlDao extends EntitySqlDao<PaymentModelDao, Payment> {
@@ -69,6 +68,17 @@ public interface PaymentSqlDao extends EntitySqlDao<PaymentModelDao, Payment> {
 
     @SqlQuery
     @SmartFetchSize(shouldStream = true)
+    public Iterator<PaymentModelDao> searchByState(@PaymentStateCollectionBinder final Collection<String> paymentStates,
+                                                   @Bind("offset") final Long offset,
+                                                   @Bind("rowCount") final Long rowCount,
+                                                   @BindBean final InternalTenantContext context);
+
+    @SqlQuery
+    public Long getSearchByStateCount(@PaymentStateCollectionBinder final Collection<String> paymentStates,
+                                      @BindBean final InternalTenantContext context);
+
+    @SqlQuery
+    @SmartFetchSize(shouldStream = true)
     public Iterator<PaymentModelDao> getByPluginName(@Bind("pluginName") final String pluginName,
                                                      @Bind("offset") final Long offset,
                                                      @Bind("rowCount") final Long rowCount,
diff --git a/payment/src/main/java/org/killbill/billing/payment/dao/PaymentStateCollectionBinder.java b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentStateCollectionBinder.java
new file mode 100644
index 0000000..addde2a
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/dao/PaymentStateCollectionBinder.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.payment.dao;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Collection;
+
+import org.killbill.billing.payment.dao.PaymentStateCollectionBinder.PaymentStateCollectionBinderFactory;
+import org.skife.jdbi.v2.SQLStatement;
+import org.skife.jdbi.v2.sqlobject.Binder;
+import org.skife.jdbi.v2.sqlobject.BinderFactory;
+import org.skife.jdbi.v2.sqlobject.BindingAnnotation;
+
+@BindingAnnotation(PaymentStateCollectionBinderFactory.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.PARAMETER})
+public @interface PaymentStateCollectionBinder {
+
+    public static class PaymentStateCollectionBinderFactory implements BinderFactory {
+
+        @Override
+        public Binder build(final Annotation annotation) {
+            return new Binder<PaymentStateCollectionBinder, Collection<String>>() {
+
+                @Override
+                public void bind(final SQLStatement<?> query, final PaymentStateCollectionBinder bind, final Collection<String> allPaymentState) {
+                    query.define("states", allPaymentState);
+
+                    int idx = 0;
+                    for (final String paymentState : allPaymentState) {
+                        query.bind("state_" + idx, paymentState);
+                        idx++;
+                    }
+                }
+            };
+        }
+    }
+}
diff --git a/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentSqlDao.sql.stg b/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentSqlDao.sql.stg
index 6b110e9..bf30477 100644
--- a/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentSqlDao.sql.stg
+++ b/payment/src/main/resources/org/killbill/billing/payment/dao/PaymentSqlDao.sql.stg
@@ -80,7 +80,33 @@ searchQuery(prefix) ::= <<
   or <prefix>account_id = :searchKey
   or <prefix>payment_method_id = :searchKey
   or <prefix>external_key like :likeSearchKey
-  or <prefix>state_name like :likeSearchKey
+>>
+
+searchByState(states) ::= <<
+select
+<allTableFields("t.")>
+from <tableName()> t
+join (
+  select <recordIdField()>
+  from <tableName()>
+  where state_name in (<states: {state | :state_<i0>}; separator="," >)
+  <AND_CHECK_TENANT()>
+  <andCheckSoftDeletionWithComma()>
+  order by <recordIdField()>
+  limit :rowCount offset :offset
+) optimization on <recordIdField("optimization.")> = <recordIdField("t.")>
+order by <recordIdField("t.")>
+;
+>>
+
+getSearchByStateCount(states) ::= <<
+select
+  count(1) as count
+from <tableName()> t
+where t.state_name in (<states: {state | :state_<i0>}; separator="," >)
+<andCheckSoftDeletionWithComma("t.")>
+<AND_CHECK_TENANT("t.")>
+;
 >>
 
 getByPluginName() ::= <<
diff --git a/payment/src/main/resources/org/killbill/billing/payment/ddl.sql b/payment/src/main/resources/org/killbill/billing/payment/ddl.sql
index 019e3cf..e994fa9 100644
--- a/payment/src/main/resources/org/killbill/billing/payment/ddl.sql
+++ b/payment/src/main/resources/org/killbill/billing/payment/ddl.sql
@@ -122,6 +122,7 @@ CREATE UNIQUE INDEX payments_id ON payments(id);
 CREATE UNIQUE INDEX payments_key ON payments(external_key, tenant_record_id);
 CREATE INDEX payments_accnt ON payments(account_id);
 CREATE INDEX payments_tenant_account_record_id ON payments(tenant_record_id, account_record_id);
+CREATE INDEX payments_tenant_record_id_state_name ON payments(tenant_record_id, state_name);
 
 
 DROP TABLE IF EXISTS payment_history;
diff --git a/payment/src/main/resources/org/killbill/billing/payment/migration/V20170123023324__add_payments_tenant_record_id_state_name_index.sql b/payment/src/main/resources/org/killbill/billing/payment/migration/V20170123023324__add_payments_tenant_record_id_state_name_index.sql
new file mode 100644
index 0000000..11ba386
--- /dev/null
+++ b/payment/src/main/resources/org/killbill/billing/payment/migration/V20170123023324__add_payments_tenant_record_id_state_name_index.sql
@@ -0,0 +1 @@
+alter table payments add index payments_tenant_record_id_state_name(tenant_record_id, state_name);
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
index 0798535..12ce2be 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/MockPaymentDao.java
@@ -34,16 +34,19 @@ import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.dao.MockNonEntityDao;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentApiException;
 import org.killbill.billing.payment.api.TransactionStatus;
 import org.killbill.billing.payment.api.TransactionType;
 import org.killbill.billing.util.entity.DefaultPagination;
 import org.killbill.billing.util.entity.Pagination;
+import org.killbill.billing.util.entity.dao.MockEntityDaoBase;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 
-public class MockPaymentDao implements PaymentDao {
+public class MockPaymentDao extends MockEntityDaoBase<PaymentModelDao, Payment, PaymentApiException> implements PaymentDao {
 
     private final Map<UUID, PaymentModelDao> payments = new HashMap<UUID, PaymentModelDao>();
     private final Map<UUID, PaymentTransactionModelDao> transactions = new HashMap<UUID, PaymentTransactionModelDao>();
diff --git a/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java b/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java
index 4d6a938..35d2c42 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dao/TestDefaultPaymentDao.java
@@ -27,6 +27,7 @@ import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
 import org.killbill.billing.payment.api.TransactionStatus;
 import org.killbill.billing.payment.api.TransactionType;
+import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -37,11 +38,11 @@ public class TestDefaultPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
     @Test(groups = "slow")
     public void testPaymentCRUD() throws Exception {
         for (int i = 0; i < 3; i++) {
-            testPaymentCRUDForAccount();
+            testPaymentCRUDForAccount(i + 1);
         }
     }
 
-    public void testPaymentCRUDForAccount() throws Exception {
+    private void testPaymentCRUDForAccount(final int runNb) throws Exception {
         final Account account = testHelper.createTestAccount(UUID.randomUUID().toString(), true);
         final UUID accountId = account.getId();
 
@@ -68,8 +69,8 @@ public class TestDefaultPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
                                                            specifiedSecondPaymentTransactionModelDao.getAttemptId(),
                                                            specifiedSecondPaymentTransactionModelDao.getPaymentId(),
                                                            specifiedFirstPaymentTransactionModelDao.getTransactionType(),
-                                                           "SOME_ERRORED_STATE",
-                                                           "SOME_ERRORED_STATE",
+                                                           PaymentStateMachineHelper.STATE_NAMES[0],
+                                                           PaymentStateMachineHelper.STATE_NAMES[0],
                                                            specifiedSecondPaymentTransactionModelDao.getId(),
                                                            TransactionStatus.PAYMENT_FAILURE,
                                                            processedAmount,
@@ -99,6 +100,7 @@ public class TestDefaultPaymentDao extends PaymentTestSuiteWithEmbeddedDB {
 
         // Verify search APIs
         Assert.assertEquals(ImmutableList.<PaymentModelDao>copyOf(paymentDao.searchPayments(accountId.toString(), 0L, 100L, internalCallContext).iterator()).size(), 4);
+        Assert.assertEquals(ImmutableList.<PaymentModelDao>copyOf(paymentDao.searchPayments("_ERRORED", 0L, 100L, internalCallContext).iterator()).size(), runNb);
     }
 
     private void verifyPaymentAndTransactions(final InternalCallContext accountCallContext, final PaymentModelDao specifiedFirstPaymentModelDao, final PaymentTransactionModelDao... specifiedFirstPaymentTransactionModelDaos) {
diff --git a/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg b/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg
index d05e54e..6f35450 100644
--- a/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg
+++ b/util/src/main/resources/org/killbill/billing/util/entity/dao/EntitySqlDao.sql.stg
@@ -151,10 +151,15 @@ get(offset, rowCount, orderBy) ::= <<
 select
 <allTableFields("t.")>
 from <tableName()> t
-where <CHECK_TENANT("t.")>
-<andCheckSoftDeletionWithComma("t.")>
+join (
+  select <recordIdField()>
+  from <tableName()>
+  where <CHECK_TENANT()>
+  <andCheckSoftDeletionWithComma()>
+  order by <orderBy>
+  limit :rowCount offset :offset
+) optimization on <recordIdField("optimization.")> = <recordIdField("t.")>
 order by t.<orderBy>
-limit :rowCount offset :offset
 ;
 >>
 
diff --git a/util/src/test/java/org/killbill/billing/util/dao/TestStringTemplateInheritance.java b/util/src/test/java/org/killbill/billing/util/dao/TestStringTemplateInheritance.java
index 7f1627c..cc50c48 100644
--- a/util/src/test/java/org/killbill/billing/util/dao/TestStringTemplateInheritance.java
+++ b/util/src/test/java/org/killbill/billing/util/dao/TestStringTemplateInheritance.java
@@ -123,9 +123,14 @@ public class TestStringTemplateInheritance extends UtilTestSuiteNoDB {
                                                                                                                                                           ", t.account_record_id\r?\n" +
                                                                                                                                                           ", t.tenant_record_id\r?\n" +
                                                                                                                                                           "from kombucha t\r?\n" +
-                                                                                                                                                          "where t.tenant_record_id = :tenantRecordId\r?\n" +
+                                                                                                                                                          "join \\(\r?\n" +
+                                                                                                                                                          "  select record_id\r?\n" +
+                                                                                                                                                          "  from kombucha\r?\n" +
+                                                                                                                                                          "  where tenant_record_id = :tenantRecordId\r?\n" +
+                                                                                                                                                          "  order by record_id\r?\n" +
+                                                                                                                                                          "  limit :rowCount offset :offset\r?\n" +
+                                                                                                                                                          "\\) optimization on optimization.record_id = t.record_id\r?\n" +
                                                                                                                                                           "order by t.record_id\r?\n" +
-                                                                                                                                                          "limit :rowCount offset :offset\r?\n" +
                                                                                                                                                           ";");
         assertPattern(kombucha.getInstanceOf("test").toString(), "select\r?\n" +
                                                                  "  t.record_id\r?\n" +