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; package google.registry.schema.domain;
import static com.google.common.base.Preconditions.checkArgument; 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.DateTimeUtils.toZonedDateTime;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable; import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject; import google.registry.model.ImmutableObject;
import google.registry.persistence.CreateAutoTimestampConverter;
import google.registry.util.DateTimeUtils; import google.registry.util.DateTimeUtils;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Optional; import java.util.Optional;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.EnumType; import javax.persistence.EnumType;
import javax.persistence.Enumerated; 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 * Unique constraint to get around Hibernate's failure to handle auto-increment field in
* composite primary key. * composite primary key.
* *
* <p>Note: because of this index, physical columns must be declared in the {@link Column} * <p>Note: indexes use the camelCase version of the field names because the {@link
* annotations for {@link RegistryLock#revisionId} and {@link RegistryLock#repoId} fields. * google.registry.persistence.NomulusNamingStrategy} does not translate the field name into the
* snake_case column name until the write itself.
*/ */
indexes = indexes = {
@Index( @Index(
name = "idx_registry_lock_repo_id_revision_id", name = "idx_registry_lock_repo_id_revision_id",
columnList = "repo_id, revision_id", columnList = "repoId, revisionId",
unique = true)) unique = true),
@Index(name = "idx_registry_lock_verification_code", columnList = "verificationCode")
})
public final class RegistryLock extends ImmutableObject implements Buildable { public final class RegistryLock extends ImmutableObject implements Buildable {
/** Describes the action taken by the user. */ /** Describes the action taken by the user. */
@ -74,11 +79,11 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "revision_id", nullable = false) @Column(nullable = false)
private Long revisionId; private Long revisionId;
/** EPP repo ID of the domain in question. */ /** EPP repo ID of the domain in question. */
@Column(name = "repo_id", nullable = false) @Column(nullable = false)
private String repoId; private String repoId;
// TODO (b/140568328): remove this when everything is in Cloud SQL and we can join on "domain" // 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. */ /** Creation timestamp is when the lock/unlock is first requested. */
@Column(nullable = false) @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 * 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() { public DateTime getCreationTimestamp() {
return toJodaDateTime(creationTimestamp); return creationTimestamp.getTimestamp();
} }
/** Returns the completion timestamp, or empty if this lock has not been completed yet. */ /** 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; return revisionId;
} }
public void setCompletionTimestamp(DateTime dateTime) {
this.completionTimestamp = toZonedDateTime(dateTime);
}
@Override @Override
public Builder asBuilder() { 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}. */ /** 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().domainName, "Domain name cannot be null");
checkArgumentNotNull(getInstance().registrarId, "Registrar ID cannot be null"); checkArgumentNotNull(getInstance().registrarId, "Registrar ID cannot be null");
checkArgumentNotNull(getInstance().action, "Action 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"); checkArgumentNotNull(getInstance().verificationCode, "Verification codecannot be null");
checkArgument( checkArgument(
getInstance().registrarPocId != null || getInstance().isSuperuser, getInstance().registrarPocId != null || getInstance().isSuperuser,
@ -220,8 +232,8 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
return this; return this;
} }
public Builder setCreationTimestamp(DateTime creationTimestamp) { public Builder setCreationTimestamp(CreateAutoTimestamp creationTimestamp) {
getInstance().creationTimestamp = toZonedDateTime(creationTimestamp); getInstance().creationTimestamp = creationTimestamp;
return this; 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 -- See the License for the specific language governing permissions and
-- limitations under the License. -- limitations under the License.
CREATE TABLE "RegistryLock" ( create index if not exists idx_registry_lock_verification_code ON "RegistryLock"
revision_id BIGSERIAL NOT NULL, using btree (verification_code);
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);

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); 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: - -- Name: ClaimsEntry fk6sc6at5hedffc0nhdcab6ivuq; Type: FK CONSTRAINT; Schema: public; Owner: -
-- --