Entity Annotations
IMPORTANT: All annotations are optional! The library works with plain Java classes using convention-based mapping. Annotations are only needed when you want to override the default behavior.
JPA-Compatible Annotations
DEV1.0 Picnic uses JPA-compatible annotation names for a familiar developer experience:
@Table- Maps entity to database table (same name as JPA)@Column- Maps field to database column (same name as JPA)@Id- Marks primary key field (same name as JPA)@Transient- Excludes field from mapping (same name as JPA)@Temporal- Specifies temporal type for java.util.Date fields
Plus additional annotations for advanced features:
@CalculatedColumn- Virtual columns from SQL expressions@MappedCollection- One-to-many relationships@JoinCustomCondition- Custom JOIN conditions@With- Common Table Expressions (CTEs)@ArrayColumn- PostgreSQL ARRAY/JSONB/JSON columns
Full JPA Entity Support
DEV1.0 Picnic also supports reading JPA annotations as a fallback! If you already have JPA entities in your project, you can use them directly without any changes.
Supports both JPA versions:
- Jakarta Persistence API (JPA 3.0+) -
jakarta.persistence.*- Java EE 9+, Spring Boot 3+ - javax.persistence API (JPA 2.x) -
javax.persistence.*- Java EE 8, Spring Boot 2.x
Annotation Priority Order
- DEV1.0 Picnic annotations (highest priority) -
com.dev10.picnic.annotations.* - JPA annotations (fallback) -
jakarta.persistence.*orjavax.persistence.* - Convention over configuration (lowest priority) - Automatic snake_case mapping
@Table (Optional)
Use this annotation only when your table name differs from the snake_case class name or you need to specify a schema.
import com.dev10.picnic.annotations.Table;
// WITHOUT annotation: EmployeeRecord -> "employee_record" table
public class EmployeeRecord { }
// WITH annotation: Override table name
@Table("t_employee")
public class EmployeeRecord { }
// WITH schema: Specify database schema
@Table(value = "employees", schema = "hr")
public class Employee { }@Column (Optional)
Use this annotation only when your column name differs from the snake_case field name.
import com.dev10.picnic.annotations.Column;
// WITHOUT annotation: firstName -> "first_name" column
private String firstName;
// WITH annotation: Override column name
@Column("fname")
private String firstName;
// WITH annotation: Legacy column names
@Column("EMP_DEPT_ID")
private Long departmentId;@Id (Optional)
Use this annotation only when your primary key field is not named id.
import com.dev10.picnic.annotations.Id;
import com.dev10.picnic.annotations.Column;
// Override primary key field
@Id
@Column("employee_id")
private Long employeeId;
// Without annotation: field named "id" is automatically the primary key
private Long id;@Temporal
Specifies the SQL temporal type for java.util.Date fields to distinguish between DATE (date-only) and TIMESTAMP (date+time) columns.
import com.dev10.picnic.annotations.Temporal;
import com.dev10.picnic.annotations.Column;
import java.util.Date;
@Column("event_date")
@Temporal(Temporal.Type.DATE)
private Date eventDate; // Maps to SQL DATE - only yyyy-MM-dd
@Column("created_at")
@Temporal(Temporal.Type.TIMESTAMP)
private Date createdAt; // Maps to SQL TIMESTAMP - full date+timeNote: Without @Temporal, java.util.Date fields default to DATE type. For new code, prefer using java.time.LocalDate or java.time.LocalDateTime which don’t require this annotation.
@CalculatedColumn
Defines a virtual column computed from a SQL expression.
@Table("employees")
public class Employee {
@Column("salary")
private Double salary;
@Column("bonus")
private Double bonus;
// Calculated column: total compensation
@CalculatedColumn("salary + COALESCE(bonus, 0)")
private Double totalCompensation;
// Calculated column: full name
@CalculatedColumn("CONCAT(first_name, ' ', last_name)")
private String fullName;
// Calculated column: age
@CalculatedColumn("EXTRACT(YEAR FROM AGE(birth_date))")
private Integer age;
// Calculated column with schema placeholder
@CalculatedColumn("(SELECT COUNT(*) FROM ${schema}.orders WHERE customer_id = customers.id)")
private Integer orderCount;
}
// Can filter and sort by calculated columns
// filter: ["totalCompensation", ">", 100000]
// sort: [{"selector": "age", "desc": true}]Security Note: Expressions are validated to prevent SQL injection.
@MappedCollection
Marks a field as a JOIN-based collection relationship (one-to-many).
public class Department {
private Long id;
private String name;
// Employees are loaded via JOIN
@JoinCustomCondition(condition = "dept.id = emp.department_id")
@MappedCollection
private List<Employee> employees;
}@JoinCustomCondition
Defines custom JOIN conditions for relationships, supporting complex logic beyond simple foreign key relationships.
// Simple equality join
@JoinCustomCondition(condition = "parent.id = child.department_id")
// Complex multi-condition join with OR logic
@JoinCustomCondition(condition = "(dept.id = emp.dept_id OR dept.backup_id = emp.dept_id) AND dept.active = true")
// Multi-table join condition
@JoinCustomCondition(condition = "p.id = l.id_prestation OR p.id = r.id_prestation OR p.id = e.id_presta_fact")@With
Defines a Common Table Expression (CTE) for complex queries with support for dynamic variables.
Basic Static Query
@With(query = "SELECT * FROM legacy_employees WHERE status = 'ACTIVE'")
@Table("legacy_employees")
public class ActiveEmployee {
@Id
private Long id;
private String firstName;
private String status;
}Dynamic Schema Names (Multi-tenant)
@With(query = "SELECT * FROM ${schema}.employees WHERE status = :status")
@Table("active_employees")
public class ActiveEmployee {
@Id
private Long id;
private String firstName;
private String status;
}
// Usage with runtime schema and parameters
DevExtremeResult<ActiveEmployee> result = gridDataProvider
.query(ActiveEmployee.class)
.loadOptions(loadOptions)
.schema("tenant_123") // Dynamic schema name
.withParameter("status", "ACTIVE") // Parameters for WITH query
.executeDevExtreme();
// Generated SQL:
// WITH ae AS (
// SELECT * FROM tenant_123.employees WHERE status = :status
// )
// SELECT * FROM "active_employees" WHERE ...Multiple Dynamic Parameters
@With(query = """
SELECT e.id, e.first_name, e.last_name, e.salary, e.hire_date
FROM ${schema}.employees e
WHERE e.hire_date >= :fromDate
AND e.hire_date <= :toDate
AND e.salary >= :minSalary
""")
@Table("employee_report")
public class EmployeeReport { }
// Usage
DevExtremeResult<EmployeeReport> result = gridDataProvider
.query(EmployeeReport.class)
.loadOptions(loadOptions)
.schema("hr_prod")
.withParameter("fromDate", LocalDate.of(2024, 1, 1))
.withParameter("toDate", LocalDate.of(2024, 12, 31))
.withParameter("minSalary", new BigDecimal("50000"))
.executeDevExtreme();Placeholder Types
| Placeholder | Description | Security |
|---|---|---|
${schema} | Replaced with schema parameter | Validated (alphanumeric, underscores, dots only) |
:paramName | JDBC named parameters | Bound via PreparedStatement (SQL injection safe) |
${customVar} | Custom context variables | Validated and replaced |
@Transient
Excludes a field from database mapping.
import com.dev10.picnic.annotations.Transient;
@Transient
private transient String temporaryValue;
@Transient
private String calculatedField; // Computed in Java, not from database@ArrayColumn
Marks a field as mapping to a database-native array or JSON column.
Supported Array Types:
- POSTGRESQL_ARRAY: Native PostgreSQL arrays (text[], integer[], uuid[], etc.)
- JSONB: PostgreSQL JSONB column containing an array
- JSON: PostgreSQL JSON column containing an array
PostgreSQL Array Example
@Table("employees")
public class Employee {
@Id
private Long id;
private String firstName;
// PostgreSQL text[] array
@ArrayColumn(type = ArrayType.POSTGRESQL_ARRAY)
private List<String> skills;
// PostgreSQL integer[] array with custom column name
@ArrayColumn(value = "badge_ids", type = ArrayType.POSTGRESQL_ARRAY)
private List<Integer> badgeIds;
}
// Database schema:
// CREATE TABLE employees (
// id SERIAL PRIMARY KEY,
// first_name TEXT,
// skills TEXT[],
// badge_ids INTEGER[]
// );JSONB Array with Complex Objects
public class Address {
private String street;
private String city;
private String zipCode;
}
@Table("customers")
public class Customer {
@Id
private Long id;
private String name;
// JSONB array containing complex objects
@ArrayColumn(type = ArrayType.JSONB, elementType = Address.class)
private List<Address> addresses;
// JSONB array with simple types
@ArrayColumn(type = ArrayType.JSONB)
private List<String> tags;
}When to Use
- Use
ArrayType.POSTGRESQL_ARRAYfor simple types (strings, numbers, UUIDs) - more efficient - Use
ArrayType.JSONBfor complex objects or when you need to query nested properties - Use
ArrayType.JSONonly when you need to preserve exact JSON formatting
Difference from @MappedCollection
@ArrayColumn: For database-native arrays (single table, single row)@MappedCollection: For JOIN-based relationships (separate table, multiple rows)