Skip to content

Commit 8877072

Browse files
committed
Improve dataprovider api and make it more user friendly
1 parent a54fd30 commit 8877072

17 files changed

Lines changed: 736 additions & 81 deletions

README.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,50 @@
1-
# DataAPI
1+
# DataProvider
2+
3+
DataProvider is a plugin-scoped database access layer for Velocity and Bukkit/Paper plugins.
4+
5+
It provides:
6+
- Caller-verified API access per plugin.
7+
- Unified registration for `MYSQL`, `MONGODB`, `REDIS`, and `REDIS_MESSAGING`.
8+
- Shared connection lifecycle management with reference counting.
9+
- Optional ORM support through `ORMContext`.
10+
11+
## Why this exists
12+
13+
Most plugin projects repeat the same patterns:
14+
- Load db config files.
15+
- Build clients and pools.
16+
- Handle reconnect/disconnect and cleanup.
17+
- Cast generic providers into specific access interfaces.
18+
19+
DataProvider centralizes that work so feature code stays focused on business logic.
20+
21+
## Quick Start
22+
23+
```java
24+
DataProviderAPI api = VelocityDataProvider.getDataProviderAPI();
25+
26+
Optional<MessagingDataAccess> bus = api.registerDataAccess(
27+
DatabaseType.REDIS_MESSAGING,
28+
"default",
29+
MessagingDataAccess.class
30+
);
31+
32+
bus.ifPresent(redisBus -> {
33+
redisBus.subscribe("proxy.staffchat.message", StaffChatMessage.class, msg -> {
34+
// Handle incoming message
35+
});
36+
});
37+
```
38+
39+
## Docs
40+
41+
- [Usage guide](docs/USAGE_GUIDE.md)
42+
- [Best practices](docs/BEST_PRACTICES.md)
43+
- [Architecture and maintainability review](docs/ARCHITECTURE_REVIEW.md)
44+
- [ProxyFeatures integration notes](docs/PROXYFEATURES_INTEGRATION.md)
45+
- [Examples](docs/examples)
46+
47+
## Notes
48+
49+
- Caller identity is resolved at runtime from the platform plugin context.
50+
- `registerDatabase(...)` is reference-counted internally; repeated registrations reuse the same connection.

docs/BEST_PRACTICES.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Best Practices
2+
3+
## API usage
4+
5+
- Prefer `registerDatabaseOptional`, `registerDatabaseAs`, and `registerDataAccess` over raw nullable APIs.
6+
- Prefer `getDataAccessOptional(...)` instead of manual casts.
7+
- Treat database registration as startup wiring, not ad-hoc runtime behavior in hot paths.
8+
9+
## Lifecycle
10+
11+
- Register once during feature/plugin init.
12+
- Unregister on disable.
13+
- If you run multiple feature modules in one plugin, prefer releasing only the connections each feature acquired.
14+
- Use `unregisterAllDatabases()` only when shutting down the entire plugin context.
15+
16+
## Messaging
17+
18+
- Use one clear message class per channel contract.
19+
- Keep channels stable and namespaced (for example: `proxy.staffchat.message`).
20+
- Handle parse/dispatch failures as non-fatal.
21+
22+
## ORM
23+
24+
- Use ORM only for relational providers.
25+
- Keep ORM entity sets explicit and small per context.
26+
- Always call `ORMContext.shutdown()` on disable.
27+
28+
## Configuration
29+
30+
- Keep one connection identifier per concrete config section (for example: `default`, `player_data_rw`).
31+
- Use separate identifiers for different operational requirements (read-only, read-write, analytics).
32+
33+
## Concurrency
34+
35+
- DataProvider connection reuse is reference-counted per `(plugin, type, identifier)`.
36+
- Re-registering the same key does not open a new connection; it acquires another reference.

docs/USAGE_GUIDE.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Usage Guide
2+
3+
## 1. Get the API
4+
5+
Velocity:
6+
7+
```java
8+
DataProviderAPI api = VelocityDataProvider.getDataProviderAPI();
9+
```
10+
11+
Bukkit/Paper:
12+
13+
```java
14+
DataProviderAPI api = BukkitDataProvider.getDataProviderAPI();
15+
```
16+
17+
Caller identity is resolved automatically from the plugin runtime context.
18+
19+
## 2. Register a connection
20+
21+
Basic:
22+
23+
```java
24+
DatabaseProvider provider = api.registerDatabase(DatabaseType.MYSQL, "player_data_rw");
25+
if (provider == null || !provider.isConnected()) {
26+
// handle unavailable database
27+
}
28+
```
29+
30+
Optional style:
31+
32+
```java
33+
Optional<DatabaseProvider> provider = api.registerDatabaseOptional(DatabaseType.MYSQL, "player_data_rw");
34+
```
35+
36+
Typed provider style:
37+
38+
```java
39+
Optional<RelationalDatabaseProvider> relational = api.registerDatabaseAs(
40+
DatabaseType.MYSQL,
41+
"player_data_rw",
42+
RelationalDatabaseProvider.class
43+
);
44+
```
45+
46+
Typed data access style:
47+
48+
```java
49+
Optional<MessagingDataAccess> redisBus = api.registerDataAccess(
50+
DatabaseType.REDIS_MESSAGING,
51+
"default",
52+
MessagingDataAccess.class
53+
);
54+
```
55+
56+
## 3. Use the provider safely
57+
58+
`DatabaseProvider` has helper methods to avoid raw casts:
59+
60+
```java
61+
Optional<MessagingDataAccess> bus = provider.getDataAccessOptional(MessagingDataAccess.class);
62+
Optional<DataSource> dataSource = provider.getDataSourceOptional();
63+
```
64+
65+
## 4. Release connections
66+
67+
Release a specific connection:
68+
69+
```java
70+
api.unregisterDatabase(DatabaseType.MYSQL, "player_data_rw");
71+
```
72+
73+
Release all connections for your plugin context:
74+
75+
```java
76+
api.unregisterAllDatabases();
77+
```
78+
79+
## 5. ORM usage
80+
81+
For relational providers:
82+
83+
```java
84+
ORMContext orm = new ORMContext(
85+
"my-plugin",
86+
dataSource,
87+
loggerAdapter,
88+
"validate",
89+
PlayerEntity.class
90+
);
91+
```
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import nl.hauntedmc.dataprovider.api.DataProviderAPI;
2+
import nl.hauntedmc.dataprovider.database.DatabaseType;
3+
import nl.hauntedmc.dataprovider.database.document.DocumentDataAccess;
4+
import nl.hauntedmc.dataprovider.database.document.model.DocumentQuery;
5+
6+
import java.util.Map;
7+
import java.util.Optional;
8+
9+
/**
10+
* Example: MongoDB document operations.
11+
*/
12+
public final class MongoDocumentExample {
13+
14+
private DocumentDataAccess documents;
15+
16+
public void onEnable(DataProviderAPI api) {
17+
Optional<DocumentDataAccess> optDocuments = api.registerDataAccess(
18+
DatabaseType.MONGODB,
19+
"default",
20+
DocumentDataAccess.class
21+
);
22+
documents = optDocuments.orElse(null);
23+
}
24+
25+
public void createProfile(String uuid, String name) {
26+
if (documents == null) {
27+
return;
28+
}
29+
documents.insertOne("profiles", Map.of(
30+
"uuid", uuid,
31+
"name", name
32+
));
33+
}
34+
35+
public void findProfile(String uuid) {
36+
if (documents == null) {
37+
return;
38+
}
39+
documents.findOne("profiles", new DocumentQuery().eq("uuid", uuid))
40+
.thenAccept(profile -> System.out.println("Profile: " + profile));
41+
}
42+
43+
public void onDisable(DataProviderAPI api) {
44+
api.unregisterDatabase(DatabaseType.MONGODB, "default");
45+
}
46+
}

