All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.morimekta.providence.jdbi.v2.MessageUpserter Maven / Gradle / Ivy

There is a newer version: 2.7.0
Show newest version
/*
 * Copyright 2018-2019 Providence Authors
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF 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 net.morimekta.providence.jdbi.v2;

import net.morimekta.providence.PMessage;
import net.morimekta.providence.PMessageOrBuilder;
import net.morimekta.providence.descriptor.PField;
import net.morimekta.providence.descriptor.PMessageDescriptor;
import net.morimekta.util.collect.UnmodifiableList;
import net.morimekta.util.collect.UnmodifiableMap;
import net.morimekta.util.collect.UnmodifiableSet;
import org.skife.jdbi.v2.Handle;
import org.skife.jdbi.v2.Update;

import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import static net.morimekta.providence.jdbi.v2.MessageFieldArgument.getDefaultColumnType;

/**
 * Helper class to handle inserting content from messages into a table.
 * The helper will only select values form the message itself, not using
 * nested structure or anything like that.
 *
 * The inserter is built in  such a way that you can create the inserter
 * (even as a static field), and use it any number of times with a handle
 * to do the pre-programmed insert. The execute method is thread safe, as
 * long as none of the modification methods are called.
 *
 * 
{@code
 * class MyInserter {
 *     private static final MessageUpserter<MyMessage,MyMessage._Field> INSERTER =
 *             new MessageUpserter.Builder<>("my_message")
 *                     .set(MyMessage.UUID, MyMessage.NAME)
 *                     .set("amount", MyMessage.VALUE, Types.INTEGER)  // DOUBLE -> INTEGER
 *                     .onDuplicateKeyUpdate(MyMessage.VALUE)
 *                     .build();
 *
 *     private final Jdbi dbi;
 *
 *     public MyInserter(Jdbi dbi) {
 *         this.dbi = dbi;
 *     }
 *
 *     int insert(HandleMyMessage... messages) {
 *         try (Handle handle = dbi.open()) {
 *             return INSERTER.execute(handle, messages);
 *         }
 *     }
 * }
 * }
* * Or it can be handled in line where needed. The building process is pretty cheap, * so this should not be a problem unless it is called a lot for very small * message. * *
{@code
 * class MyInserter {
 *     int insert(HandleMyMessage... messages) {
 *         try (Handle handle = dbi.open()) {
 *             return new MessageUpserter.Builder<MyMessage,MyMessage._Field>("my_message")
 *                     .set(MyMessage.UUID, MyMessage.NAME)
 *                     .set("amount", MyMessage.VALUE, Types.INTEGER)  // DOUBLE -> INTEGER
 *                     .onDuplicateKeyUpdateAllExcept(MyMessage.UUID)
 *                     .build()
 *                     .execute(handle, messages);
 *         }
 *     }
 * }
 * }
* * The rules for using this is pretty simple: * *
    *
  • * All fields set must be specified before onDuplicateKey* behavior. *
  • *
  • * Only one of onDuplicateKeyIgnore and onDuplicateKeyUpdate * can be set. *
  • *
  • * execute(...) can be called any number of times, and is thread safe. *
  • *
*/ public class MessageUpserter> { private final String queryPrefix; private final String querySuffix; private final Map> columnToFieldMap; private final Map columnTypeMap; private final List columnOrder; private final String valueMarkers; private MessageUpserter(String queryPrefix, String querySuffix, List columnOrder, Map> columnToFieldMap, Map columnTypeMap) { this.queryPrefix = queryPrefix; this.querySuffix = querySuffix; this.columnOrder = UnmodifiableList.copyOf(columnOrder); this.columnToFieldMap = UnmodifiableMap.copyOf(columnToFieldMap); this.columnTypeMap = UnmodifiableMap.copyOf(columnTypeMap); this.valueMarkers = "(" + columnOrder.stream() .map(k -> "?") .collect(Collectors.joining(",")) + ")"; } @Override public String toString() { return queryPrefix + " (...) " + querySuffix; } @SafeVarargs public final int execute(@Nonnull Handle handle, @Nonnull PMessageOrBuilder... items) { return execute(handle, UnmodifiableList.copyOf(items)); } public > int execute(@Nonnull Handle handle, @Nonnull Collection items) { if (items.isEmpty()) { return 0; } String query = queryPrefix + items.stream() .map(item -> valueMarkers) .collect(Collectors.joining(", ")) + querySuffix; Update update = handle.createStatement(query); int offset = 0; for (PMessageOrBuilder item : items) { for (String column : columnOrder) { PField field = columnToFieldMap.get(column); int type = columnTypeMap.get(column); update.bind(offset++, new MessageFieldArgument<>(item, field, type)); } } return update.execute(); } public static class Builder> { private final String intoTable; private final Map> columnToFieldMap; private final Map columnTypeMap; private final Set onDuplicateUpdate; private final AtomicBoolean onDuplicateIgnore; private final PMessageDescriptor descriptor; /** * Create a message inserter builder. * * @param descriptor The type descriptor. * @param intoTable The table name to insert info. */ public Builder(@Nonnull PMessageDescriptor descriptor, @Nonnull String intoTable) { this.descriptor = descriptor; this.intoTable = intoTable; this.columnToFieldMap = new LinkedHashMap<>(); this.columnTypeMap = new HashMap<>(); this.onDuplicateUpdate = new TreeSet<>(); this.onDuplicateIgnore = new AtomicBoolean(); } /** * Set all fields not already handled with default name and type. * * @return The builder. */ public final Builder setAll() { for (PField field : descriptor.getFields()) { // Not efficient, but should not be called often. if (!columnToFieldMap.values().contains(field)) { set(field); } } return this; } /** * Set all fields with defaults. * * @param except Fields to exclude. * @return The builder. */ @SafeVarargs public final Builder setAllExcept(PField... except) { return setAllExcept(UnmodifiableSet.copyOf(except)); } /** * Set all fields with defaults. * * @param except Fields to exclude. * @return The builder. */ public Builder setAllExcept(Collection> except) { for (PField field : descriptor.getFields()) { if (!except.contains(field)) { set(field.getName(), field, getDefaultColumnType(field)); } } return this; } /** * Set the specific fields with default name and type. * * @param fields The fields to be set. * @return The builder. */ @SafeVarargs public final Builder set(PField... fields) { return set(UnmodifiableList.copyOf(fields)); } /** * Set the specific fields with default name and type. * * @param fields The fields to be set. * @return The builder. */ public final Builder set(Collection> fields) { for (PField field : fields) { set(field.getName(), field, getDefaultColumnType(field)); } return this; } /** * Set the specific field with name and default type. * * @param column The column name to set. * @param field The field to be set. * @return The builder. */ public final Builder set(String column, PField field) { return set(column, field, getDefaultColumnType(field)); } /** * Set the specific field with specific type and default name. * * @param field The field to be set. * @param type The field type to set as. * @return The builder. */ public final Builder set(PField field, int type) { return set(field.getName(), field, type); } /** * Set the specific field with specific name and type. * * @param column The column name to set. * @param field The field to be set. * @param type The field type to set as. * @return The builder. */ public final Builder set(String column, PField field, int type) { if (columnToFieldMap.containsKey(column)) { throw new IllegalArgumentException("Column " + column + " already inserted"); } if (onDuplicateIgnore.get() || onDuplicateUpdate.size() > 0) { throw new IllegalStateException("Duplicate key behavior already determined"); } this.columnToFieldMap.put(column, field); this.columnTypeMap.put(column, type); return this; } /** * On duplicate keys update the given fields. * * @param fields The fields to update. * @return The builder. */ @SafeVarargs public final Builder onDuplicateKeyUpdate(PField... fields) { return onDuplicateKeyUpdate(UnmodifiableList.copyOf(fields)); } /** * On duplicate keys update the given fields. * * @param fields The fields to update. * @return The builder. */ public final Builder onDuplicateKeyUpdate(Collection> fields) { List columns = new ArrayList<>(fields.size()); fields.forEach(field -> { AtomicBoolean found = new AtomicBoolean(); columnToFieldMap.forEach((column, f) -> { if (f.equals(field)) { columns.add(column); found.set(true); } }); if (!found.get()) { throw new IllegalArgumentException("Field " + field + " not inserted"); } }); return onDuplicateKeyUpdate(columns.toArray(new String[0])); } /** * On duplicate keys update all except the given fields. * * @param fields The fields to NOT update. * @return The builder. */ @SafeVarargs public final Builder onDuplicateKeyUpdateAllExcept(PField... fields) { return onDuplicateKeyUpdateAllExcept(UnmodifiableList.copyOf(fields)); } /** * On duplicate keys update all except the given fields. * * @param fields The fields to NOT update. * @return The builder. */ public final Builder onDuplicateKeyUpdateAllExcept(Collection> fields) { List columns = new ArrayList<>(fields.size()); fields.forEach(field -> { AtomicBoolean found = new AtomicBoolean(); columnToFieldMap.forEach((column, f) -> { if (f.equals(field)) { columns.add(column); found.set(true); } }); if (!found.get()) { throw new IllegalArgumentException("Field " + field + " not inserted"); } }); return onDuplicateKeyUpdateAllExcept(columns.toArray(new String[0])); } /** * On duplicate keys update all except the given fields. * * @param exceptColumns The column names NOT to update. * @return The builder. */ public final Builder onDuplicateKeyUpdateAllExcept(String... exceptColumns) { TreeSet columns = new TreeSet<>(columnToFieldMap.keySet()); columns.removeAll(UnmodifiableList.copyOf(exceptColumns)); return onDuplicateKeyUpdate(columns.toArray(new String[0])); } /** * On duplicate keys update the given columns. * * @param columns The column names NOT to update. * @return The builder. */ public final Builder onDuplicateKeyUpdate(String... columns) { if (onDuplicateIgnore.get()) { throw new IllegalStateException("Duplicate key behavior already set to ignore"); } Collections.addAll(onDuplicateUpdate, columns); return this; } /** * On duplicate keys ignore updates. * * @return The builder. */ public final Builder onDuplicateKeyIgnore() { if (onDuplicateUpdate.size() > 0) { throw new IllegalStateException("Duplicate key behavior already set to update"); } onDuplicateIgnore.set(true); return this; } /** * @return The final built inserter. */ public MessageUpserter build() { if (columnToFieldMap.isEmpty()) { throw new IllegalStateException("No columns inserted"); } List columnOrder = new ArrayList<>(columnToFieldMap.keySet()); StringBuilder prefixBuilder = new StringBuilder("INSERT "); if (onDuplicateIgnore.get()) { prefixBuilder.append("IGNORE "); } prefixBuilder.append("INTO ") .append(intoTable) .append(" (") .append(columnOrder.stream() .map(col -> "`" + col + "`") .collect(Collectors.joining(", "))) .append(") VALUES "); StringBuilder suffixBuilder = new StringBuilder(); if (onDuplicateUpdate.size() > 0) { suffixBuilder.append(" ON DUPLICATE KEY UPDATE"); boolean first = true; for (String column : onDuplicateUpdate) { if (first) { first = false; } else { suffixBuilder.append(","); } suffixBuilder.append(" `") .append(column) .append("` = VALUES(`") .append(column) .append("`)"); } } return new MessageUpserter<>(prefixBuilder.toString(), suffixBuilder.toString(), columnOrder, columnToFieldMap, columnTypeMap); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy