Add a DAO for RegistryLock objects (#290)

* Add a DAO for RegistryLock objects

* Add an index on verification code and remove old file

* Move to v4

* Use camelCase in index names

* Javadoc fixes

* Allow alteration of RegistryLock objects in-place

* save, load-modify, read in separate transactions

* Change the creation timestamp to be a CreateAutoTimestamp
This commit is contained in:
gbrodman 2019-10-07 11:24:08 -04:00 committed by GitHub
parent b061d763df
commit 0855040bd7
5 changed files with 193 additions and 32 deletions

View file

@ -0,0 +1,52 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed 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 google.registry.model.registry;
import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
import google.registry.schema.domain.RegistryLock;
import javax.persistence.EntityManager;
/** Data access object for {@link google.registry.schema.domain.RegistryLock}. */
public final class RegistryLockDao {
/**
* Returns the most recent version of the {@link RegistryLock} referred to by the verification
* code (there may be two instances of the same code in the database--one after lock object
* creation and one after verification.
*/
public static RegistryLock getByVerificationCode(String verificationCode) {
return jpaTm()
.transact(
() -> {
EntityManager em = jpaTm().getEntityManager();
Long revisionId =
em.createQuery(
"SELECT MAX(revisionId) FROM RegistryLock WHERE verificationCode ="
+ " :verificationCode",
Long.class)
.setParameter("verificationCode", verificationCode)
.getSingleResult();
checkNotNull(revisionId, "No registry lock with this code");
return em.find(RegistryLock.class, revisionId);
});
}
public static void save(RegistryLock registryLock) {
checkNotNull(registryLock, "Null registry lock cannot be saved");
jpaTm().transact(() -> jpaTm().getEntityManager().persist(registryLock));
}
}

View file

@ -15,16 +15,18 @@
package google.registry.schema.domain;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.util.DateTimeUtils.toJodaDateTime;
import static google.registry.util.DateTimeUtils.toZonedDateTime;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
import google.registry.persistence.CreateAutoTimestampConverter;
import google.registry.util.DateTimeUtils;
import java.time.ZonedDateTime;
import java.util.Optional;
import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
@ -56,14 +58,17 @@ import org.joda.time.DateTime;
* Unique constraint to get around Hibernate's failure to handle auto-increment field in
* composite primary key.
*
* <p>Note: because of this index, physical columns must be declared in the {@link Column}
* annotations for {@link RegistryLock#revisionId} and {@link RegistryLock#repoId} fields.
* <p>Note: indexes use the camelCase version of the field names because the {@link
* google.registry.persistence.NomulusNamingStrategy} does not translate the field name into the
* snake_case column name until the write itself.
*/
indexes =
indexes = {
@Index(
name = "idx_registry_lock_repo_id_revision_id",
columnList = "repo_id, revision_id",
unique = true))
columnList = "repoId, revisionId",
unique = true),
@Index(name = "idx_registry_lock_verification_code", columnList = "verificationCode")
})
public final class RegistryLock extends ImmutableObject implements Buildable {
/** Describes the action taken by the user. */
@ -74,11 +79,11 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "revision_id", nullable = false)
@Column(nullable = false)
private Long revisionId;
/** EPP repo ID of the domain in question. */
@Column(name = "repo_id", nullable = false)
@Column(nullable = false)
private String repoId;
// TODO (b/140568328): remove this when everything is in Cloud SQL and we can join on "domain"
@ -104,7 +109,8 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
/** Creation timestamp is when the lock/unlock is first requested. */
@Column(nullable = false)
private ZonedDateTime creationTimestamp;
@Convert(converter = CreateAutoTimestampConverter.class)
private CreateAutoTimestamp creationTimestamp = CreateAutoTimestamp.create(null);
/**
* Completion timestamp is when the user has verified the lock/unlock, when this object de facto
@ -148,7 +154,7 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
}
public DateTime getCreationTimestamp() {
return toJodaDateTime(creationTimestamp);
return creationTimestamp.getTimestamp();
}
/** Returns the completion timestamp, or empty if this lock has not been completed yet. */
@ -168,9 +174,16 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
return revisionId;
}
public void setCompletionTimestamp(DateTime dateTime) {
this.completionTimestamp = toZonedDateTime(dateTime);
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
RegistryLock clone = clone(this);
// Revision ID should be different for every object
clone.revisionId = null;
return new Builder(clone);
}
/** Builder for {@link google.registry.schema.domain.RegistryLock}. */
@ -187,7 +200,6 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
checkArgumentNotNull(getInstance().domainName, "Domain name cannot be null");
checkArgumentNotNull(getInstance().registrarId, "Registrar ID cannot be null");
checkArgumentNotNull(getInstance().action, "Action cannot be null");
checkArgumentNotNull(getInstance().creationTimestamp, "Creation timestamp cannot be null");
checkArgumentNotNull(getInstance().verificationCode, "Verification codecannot be null");
checkArgument(
getInstance().registrarPocId != null || getInstance().isSuperuser,
@ -220,8 +232,8 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
return this;
}
public Builder setCreationTimestamp(DateTime creationTimestamp) {
getInstance().creationTimestamp = toZonedDateTime(creationTimestamp);
public Builder setCreationTimestamp(CreateAutoTimestamp creationTimestamp) {
getInstance().creationTimestamp = creationTimestamp;
return this;
}

View file

@ -0,0 +1,104 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed 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 google.registry.model.registry;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.testing.JUnitBackports.assertThrows;
import google.registry.model.transaction.JpaTransactionManagerRule;
import google.registry.schema.domain.RegistryLock;
import google.registry.schema.domain.RegistryLock.Action;
import google.registry.testing.AppEngineRule;
import java.util.UUID;
import javax.persistence.PersistenceException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link RegistryLockDao}. */
@RunWith(JUnit4.class)
public final class RegistryLockDaoTest {
@Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build();
@Rule
public final JpaTransactionManagerRule jpaTmRule =
new JpaTransactionManagerRule.Builder().withEntityClass(RegistryLock.class).build();
@Test
public void testSaveAndLoad_success() {
RegistryLock lock = createLock();
RegistryLockDao.save(lock);
RegistryLock fromDatabase = RegistryLockDao.getByVerificationCode(lock.getVerificationCode());
assertThat(fromDatabase.getDomainName()).isEqualTo(lock.getDomainName());
assertThat(fromDatabase.getVerificationCode()).isEqualTo(lock.getVerificationCode());
}
@Test
public void testSaveAndLoad_failure_differentCode() {
RegistryLock lock = createLock();
RegistryLockDao.save(lock);
PersistenceException exception =
assertThrows(
PersistenceException.class,
() -> RegistryLockDao.getByVerificationCode(UUID.randomUUID().toString()));
assertThat(exception)
.hasCauseThat()
.hasMessageThat()
.isEqualTo("No registry lock with this code");
assertThat(exception).hasCauseThat().isInstanceOf(NullPointerException.class);
}
@Test
public void testSaveTwiceAndLoad_returnsLatest() {
RegistryLock lock = createLock();
jpaTm().transact(() -> RegistryLockDao.save(lock));
jpaTmRule.getTxnClock().advanceOneMilli();
jpaTm()
.transact(
() -> {
RegistryLock secondLock =
RegistryLockDao.getByVerificationCode(lock.getVerificationCode());
secondLock.setCompletionTimestamp(jpaTmRule.getTxnClock().nowUtc());
RegistryLockDao.save(secondLock);
});
jpaTm()
.transact(
() -> {
RegistryLock fromDatabase =
RegistryLockDao.getByVerificationCode(lock.getVerificationCode());
assertThat(fromDatabase.getCompletionTimestamp().get())
.isEqualTo(jpaTmRule.getTxnClock().nowUtc());
});
}
@Test
public void testFailure_saveNull() {
assertThrows(NullPointerException.class, () -> RegistryLockDao.save(null));
}
private RegistryLock createLock() {
return new RegistryLock.Builder()
.setRepoId("repoId")
.setDomainName("example.test")
.setRegistrarId("TheRegistrar")
.setAction(Action.LOCK)
.setVerificationCode(UUID.randomUUID().toString())
.isSuperuser(true)
.build();
}
}