docs/examples/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# DataProvider Examples
2+
3+
These examples are intentionally minimal and focused on API usage patterns.
4+
5+
- [RelationalOrmExample.java](RelationalOrmExample.java)
6+
- Register a MySQL connection and initialize `ORMContext`.
7+
- [RedisMessagingExample.java](RedisMessagingExample.java)
8+
- Register Redis messaging, subscribe, publish, and unsubscribe cleanly.
9+
- [MongoDocumentExample.java](MongoDocumentExample.java)
10+
- Register MongoDB and perform simple document insert/find operations.
11+
- [RedisKeyValueExample.java](RedisKeyValueExample.java)
12+
- Register Redis key-value and perform basic cache reads/writes.
13+
14+
Use these files as templates in your plugin service/lifecycle classes.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import nl.hauntedmc.dataprovider.api.DataProviderAPI;
2+
import nl.hauntedmc.dataprovider.database.DatabaseType;
3+
import nl.hauntedmc.dataprovider.database.keyvalue.KeyValueDataAccess;
4+
5+
import java.util.Optional;
6+
7+
/**
8+
* Example: Redis key-value access.
9+
*/
10+
public final class RedisKeyValueExample {
11+
12+
private KeyValueDataAccess keyValue;
13+
14+
public void onEnable(DataProviderAPI api) {
15+
Optional<KeyValueDataAccess> optAccess = api.registerDataAccess(
16+
DatabaseType.REDIS,
17+
"default",
18+
KeyValueDataAccess.class
19+
);
20+
keyValue = optAccess.orElse(null);
21+
}
22+
23+
public void cachePlayerLanguage(String playerUuid, String languageCode) {
24+
if (keyValue == null) {
25+
return;
26+
}
27+
keyValue.setKey("player:lang:" + playerUuid, languageCode);
28+
}
29+
30+
public void loadPlayerLanguage(String playerUuid) {
31+
if (keyValue == null) {
32+
return;
33+
}
34+
keyValue.getKey("player:lang:" + playerUuid)
35+
.thenAccept(language -> System.out.println("Language for " + playerUuid + ": " + language));
36+
}
37+
38+
public void onDisable(DataProviderAPI api) {
39+
api.unregisterDatabase(DatabaseType.REDIS, "default");
40+
}
41+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import nl.hauntedmc.dataprovider.api.DataProviderAPI;
2+
import nl.hauntedmc.dataprovider.database.DatabaseType;
3+
import nl.hauntedmc.dataprovider.database.messaging.MessagingDataAccess;
4+
import nl.hauntedmc.dataprovider.database.messaging.api.AbstractEventMessage;
5+
import nl.hauntedmc.dataprovider.database.messaging.api.Subscription;
6+
7+
import java.util.Optional;
8+
9+
/**
10+
* Example: Redis messaging publish/subscribe workflow.
11+
*/
12+
public final class RedisMessagingExample {
13+
14+
private MessagingDataAccess bus;
15+
private Subscription subscription;
16+
17+
public void onEnable(DataProviderAPI api) {
18+
Optional<MessagingDataAccess> optBus = api.registerDataAccess(
19+
DatabaseType.REDIS_MESSAGING,
20+
"default",
21+
MessagingDataAccess.class
22+
);
23+
24+
if (optBus.isEmpty()) {
25+
return;
26+
}
27+
bus = optBus.get();
28+
29+
subscription = bus.subscribe("proxy.staffchat.message", StaffChatMessage.class, msg -> {
30+
System.out.println("[" + msg.getServer() + "] " + msg.getSender() + ": " + msg.getMessage());
31+
});
32+
}
33+
34+
public void publishMessage(String sender, String server, String message) {
35+
if (bus == null) {
36+
return;
37+
}
38+
bus.publish("proxy.staffchat.message", new StaffChatMessage(sender, server, message));
39+
}
40+
41+
public void onDisable(DataProviderAPI api) {
42+
if (subscription != null) {
43+
subscription.unsubscribe();
44+
subscription = null;
45+
}
46+
if (bus != null) {
47+
bus.shutdown();
48+
bus = null;
49+
}
50+
api.unregisterDatabase(DatabaseType.REDIS_MESSAGING, "default");
51+
}
52+
53+
public static final class StaffChatMessage extends AbstractEventMessage {
54+
private final String sender;
55+
private final String server;
56+
private final String message;
57+
58+
@SuppressWarnings("unused")
59+
private StaffChatMessage() {
60+
super("staffchat");
61+
this.sender = null;
62+
this.server = null;
63+
this.message = null;
64+
}
65+
66+
public StaffChatMessage(String sender, String server, String message) {
67+
super("staffchat");
68+
this.sender = sender;
69+
this.server = server;
70+
this.message = message;
71+
}
72+
73+
public String getSender() {
74+
return sender;
75+
}
76+
77+
public String getServer() {
78+
return server;
79+
}
80+
81+
public String getMessage() {
82+
return message;
83+
}
84+
}
85+
}

0 commit comments

Comments
 (0)