001package io.ebean;
002
003import java.util.ArrayList;
004import java.util.Collections;
005import java.util.List;
006import java.util.Objects;
007
008/**
009 * Holds a list of value object pairs.
010 * <p>
011 * This feature is to enable use of L2 cache with complex natural keys with findList() queries in cases where the
012 * IN clause is not a single property but instead a pair of properties.
013 * </p>
014 * <p>
015 * These queries can have predicates that can be translated into a list of complex natural keys such that the L2
016 * cache can be hit with these keys to obtain some or all of the beans from L2 cache rather than the DB.
017 * </p>
018 * <pre>{@code
019 *
020 *   // where a bean is annotated with a complex
021 *   // natural key made of several properties
022 *   @Cache(naturalKey = {"store","code","sku"})
023 *
024 *
025 *   Pairs pairs = new Pairs("sku", "code");
026 *   pairs.add("sj2", 1000);
027 *   pairs.add("sj2", 1001);
028 *   pairs.add("pf3", 1000);
029 *
030 *   List<OCachedNatKeyBean3> list = DB.find(OCachedNatKeyBean3.class)
031 *   .where()
032 *   .eq("store", "def")
033 *   .inPairs(pairs)       // IN clause with 'pairs' of values
034 *   .order("sku desc")
035 *
036 *   // query expressions cover the natural key properties
037 *   // so we can choose to hit the L2 bean cache if we want
038 *   .setUseCache(true)
039 *   .findList();
040 *
041 * }</pre>
042 * <h3>Important implementation Note</h3>
043 * <p>
044 * When binding many pairs of values we want to be able to utilise a DB index (as this type of query usually means the
045 * pairs are a unique key/index or part of a unique key/index and highly selective). Currently we know we can do this
046 * on any DB that supports expression/formula based indexes.
047 * using a DB string concatenation formula
048 * </p>
049 * <p>
050 * This means, the implementation converts the list of pairs into a list of strings via concatenation and we use a
051 * DB concatenation formula to match. We see SQL like:
052 * </p>
053 * <pre>{@code sql
054 *
055 *   ...
056 *   where t0.store = ?  and (t0.sku||'-'||t0.code) in (?, ? )
057 *
058 *   // bind values like: "sj2-1000", "pf3-1000"
059 *
060 * }</pre>
061 * <p>
062 * We often create a DB expression index to match the DB concat formula like:
063 * </p>
064 * <pre>{@code sql
065 *
066 *   create index ix_name on table_name (sku || '-' || code);
067 *
068 * }</pre>
069 */
070public final class Pairs {
071
072  private final String property0;
073  private final String property1;
074  private final List<Entry> entries = new ArrayList<>();
075
076  /**
077   * Character between the values when combined via DB varchar concatenation.
078   */
079  private String concatSeparator = "-";
080
081  /**
082   * Optional suffix added to DB varchar concatenation formula.
083   */
084  private String concatSuffix;
085
086  /**
087   * Create with 2 property names.
088   *
089   * @param property0 The property of the first value
090   * @param property1 The property of the second value
091   */
092  public Pairs(String property0, String property1) {
093    this.property0 = property0;
094    this.property1 = property1;
095  }
096
097  /**
098   * Add a pair of value objects.
099   * <p>
100   * Both values are expected to be immutable with equals and hashCode implementations.
101   * </p>
102   *
103   * @param a Value of the first property
104   * @param b Value of the second property
105   */
106  public Pairs add(Object a, Object b) {
107    entries.add(new Entry(a, b));
108    return this;
109  }
110
111  /**
112   * Return the first property name.
113   */
114  public String property0() {
115    return property0;
116  }
117
118  /**
119   * Return the second property name.
120   */
121  public String property1() {
122    return property1;
123  }
124
125  /**
126   * Return all the value pairs.
127   */
128  public List<Entry> entries() {
129    return Collections.unmodifiableList(entries);
130  }
131
132  /**
133   * Return the separator character used with DB varchar concatenation to combine the 2 values.
134   */
135  public String concatSeparator() {
136    return concatSeparator;
137  }
138
139  /**
140   * Set the separator character used with DB varchar concatenation to combine the 2 values.
141   */
142  public Pairs concatSeparator(String concatSeparator) {
143    this.concatSeparator = concatSeparator;
144    return this;
145  }
146
147  /**
148   * Deprecated migrate to concatSeparator()
149   */
150  @Deprecated
151  public Pairs setConcatSeparator(String concatSeparator) {
152    return concatSeparator(concatSeparator);
153  }
154
155  /**
156   * Return  a suffix used with DB varchar concatenation to combine the 2 values.
157   */
158  public String concatSuffix() {
159    return concatSuffix;
160  }
161
162  /**
163   * Add a suffix used with DB varchar concatenation to combine the 2 values.
164   */
165  public Pairs concatSuffix(String concatSuffix) {
166    this.concatSuffix = concatSuffix;
167    return this;
168  }
169
170  /**
171   * Deprecated migrate to concatSuffix()
172   */
173  @Deprecated
174  public Pairs setConcatSuffix(String concatSuffix) {
175    return concatSuffix(concatSuffix);
176  }
177
178  @Override
179  public String toString() {
180    return "p0:" + property0 + " p1:" + property1 + " entries:" + entries;
181  }
182
183  /**
184   * A pair of 2 value objects.
185   * <p>
186   * Used to support inPairs() expression.
187   */
188  public static class Entry {
189
190    private final Object a;
191    private final Object b;
192
193    /**
194     * Create with values for property0 and property1 respectively.
195     *
196     * @param a Value of the first property
197     * @param b Value of the second property
198     */
199    public Entry(Object a, Object b) {
200      this.a = a;
201      this.b = b;
202    }
203
204    @Override
205    public String toString() {
206      return "{" + a + "," + b + "}";
207    }
208
209    /**
210     * Return the value for the first property.
211     */
212    public Object getA() {
213      return a;
214    }
215
216    /**
217     * Return the value for the second property.
218     */
219    public Object getB() {
220      return b;
221    }
222
223    @Override
224    public boolean equals(Object o) {
225      if (this == o) return true;
226      if (o == null || getClass() != o.getClass()) return false;
227      Entry that = (Entry) o;
228      return a.equals(that.a) && b.equals(that.b);
229    }
230
231    @Override
232    public int hashCode() {
233      return Objects.hash(a, b);
234    }
235  }
236}