View file

@ -12,19 +12,5 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
CREATE TABLE "RegistryLock" (
revision_id BIGSERIAL NOT NULL,
action TEXT NOT NULL,
completion_timestamp TIMESTAMPTZ,
creation_timestamp TIMESTAMPTZ NOT NULL,
domain_name TEXT NOT NULL,
is_superuser BOOLEAN NOT NULL,
registrar_id TEXT NOT NULL,
registrar_poc_id TEXT,
repo_id TEXT NOT NULL,
verification_code TEXT NOT NULL,
PRIMARY KEY (revision_id)
);
ALTER TABLE IF EXISTS "RegistryLock"
ADD CONSTRAINT idx_registry_lock_repo_id_revision_id UNIQUE (repo_id, revision_id);
create index if not exists idx_registry_lock_verification_code ON "RegistryLock"
using btree (verification_code);

View file

@ -220,6 +220,13 @@ ALTER TABLE ONLY public."RegistryLock"
ADD CONSTRAINT idx_registry_lock_repo_id_revision_id UNIQUE (repo_id, revision_id);
--
-- Name: idx_registry_lock_verification_code; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX idx_registry_lock_verification_code ON public."RegistryLock" USING btree (verification_code);
--
-- Name: ClaimsEntry fk6sc6at5hedffc0nhdcab6ivuq; Type: FK CONSTRAINT; Schema: public; Owner: -
--