Skip to content

Storage

Danila Rassokhin edited this page Jan 28, 2023 · 2 revisions

Storage

Storage stores any game entities, which implement StorageEntity interface. BasicStorage implements simple in memory database. You can create your own implementations to store entities in some external DB like Postgres, MySQL etc.

Storage has only one method:

  • getTableFor(Class entityClass)
    • returns StorageTable<I, E> for given class or creates new one if there is no table exist

Storage Table

StorageTable<I, E> represents table for one entity class. E.g. StorageEntity<Long, MyEntity> can store only entities of type MyEntity with primary key of type Long.

Table has 4 main operations:

  • save(E entity)
    • calls entity.init() and then saves entity in this table
  • get(I id)
    • finds entity by it's id
  • search(Criteria searchCriteria)
    • advanced search using Criteria class (see below)
  • clear()
    • removes all entities from this table

Storage Entity

StorageEntity<I> represents entity which can be saved in StorageTable<I, E>.

EntityMetadata

EntityMetadata is a container for entity fields and their values. It uses Map<String, Object> to store them like fieldName -> fieldValue.

Basic StorageEntity

StorageEntity is the most abstract entity with such methods:

  • init()
    • initiates entity. Will be called before saving in StorageTable
  • I getId()
    • returns id of this entity. Id must be unique per entity
  • EntityMetadata getMetadata()
    • return EntityMetadata for this entity
  • Object getMetadataFrom(String field)
    • returns value of some entity field
  • boolean includeField(Field f, Object from)
    • checks if some entity field must be included in EntityMetadata

ReflectedStorageEntity

ReflectedStorageEntity is a partial implementation of StorageEntity. It uses reflection to create metadata based on class fields. Using this interface you need to implement only getId() method. You can override other methods to change reflection strategy.

EntityMetadata won't be saved and field values will be collected on every getMetadata() call. This implementation can be slow, cause EntityMetadata won't be cached.

ReflectedCacheableStorageEntity

ReflectedCacheableStorageEntity is a child of ReflectedStorageEntity. It does the same things, but has one additional method:

  • saveMetadata(EntityMetadata entityMetadata)
    • Will be called inside init() to save generated metadata. You need to implement this method in your entity to store metadata inside You also need to implement getMetadata() to return saved metadata from your entity.

With this entity type metadata won't be automatically updated for search purposes. You need to update it manually after changing field values using EntityMetadata.put() method.

Example

public class TestEntity implements ReflectedStorageEntity<Long> {

  private final Long id;

  private final String title;

  public TestEntity(Long id, String title) {
    this.id = id;
    this.title = title;
  }

  @Override
  public Long getId() {
    return id;
  }

  @Override
  public int hashCode() {
    return Objects.hash(getId());
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    TestEntity that = (TestEntity) o;
    return getId().equals(that.getId());
  }

  @Override
  public String toString() {
    return "TestEntity{" +
        "id=" + id +
        ", title='" + title + '\'' +
        '}';
  }
}

Nullable

By default fields with null values are not included. If you want to include them, you need to annotate them as @Nullable

Example

public class TestEntity implements ReflectedStorageEntity<Long> {

  private final Long id;

  private final String title;
  
  @Nullable
  private final String name = null;

  ...
}

Embedded

Fields with non primitive and boxed types won't be included in EntityMetadata by default. If you need to search across internal fields of this type, then you need to annotate it as @Embedded. Then you can use fieldName.internalFieldName in Criteria.

Example

public class TestEntity implements ReflectedStorageEntity<Long> {

  private final Long id;

  private final String title;

  @Embedded
  private final EmbeddedField embeddedField = new EmbeddedField(1);
 
  ...

  private static class EmbeddedField {

    private final int embeddedId;

    public EmbeddedField(int embeddedId) {
      this.embeddedId = embeddedId;
    }
  }
}

In this example you can use embeddedField in search criteria like name = $0 & embeddedField.embeddedId = $1.

Criteria

Criteria represents some criteria to check if given key-value pairs matches some rules in criteria.

SearchCriteria

Represents search query. Supports basic operations:

  • between values: >, <, >=, <=, =, ?= (fuzzy search)
  • between predicates: or, and, &, | SearchCriteria works like boolean predicate where all small checks connected with or or and operations.

Example

SearchCriteria searchCriteria = SearchCriteria.create()
        // Checks if given value of key "a" will be equal to 1
        .<Integer>or(o -> o == 1, "a")
        // Checks if given value of key "b" will be equal to "Hello"
        .<String>or(o -> o.equals("Hello"), "b")
        .and()
         // Checks if given value of key "c" won't be null
        .or(Objects::nonNull, "c")
        .build();

