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

com.google.cloud.spanner.hibernate.BitReversedSequenceStyleGenerator Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2019-2023 Google LLC
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */

package com.google.cloud.spanner.hibernate;

import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcAbortedDueToConcurrentModificationException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.Range;
import java.util.List;
import java.util.Properties;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.relational.Database;
import org.hibernate.boot.model.relational.InitCommand;
import org.hibernate.boot.model.relational.Namespace;
import org.hibernate.boot.model.relational.QualifiedName;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.exception.GenericJDBCException;
import org.hibernate.id.IdentifierGenerationException;
import org.hibernate.id.IdentifierGenerator;
import org.hibernate.id.enhanced.DatabaseStructure;
import org.hibernate.id.enhanced.SequenceStyleGenerator;
import org.hibernate.id.enhanced.TableStructure;
import org.hibernate.internal.util.config.ConfigurationHelper;
import org.hibernate.mapping.Table;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.type.Type;
import org.jboss.logging.Logger;

/**
 * DEPRECATED: Use {@link PooledBitReversedSequenceStyleGenerator} instead.
 *
 * 

Table backed ID generator that reverses the bits in the returned sequence value. * *

This generator uses a table to emulate a sequence. Cloud Spanner also * supports bit-reversed sequences that are stored and managed in the database. These are * recommended above this table backed solution. * *

Using a bit-reversed sequence for ID generation is recommended above sequences that return a * monotonically increasing value for Cloud Spanner. This generator also supports both an increment * size larger than 1 and an initial value larger than 1. * *

Use the BitReversedSequenceStyleGenerator.EXCLUDE_RANGES_PARAM to exclude ranges of values * that should be skipped by the generator if your entity table already contains data. The excluded * values should be given as space-separated closed ranges. E.g. "[1,1000] [2001,3000]" to skip all * values between 1 and 1000 (inclusive) and between 2001 and 3000 (inclusive). * *

It is recommended to use a separate table for each generator to prevent a large number of * writes for a single ID generator table. Set the table name to use for a generator with the * SequenceStyleGenerator.SEQUENCE_PARAM parameter (see example below). * *

Example usage: * *

{@code
 * @Id
 * @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "customerId")
 * @GenericGenerator(
 *       name = "customerId",
 *       strategy = "com.google.cloud.spanner.hibernate.BitReversedSequenceStyleGenerator",
 *       parameters = {
 *           @Parameter(name = SequenceStyleGenerator.SEQUENCE_PARAM, value = "customerId"),
 *           @Parameter(name = SequenceStyleGenerator.INCREMENT_PARAM, value = "1000"),
 *           @Parameter(name = SequenceStyleGenerator.INITIAL_PARAM, value = "50000"),
 *           @Parameter(name = BitReversedSequenceStyleGenerator.EXCLUDE_RANGES_PARAM,
 *                      value = "[1,1000] [10000,20000]"),
 *       })
 * @Column(nullable = false)
 * private Long customerId;
 * }
* * @deprecated Use #{link {@link PooledBitReversedSequenceStyleGenerator}} */ @Deprecated public class BitReversedSequenceStyleGenerator extends SequenceStyleGenerator { private static final Logger log = Logger.getLogger(BitReversedSequenceStyleGenerator.class); /** Specific implementation of a backing {@link TableStructure} for bit-reversed sequences. */ private static class SpannerSequenceTableStructure extends TableStructure { private final JdbcEnvironment jdbcEnvironment; private final QualifiedName qualifiedName; private final Identifier valueColumnNameIdentifier; private final int initialValue; public SpannerSequenceTableStructure( JdbcEnvironment jdbcEnvironment, String contributor, QualifiedName qualifiedTableName, Identifier valueColumnNameIdentifier, int initialValue, int incrementSize, Class numberType) { super( jdbcEnvironment, contributor, qualifiedTableName, valueColumnNameIdentifier, initialValue, incrementSize, numberType); this.jdbcEnvironment = jdbcEnvironment; this.qualifiedName = qualifiedTableName; this.valueColumnNameIdentifier = valueColumnNameIdentifier; this.initialValue = initialValue; } @Override public void registerExportables(Database database) { super.registerExportables(database); // Replace the init command for the table-backed sequence. // Hibernate by default generates an 'insert into table_name values (?)' statement. // That is not supported by Cloud Spanner, as Cloud Spanner requires the insert statement to // include the column name(s) that are being used in the insert statement. final Namespace namespace = database.locateNamespace(qualifiedName.getCatalogName(), qualifiedName.getSchemaName()); Table table = namespace.locateTable(qualifiedName.getObjectName()); if (table != null) { Dialect dialect = jdbcEnvironment.getDialect(); String valueColumnNameText = valueColumnNameIdentifier.render(dialect); table.addInitCommand( context -> new ReplaceInitCommand( "insert into " + context.format(table.getQualifiedTableName()) + " (" + valueColumnNameText + ") values ( " + initialValue + " )")); } } } /** * Acts as a replacement for other {@link InitCommand}. * *
    *
  1. If the list contains at least one {@link ReplaceInitCommand} and at least one normal * {@link InitCommand}, then all normal {@link InitCommand}s will be ignored during * execution and only the {@link ReplaceInitCommand}s will be executed. *
  2. If the list only contains {@link ReplaceInitCommand}s, nothing will be executed. *
  3. If the list only contains normal {@link InitCommand}s, all normal {@link InitCommand}s * will be executed as normal. *
*/ public static class ReplaceInitCommand extends InitCommand { public ReplaceInitCommand(String... initCommands) { super(initCommands); } } /** * Configuration property for defining ranges that should be excluded by a bit-reversed sequence * generator. */ public static final String EXCLUDE_RANGES_PARAM = "exclude_ranges"; private QualifiedName sequenceName; /** * A set of ranges that should be excluded from generation by this generator. This can be used to * make sure that existing values from a previous system are not re-generated by this generator. * The lower and upper bounds of the ranges are always closed. */ private List> excludeRanges = ImmutableList.of(); @Override protected DatabaseStructure buildTableStructure( Type type, Properties params, JdbcEnvironment jdbcEnvironment, QualifiedName sequenceName, int initialValue, int incrementSize) { this.sequenceName = sequenceName; final Identifier valueColumnName = determineValueColumnName(params, jdbcEnvironment); final String contributor = determineContributor(params); return new SpannerSequenceTableStructure( jdbcEnvironment, contributor, sequenceName, valueColumnName, initialValue, incrementSize, type.getReturnedClass()); } private String determineContributor(Properties params) { final String contributor = params.getProperty(IdentifierGenerator.CONTRIBUTOR_NAME); return contributor == null ? "orm" : contributor; } @Override public boolean supportsBulkInsertionIdentifierGeneration() { return false; } @Override public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException { // Force the use of a table-backed sequence for this generator, as Cloud Spanner does not // support regular sequences (only bit-reversed sequences). params.put(FORCE_TBL_PARAM, true); super.configure(type, params, serviceRegistry); configureExcludedRanges(this.sequenceName.getObjectName().getText(), params); } @VisibleForTesting static List> parseExcludedRanges(String sequenceName, Properties params) { String[] excludedRangesArray = ConfigurationHelper.toStringArray(EXCLUDE_RANGES_PARAM, " ", params); if (excludedRangesArray == null) { return ImmutableList.of(); } Builder> builder = ImmutableList.builder(); for (String rangeString : excludedRangesArray) { rangeString = rangeString.trim(); String invalidRangeMessage = String.format( "Invalid range found for the [%s] sequence: %%s\n" + "Excluded ranges must be given as a space-separated sequence of ranges between " + "square brackets, e.g. '[1,1000] [2001,3000]'. " + "Found '%s'", sequenceName, rangeString); if (!(rangeString.startsWith("[") && rangeString.endsWith("]"))) { throw new MappingException( String.format(invalidRangeMessage, "Range is not enclosed between '[' and ']'")); } rangeString = rangeString.substring(1, rangeString.length() - 1); String[] values = rangeString.split(","); if (values.length != 2) { throw new MappingException( String.format(invalidRangeMessage, "Range does not contain exactly two elements")); } long from; long to; try { from = Long.parseLong(values[0]); to = Long.parseLong(values[1]); builder.add(Range.closed(from, to)); } catch (IllegalArgumentException e) { throw new MappingException(String.format(invalidRangeMessage, e.getMessage()), e); } } return builder.build(); } @VisibleForTesting void configureExcludedRanges(String sequenceName, Properties params) throws MappingException { this.excludeRanges = parseExcludedRanges(sequenceName, params); } @VisibleForTesting static final int MAX_ATTEMPTS = 100; /** * Generates a new ID. This uses the normal sequence strategy, but the returned ID is bit-reversed * before it is returned to the application. */ @Override public Object generate(SharedSessionContractImplementor session, Object object) throws HibernateException { Object id; // Loop to skip excluded ranges. while (true) { int attempts = 0; // Loop to retry the transaction that updates the table-backed sequence if it fails due to a // concurrent modification error. This can happen if multiple entities are using the same // sequence for generated primary keys. while (true) { try { id = generateBaseValue(session, object); break; } catch (GenericJDBCException exception) { JdbcAbortedDueToConcurrentModificationException aborted; if (exception.getSQLException() instanceof JdbcAbortedDueToConcurrentModificationException) { aborted = (JdbcAbortedDueToConcurrentModificationException) exception.getSQLException(); } else { throw exception; } attempts++; if (attempts == MAX_ATTEMPTS) { throw exception; } try { sleep(aborted.getCause().getRetryDelayInMillis()); } catch (InterruptedException interruptedException) { Thread.currentThread().interrupt(); throw new IdentifierGenerationException( "Interrupted while trying to generate a new ID", interruptedException); } } } if (id instanceof Long) { Long reversed = Long.reverse((Long) id); if (excludeRanges.stream().noneMatch(range -> range.contains(reversed))) { return reversed; } log.debugf( "Skipping reversed id %d (original id %d) as it is in an excluded range", reversed, id); } else { return id; } } } @VisibleForTesting protected void sleep(long millis) throws InterruptedException { Thread.sleep(millis); } @VisibleForTesting protected Object generateBaseValue(SharedSessionContractImplementor session, Object object) { return super.generate(session, object); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy