362 lines
9.6 KiB
Markdown
362 lines
9.6 KiB
Markdown
# Developer Experience Improvements for bstore-java
|
|
|
|
## Current State Analysis
|
|
|
|
### Current API Patterns
|
|
- Field access: `Product.name.set(product, "Alice")` / `Product.name.get(product, null)`
|
|
- Slot-based access: `dbo.set(nameSlot, "Alice")` / `dbo.get(nameSlot, null)`
|
|
- Positional access: `dbo.set(0, value)` / `dbo.get(0)` (already Java-like!)
|
|
- Field-based queries: `PersonDBO.AGE.gte(18)`
|
|
- Action chaining: `terminal.begin().load(PersonDBO).filterBy(...).execute()`
|
|
|
|
### Issues Identified
|
|
1. **Verbose field access**: `Product.name.set(product, "Alice")` is verbose
|
|
2. **No convenience methods**: Can't use `dbo.get(Product.NAME)` or `dbo.set(Product.NAME, "Alice")`
|
|
3. **No equals/hashCode**: Can't compare DBOs by value
|
|
4. **No cloning**: Can't easily copy DBO instances
|
|
5. **No map conversion**: Can't convert to/from `Map<String, Object>`
|
|
6. **No fluent API**: Missing `withField()` methods for method chaining
|
|
7. **No field lookup by name**: Entity doesn't have `getField(String name)` method
|
|
8. **toString() could be better**: Currently returns JSON, could have human-readable format
|
|
|
|
## Proposed Improvements
|
|
|
|
### 1. Convenience Methods for Field Access (High Impact) ✅
|
|
**Goal**: Allow `dbo.get(Product.NAME)` instead of `Product.NAME.get(dbo, null)`
|
|
|
|
**Implementation**:
|
|
```java
|
|
/**
|
|
* Get field value by Field instance.
|
|
*
|
|
* @param field Field to get value for
|
|
* @return Field value, or null if not set
|
|
*/
|
|
public Object get(Field field) {
|
|
return get(field, null);
|
|
}
|
|
|
|
/**
|
|
* Get field value by Field instance with default.
|
|
*
|
|
* @param field Field to get value for
|
|
* @param defaultValue Default value if field is null
|
|
* @return Field value, or defaultValue if not set
|
|
*/
|
|
public Object get(Field field, Object defaultValue) {
|
|
return get((Slot)field, defaultValue);
|
|
}
|
|
|
|
/**
|
|
* Set field value by Field instance.
|
|
*
|
|
* @param field Field to set value for
|
|
* @param value Value to set
|
|
* @return Self for chaining
|
|
*/
|
|
public DBO set(Field field, Object value) {
|
|
return (DBO)set((Slot)field, value);
|
|
}
|
|
```
|
|
|
|
**Benefits**:
|
|
- More intuitive: `person.get(PersonDBO.NAME)` vs `PersonDBO.NAME.get(person, null)`
|
|
- Consistent with positional access pattern
|
|
- Backward compatible (Field.get/set still work)
|
|
|
|
**Trade-offs**:
|
|
- Minimal - just convenience wrappers
|
|
|
|
### 2. Equals and HashCode (Medium Impact)
|
|
**Goal**: Value-based comparison
|
|
|
|
**Implementation**:
|
|
```java
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
if (this == obj) return true;
|
|
if (obj == null || getClass() != obj.getClass()) return false;
|
|
DBO other = (DBO)obj;
|
|
|
|
// Compare entity types
|
|
if (type != other.type) return false;
|
|
if (type == null) return true;
|
|
|
|
// Compare by primary key if available
|
|
Field pk = type.getPk();
|
|
if (pk != null) {
|
|
Object thisPk = get(pk);
|
|
Object otherPk = other.get(pk);
|
|
if (thisPk != null && otherPk != null) {
|
|
return thisPk.equals(otherPk);
|
|
}
|
|
}
|
|
|
|
// Compare all values
|
|
if (values == null) return other.values == null;
|
|
if (other.values == null) return false;
|
|
if (values.length != other.values.length) return false;
|
|
for (int i = 0; i < values.length; i++) {
|
|
if (!java.util.Objects.equals(values[i], other.values[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
if (type == null) return System.identityHashCode(this);
|
|
|
|
// Hash by primary key if available
|
|
Field pk = type.getPk();
|
|
if (pk != null) {
|
|
Object pkValue = get(pk);
|
|
if (pkValue != null) {
|
|
return java.util.Objects.hash(type.getName(), pkValue);
|
|
}
|
|
}
|
|
|
|
// Hash by all values
|
|
return java.util.Objects.hash(type.getName(), java.util.Arrays.hashCode(values));
|
|
}
|
|
```
|
|
|
|
### 3. Cloning Support (Low Effort)
|
|
**Goal**: Standard Java pattern
|
|
|
|
**Implementation**:
|
|
```java
|
|
/**
|
|
* Create a shallow copy of this DBO.
|
|
*
|
|
* @return New DBO instance with copied values
|
|
*/
|
|
public DBO clone() {
|
|
DBO cloned = new DBO();
|
|
cloned.setType(type);
|
|
cloned.setTerminal(terminal);
|
|
cloned.status = status;
|
|
if (values != null) {
|
|
cloned.values = values.clone();
|
|
}
|
|
return cloned;
|
|
}
|
|
```
|
|
|
|
### 4. Map Conversion (Medium Impact)
|
|
**Goal**: Dictionary-like access
|
|
|
|
**Implementation**:
|
|
```java
|
|
import java.util.Map;
|
|
import java.util.HashMap;
|
|
|
|
/**
|
|
* Convert DBO to Map with field names as keys.
|
|
*
|
|
* @return Map with field names as keys and values as values
|
|
*/
|
|
public Map<String, Object> toMap() {
|
|
Map<String, Object> result = new HashMap<>();
|
|
if (type != null) {
|
|
FieldSlice slice = new FieldSlice(type);
|
|
while (slice.hasNext()) {
|
|
Field field = slice.next();
|
|
Object value = get(field);
|
|
if (value != null) {
|
|
result.put(field.getName(), value);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Create DBO instance from Map.
|
|
*
|
|
* @param cls DBO class
|
|
* @param data Map with field values
|
|
* @param terminal Optional terminal to attach
|
|
* @return New DBO instance
|
|
*/
|
|
public static <T extends DBO> T fromMap(Class<T> cls, Map<String, Object> data, Terminal terminal) {
|
|
try {
|
|
T instance = cls.getDeclaredConstructor().newInstance();
|
|
if (terminal != null) {
|
|
instance.setTerminal(terminal);
|
|
}
|
|
Entity entity = Entity.recall(cls);
|
|
if (entity != null && data != null) {
|
|
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
|
Field field = entity.getField(entry.getKey());
|
|
if (field != null) {
|
|
instance.set(field, entry.getValue());
|
|
}
|
|
}
|
|
}
|
|
return instance;
|
|
} catch (Exception e) {
|
|
throw new RuntimeException("Failed to create DBO from map", e);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5. Fluent API (Low Effort)
|
|
**Goal**: Method chaining
|
|
|
|
**Implementation**:
|
|
```java
|
|
/**
|
|
* Set field value (fluent API).
|
|
*
|
|
* @param field Field to set
|
|
* @param value Value to set
|
|
* @return Self for chaining
|
|
*/
|
|
public DBO withField(Field field, Object value) {
|
|
set(field, value);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set multiple fields from Map (fluent API).
|
|
*
|
|
* @param data Map with field values
|
|
* @return Self for chaining
|
|
*/
|
|
public DBO withFields(Map<String, Object> data) {
|
|
if (type != null && data != null) {
|
|
for (Map.Entry<String, Object> entry : data.entrySet()) {
|
|
Field field = type.getField(entry.getKey());
|
|
if (field != null) {
|
|
set(field, entry.getValue());
|
|
}
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
```
|
|
|
|
### 6. Entity Field Lookup by Name (Low Effort)
|
|
**Goal**: Find fields by name
|
|
|
|
**Implementation**:
|
|
```java
|
|
// In Entity class
|
|
/**
|
|
* Get field by name (case-insensitive).
|
|
*
|
|
* @param name Field name to find
|
|
* @return Field instance or null if not found
|
|
*/
|
|
public Field getField(String name) {
|
|
if (name == null) return null;
|
|
|
|
// Search own fields
|
|
for (Slot slot : getOwnSlots()) {
|
|
if (slot instanceof Field && slot.equals(name)) {
|
|
return (Field)slot;
|
|
}
|
|
}
|
|
|
|
// Search base entity
|
|
if (base != null) {
|
|
Field field = base.getField(name);
|
|
if (field != null) return field;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
```
|
|
|
|
### 7. Better toString() (Low Effort)
|
|
**Goal**: Human-readable representation
|
|
|
|
**Implementation**:
|
|
```java
|
|
/**
|
|
* Return human-readable string representation.
|
|
*
|
|
* @return String like "PersonDBO(name=Alice, age=30)"
|
|
*/
|
|
public String toDisplayString() {
|
|
if (type == null) {
|
|
return getClass().getSimpleName() + "()";
|
|
}
|
|
|
|
StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append("(");
|
|
FieldSlice slice = new FieldSlice(type);
|
|
boolean first = true;
|
|
while (slice.hasNext()) {
|
|
Field field = slice.next();
|
|
Object value = get(field);
|
|
if (value != null) {
|
|
if (!first) sb.append(", ");
|
|
sb.append(field.getName()).append("=").append(value);
|
|
first = false;
|
|
}
|
|
}
|
|
sb.append(")");
|
|
return sb.toString();
|
|
}
|
|
|
|
// Keep existing toString() for JSON representation
|
|
```
|
|
|
|
## Priority Recommendations
|
|
|
|
### Phase 1: Quick Wins (Low Effort, High Impact)
|
|
1. ✅ **Convenience methods** (`get(Field)`, `set(Field, value)`)
|
|
2. ✅ **Entity.getField(String)** - field lookup by name
|
|
3. ✅ **Fluent API** (`withField()`, `withFields()`)
|
|
|
|
### Phase 2: Standard Java Patterns (Medium Effort, High Value)
|
|
4. ✅ **Equals and hashCode** - value comparison
|
|
5. ✅ **Clone support** - object copying
|
|
6. ✅ **Map conversion** - `toMap()`, `fromMap()`
|
|
|
|
### Phase 3: Nice to Have
|
|
7. ✅ **Better toString()** - human-readable format (keep JSON as default)
|
|
|
|
## Implementation Notes
|
|
|
|
- All changes should be **backward compatible**
|
|
- Maintain existing `Field.get()`/`Field.set()` methods
|
|
- Add new methods alongside old ones
|
|
- Follow Java conventions (PEP 8 equivalent: Java Code Conventions)
|
|
- Use `@Override` where appropriate
|
|
- Add comprehensive Javadoc
|
|
|
|
## Example Usage After Improvements
|
|
|
|
```java
|
|
// Before (still works):
|
|
Product product = new Product();
|
|
Product.name.set(product, "Widget");
|
|
String name = (String)Product.name.get(product, null);
|
|
|
|
// After (new convenience methods):
|
|
Product product = new Product();
|
|
product.set(Product.NAME, "Widget"); // Convenience method
|
|
String name = (String)product.get(Product.NAME); // Convenience method
|
|
|
|
// Fluent API:
|
|
Product product = new Product()
|
|
.withField(Product.NAME, "Widget")
|
|
.withField(Product.PRICE, 19.99);
|
|
|
|
// Map conversion:
|
|
Map<String, Object> data = product.toMap();
|
|
Product product2 = DBO.fromMap(Product.class, data, terminal);
|
|
|
|
// Equality:
|
|
if (product1.equals(product2)) {
|
|
System.out.println("Same product");
|
|
}
|
|
|
|
// Cloning:
|
|
Product copy = product.clone();
|
|
```
|
|
|