001package io.ebean;
002
003import java.util.ArrayList;
004import java.util.Collections;
005import java.util.List;
006
007/**
008 * Holds a list of value object pairs.
009 * <p>
010 * This feature is to enable use of L2 cache with complex natural keys with findList() queries in cases where the
011 * IN clause is not a single property but instead a pair of properties.
012 * </p>
013 * <p>
014 * These queries can have predicates that can be translated into a list of complex natural keys such that the L2
015 * cache can be hit with these keys to obtain some or all of the beans from L2 cache rather than the DB.
016 * </p>
017 * <pre>{@code
018 *
019 *   // where a bean is annotated with a complex
020 *   // natural key made of several properties
021 *   @Cache(naturalKey = {"store","code","sku"})
022 *
023 *
024 *   Pairs pairs = new Pairs("sku", "code");
025 *   pairs.add("sj2", 1000);
026 *   pairs.add("sj2", 1001);
027 *   pairs.add("pf3", 1000);
028 *
029 *   List<OCachedNatKeyBean3> list = DB.find(OCachedNatKeyBean3.class)
030 *   .where()
031 *   .eq("store", "def")
032 *   .inPairs(pairs)       // IN clause with 'pairs' of values
033 *   .order("sku desc")
034 *
035 *   // query expressions cover the natural key properties
036 *   // so we can choose to hit the L2 bean cache if we want
037 *   .setUseCache(true)
038 *   .findList();
039 *
040 * }</pre>
041 * <h3>Important implementation Note</h3>
042 * <p>
043 * When binding many pairs of values we want to be able to utilise a DB index (as this type of query usually means the
044 * pairs are a unique key/index or part of a unique key/index and highly selective). Currently we know we can do this
045 * on any DB that supports expression/formula based indexes.
046 * using a DB string concatenation formula
047 * </p>
048 * <p>
049 * This means, the implementation converts the list of pairs into a list of strings via concatenation and we use a
050 * DB concatenation formula to match. We see SQL like:
051 * </p>
052 * <pre>{@code sql
053 *
054 *   ...
055 *   where t0.store = ?  and (t0.sku||'-'||t0.code) in (?, ? )
056 *
057 *   // bind values like: "sj2-1000", "pf3-1000"
058 *
059 * }</pre>
060 * <p>
061 * We often create a DB expression index to match the DB concat formula like:
062 * </p>
063 * <pre>{@code sql
064 *
065 *   create index ix_name on table_name ((sku || '-' || code));
066 *
067 * }</pre>
068 */
069public class Pairs {
070
071  private final String property0;
072  private final String property1;
073
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 getProperty0() {
115    return property0;
116  }
117
118  /**
119   * Return the second property name.
120   */
121  public String getProperty1() {
122    return property1;
123  }
124
125  /**
126   * Return all the value pairs.
127   */
128  public List<Entry> getEntries() {
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 getConcatSeparator() {
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 setConcatSeparator(String concatSeparator) {
143    this.concatSeparator = concatSeparator;
144    return this;
145  }
146
147  /**
148   * Return  a suffix used with DB varchar concatenation to combine the 2 values.
149   */
150  public String getConcatSuffix() {
151    return concatSuffix;
152  }
153
154  /**
155   * Add a suffix used with DB varchar concatenation to combine the 2 values.
156   */
157  public Pairs setConcatSuffix(String concatSuffix) {
158    this.concatSuffix = concatSuffix;
159    return this;
160  }
161
162  @Override
163  public String toString() {
164    return "p0:" + property0 + " p1:" + property1 + " entries:" + entries;
165  }
166
167  /**
168   * A pair of 2 value objects.
169   * <p>
170   * Used to support inPairs() expression.
171   */
172  public static class Entry {
173
174    private final Object a;
175    private final Object b;
176
177    /**
178     * Create with values for property0 and property1 respectively.
179     *
180     * @param a Value of the first property
181     * @param b Value of the second property
182     */
183    public Entry(Object a, Object b) {
184      this.a = a;
185      this.b = b;
186    }
187
188    @Override
189    public String toString() {
190      return "{" + a + "," + b + "}";
191    }
192
193    /**
194     * Return the value for the first property.
195     */
196    public Object getA() {
197      return a;
198    }
199
200    /**
201     * Return the value for the second property.
202     */
203    public Object getB() {
204      return b;
205    }
206
207    @Override
208    public boolean equals(Object o) {
209      if (this == o) return true;
210      if (o == null || getClass() != o.getClass()) return false;
211
212      Entry that = (Entry) o;
213      return a.equals(that.a) && b.equals(that.b);
214    }
215
216    @Override
217    public int hashCode() {
218      int result = a.hashCode();
219      result = 92821 * result + b.hashCode();
220      return result;
221    }
222  }
223}