-
Notifications
You must be signed in to change notification settings - Fork 0
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
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
- calls
- get(I id)
- finds entity by it's id
- search(Criteria searchCriteria)
- advanced search using
Criteria
class (see below)
- advanced search using
- clear()
- removes all entities from this table
StorageEntity<I>
represents entity which can be saved in StorageTable<I, E>
.
EntityMetadata
is a container for entity fields and their values. It uses Map<String, Object> to store them like fieldName -> fieldValue.
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
- return
- 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
- checks if some entity field must be included in
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
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 implementgetMetadata()
to return saved metadata from your entity.
- Will be called inside
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.
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 + '\'' +
'}';
}
}
By default fields with null
values are not included. If you want to include them, you need to annotate them as @Nullable
public class TestEntity implements ReflectedStorageEntity<Long> {
private final Long id;
private final String title;
@Nullable
private final String name = null;
...
}
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
.
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
represents some criteria to check if given key-value pairs matches some rules in criteria.
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
orand
operations.
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
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.
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
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...)
InStorageTable.search(Criteria)
first argument is an entity field, and second argument is value which will be passedCriteria
constructor.
- First argument of function is value which will be passed to
// 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
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
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:
To create instance of your repository just call BasicStorage.createRepository(repositoryClass)
.
public interface TestRepository extends StorageRepository<Long, TestEntity> {
}
TestRepository testRepository = BasicStorage.createRepository(TestRepository.class);
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).
@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
Except standard methods you can create your own search methods with predefined Criteria
with @Select
annotation.
@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);
}