Example above can be read as: If ("a" equals 1 OR "b" equals "Hello") AND "c" is not null then return true (false otherwise) Now we can create test data and check it with our criteria:

Map<String, Object> testData1 = new HashMap<>();
testData1.put("a", 1);
testData1.put("b", "Hello");
testData1.put("c", new Object());

Map<String, Object> testData2 = new HashMap<>();
testData2.put("a", 2);
testData2.put("b", "Hello");
testData2.put("c", new Object());

Map<String, Object> testData3 = new HashMap<>();
testData3.put("a", 1);
testData3.put("b", "WRONG");
testData3.put("c", new Object());

Map<String, Object> testData4 = new HashMap<>();
testData4.put("a", 1);
testData4.put("b", "Hello");
testData4.put("c", null);

searchCriteria.test(testData1); //TRUE
searchCriteria.test(testData2); //TRUE
searchCriteria.test(testData3); //TRUE
searchCriteria.test(testData4); //FALSE

Expression query

SearchCriteria can parse query expression from string. Search query should look like: <fieldName> <operation> $<argumentPosition> [[predicateOperation] ...]

Example of query: age >= $0 and height < $1 | name ?= $2

To create Criteria call SearchCriteria.createFromExpression(String query, Object... args). As args you need to pass values which will be used instead of $ keys in criteria. E.g. in example above the first arg will be used instead of $0, the second instead of $1 etc.

Example

Criteria criteria = SearchCriteria.createFromExpression("a >= $0", 2);

Map<String, Object> testData1 = new HashMap<>();
testData1.put("a", 1);
testData1.put("b", "Hello");
testData1.put("c", new Object());

criteria.test(testData1); // $0 will be replaced with 2, and "a" will be replaced with 1, result is FALSE

Extend SearchCriteria with new operations

You can add new operations to SearchCriteria easily with SearchCriteria.addExternalOperation(code, BiFunction<Object, Object, Boolean> predicate):

  • code is a symbol which will be used in expression. 1 <= code.length <= 3
  • predicate is a function to check some predicate between two values
    • First argument of function is value which will be passed to Criteria.test(Map).
    • Second argument of function is value which will be passed to SearchCriteria.createFromExpression(String, Object...) In StorageTable.search(Criteria) first argument is an entity field, and second argument is value which will be passed Criteria constructor.

Example

// This operation will check if argument (entity's field value) class is equals to query class.
SearchCriteria.addExternalOperation("^", (arg, query) -> arg.getClass().equals(query));

Map<String, Object> testData = new HashMap<>();
testData.put("name", "Hello");

Criteria classTypeCriteria = SearchCriteria.createFromExpression("name ^ $0", String.class);
classTypeCriteria.test(testData); // TRUE

Storage repositories

All functional above is low-level. There is a more simple way to interact with storage - StorageRepository.

StorageRepository works like JPA in other frameworks: you can create simple interface without implementation and use its methods or create your own.

StorageRepository has 3 basic operations:

  • save(E entity)
    • saves entity in corresponding table
  • Optional findById(I id)
    • finds entity by id and returns optional
  • List search(Criteria criteria)
    • searches for entities by criteria

Repository creation

To create new repository you need to create interface and extend it from StorageRepository<I, E> where - entity id type, - entity type.

Now you have to ways to get instance of this repository:

Create repository with BasicStorage

To create instance of your repository just call BasicStorage.createRepository(repositoryClass).

Example

public interface TestRepository extends StorageRepository<Long, TestEntity> {

}

TestRepository testRepository = BasicStorage.createRepository(TestRepository.class);

Repositories as a beans

You can mark your repositories as beans with @Repository annotation. This annotation has only one property value() - name of this repository. If name is not specified then standard name convention is used (class name in lower case).

Example

@Repository("TestRepository") //Or just @Repository
public interface TestRepository extends StorageRepository<Long, TestEntity> {

}

And then:

diContainer.loadBean(TestRepository.class); // Can be loaded in runtime
diContainer.init(); // Can be loaded during initial package scan
diContainer.scanPackage("my.package", new SimplePackageScanner()); // Can be loaded during some package scan

Select methods

Except standard methods you can create your own search methods with predefined Criteria with @Select annotation.

Example

@Repository
public interface TestRepository extends StorageRepository<Long, TestEntity> {

  @Select("title = $0")
  Optional<TestEntity> findByByTitle(String title);

  @Select("embeddedField.embeddedId = $0")
  List<TestEntity> findByEmbeddedId(Integer id);

}