Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Amazon DynamoDB and class hierarchy

I'm working with Spring Boot and Amazon DynamoDB using this library. The problem is with this class hierarchy:

@DynamoDBTable(tableName = "EventLogs")
abstract class AbstractEventLogEntry implements Serializable {
  private static final long serialVersionUID = 7713867887326010287L;

  @DynamoDBHashKey(attributeName = "EventId")
  private String eventId;

  @DynamoDBAttribute(attributeName = "GeneratedAt")
  @DynamoDBMarshalling(marshallerClass = ZonedDateTimeMarshaller.class)
  private ZonedDateTime generatedAt;

  AbstractEventLogEntry() {
    eventId = new UUID().value();
    generatedAt = ZonedDateTime.now();
  }

  /* Getters / Setter */
}

...another class:

public abstract class EventLogEntry extends AbstractEventLogEntry {
  private static final long serialVersionUID = 1638093418868197192L;

  @DynamoDBAttribute(attributeName = "UserId")
  private String userId;

  @DynamoDBAttribute(attributeName = "EventName")
  private String eventName;

  protected EventLogEntry(AdminEvent event) {
    userId = event.getUserName();
    eventName = event.getClass().getSimpleName();
  }

  protected EventLogEntry(UserEvent event) {
    userId = event.getUserId();
    eventName = event.getClass().getSimpleName();
  }

  /* Getters / Setter */
}

...another one:

public class AdminEventLogEntry extends EventLogEntry {
  private static final long serialVersionUID = 1953428576998278984L;

  public AdminEventLogEntry(AdminEvent event) {
    super(event);
  }
}

...and the last one:

public class UserEventLogEntry extends EventLogEntry {
  private static final long serialVersionUID = 6845335344191463717L;

  public UserEventLogEntry(UserEvent event) {
    super(event);
  }
}

A typical class hierarchy. Now I'm trying to store AdminEventLogEntry and UserEventLogEntry using a common repository:

@EnableScan
public interface EventLogEntryRepository extends DynamoDBCrudRepository<EventLogEntry, String> {
  // ...
}

...and it always tells me:

com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMappingException: no key(s) present on class io.shido.events.domain.AdminEventLogEntry

As soon as I declare (again) the key it works:

@DynamoDBHashKey(attributeName = "EventId")
private String eventId;

So my question is: do I need to re-declared all the fields that might be common between the hierarchies? It looks like it's not recognizing the HashKey from the parent.

Any clues?


1 Answers

I found the solution (some time ago), so I'm updating the post in case someone needs it along the way in the future. Notice the abstract class is no longer there, maybe you can tweak it for your own purposes, I didn't have time by then to test it (not now...so it's simpler, maybe not fully correct from the OOP standpoint).

The issue was with the class hierarchy and the (Spring-based) AmazonDB client's configuration. The next classes are the actual solution.

(a) Spring config file for Amazon DynamoDB client.

Notice that you might not need the dynamoDBOperationsRef since it's only used in case you need different tables "per environment". With DynamoDB (if you have only one account) you can't have different "environments" so you have to find a way to workaround that. This is the solution: to prefix tables (and apply security settings as required).

@Configuration
@EnableContextInstanceData // Only if you are going to use Identity and Access Management (IAM)
@EnableDynamoDBRepositories(basePackages = "io.shido.events", dynamoDBOperationsRef = "dynamoDBOperations")
class AmazonConfiguration {
  @Value("${aws.endpoint.dynamodb}")
  private String dynamoDbEndpoint;

  @Value("${ENV:local}")
  private String environment;

  @Bean
  public AmazonDynamoDB amazonDynamoDB() {
    final AmazonDynamoDBClient client = new AmazonDynamoDBClient();
    //client.setSignerRegionOverride(Regions.fromName(region).getName());
    if (StringUtils.isNotEmpty(dynamoDbEndpoint)) {
      client.setEndpoint(dynamoDbEndpoint);
    }
    return client;
  }

  @Bean
  public DynamoDBOperations dynamoDBOperations() {
    final DynamoDBTemplate template = new DynamoDBTemplate(amazonDynamoDB());
    final DynamoDBMapperConfig mapperConfig = DynamoDBMapperConfig.builder()
      .withTableNameOverride(DynamoDBMapperConfig.TableNameOverride.withTableNamePrefix(environment + "-"))
      .build();
    template.setDynamoDBMapperConfig(mapperConfig);
    return template;
  }
}

(b) DynamoDB annotated "entity" class.

package io.shido.events;

// imports

@DynamoDBTable(tableName = "EventLogs")
final class EventLogEntry implements Serializable {
  // Define your own long serialVersionUID

  @DynamoDBHashKey(attributeName = "EventId")
  private String eventId;

  @DynamoDBTypeConvertedEnum
  @DynamoDBAttribute(attributeName = "EventType")
  private EventType type;

  @DynamoDBAttribute(attributeName = "EntityId")
  private String entityId;

  @Scrambled
  @DynamoDBAttribute(attributeName = "Event")
  private Event event;

  @DynamoDBAttribute(attributeName = "GeneratedAt")
  @DynamoDBTypeConverted(converter = ZonedDateTimeConverter.class)
  private ZonedDateTime generatedAt;

  public EventLogEntry() {
    generatedAt = ZonedDateTime.now();
  }

  public EventLogEntry(AdminEvent event) {
    this();
    eventId = event.getId();
    type = EventType.ADMIN;
    entityId = event.getEntityId();
    this.event = event;
  }

  public EventLogEntry(UserEvent event) {
    this();
    eventId = event.getId();
    type = EventType.USER;
    entityId = event.getEntityId();
    this.event = event;
  }

  // getters and setters (a MUST, at least till the version I'm using)

  // hashCode(), equals and toString()
}

(c) The Spring repository definition.

@EnableScan
public interface EventLogEntryRepository extends DynamoDBCrudRepository<EventLogEntry, String> { }

(d) The table(s) definition.

Eventually, the way you define the attributes and things like that it's up to you and/or your requirement(s).

{
  "TableName" : "local-EventLogs",
  "AttributeDefinitions" : [
    { "AttributeName" : "EventId", "AttributeType" : "S" },
    { "AttributeName" : "EventType", "AttributeType" : "S" },
    { "AttributeName" : "EntityId", "AttributeType" : "S" },
    { "AttributeName" : "Event", "AttributeType" : "S" },
    { "AttributeName" : "GeneratedAt", "AttributeType" : "S" }
  ],
  "KeySchema" : [ { "AttributeName" : "EventId", "KeyType" : "HASH" } ],
  "ProvisionedThroughput" : { "ReadCapacityUnits" : 10, "WriteCapacityUnits" : 10 }
}