I'm planning to display an listview of CarServiceEntries.
The CarServiceEntry class contains basic data of a service:
#Entity
abstract class CarServiceEntry(
private int id;
private Date date;
private float odometer;
/*
getters and setters constructor....
*/
)
CarServiceEntry is abstract as the classes, which inherit it, have more detailied information:
#Entity
abstract class Income extends CarServiceEntry(
#Converter(....)
private PaymentType paymentType;
private float totalAmount;
/*
getters and setters constructor....
*/
)
The issue arrises with building the SQLScheme.
As I want to load all entries TOGETHER rather than making an own sqltable for each CarServiceEntry and query each table (getAllIncomes() getAllExpenses() etc), how can I load every CarServiceEntry(income,expense, service) using one sqltabletable for loading (if possible).
The current approach, which I'm not a fan of, looks like this:
CREATE TABLE CarServiceEntry(
id INTEGER PRIMARY KEY,
serviceType TEXT CHECK CONSTRAINT (....) //expense, income, service)
date,
odometer,
/*
A LOT of fields as each service has its own "unique" fields and I put it all together which I don' really like :( loading 30+ fields just for one sql statement is not something I like, unless I don't have any other option.
*/
)
I'd rather search for a solution like this:
CREATE TABLE CarServiceEntry(
id INTEGER PRIMARY KEY,
date DATE,
odometer NUMBER
)
CREATE TABLE Income INHERITS CarServiceEntry(
paymentType TEXT,
totalAmount NUMBER
/*some other fields*/
)
CREATE TABLE Expense INHERITS CarServiceEntry(
location TEXT
totalCost NUMBER
/*some other fields*/
)
==>
#Query("SELECT * FROM CarServiceEntry") //this should also return Income and Expense table
Flowable<List<CarServiceEntry>> getAllEntries();
Is there a way to it this way? Or is creating one table with a bulk of fields the only way?
According to: sql inheritance creating an FOREIGN KEY to my subentrytype tables would be kind of inheritance, however this does not solve my issue, as I would still have to need to load every table. I guess the only solution would be using one table with many null value fields?
SQLite doesn't support inheritance and I believe that it will be as simple, if not simpler, to utilise relationships which SQLite and Room support.
Creating multiple tables via room is pretty easy as is creating and handling relationships. So I would suggest taking the typical approach.
Here's an example based upon what I think that yo are trying to accomplish.
First the CarServiceEntry table (which will later have Expenses and Incomes related to it) :-
public class CarServiceEntry {
#PrimaryKey
private Long id;
private String date;
private float odometer;
public CarServiceEntry(){}
.... getters and setters removed for brevity
/* more convenient constructor (see cse3 in example) */
/* #Ignore to supress Room warning */
#Ignore
public CarServiceEntry(String date, Float odometer) {
this.date = date;
this.odometer = odometer;
}
}
note simplified so TypeConverters aren't required
Next the income table :-
#Entity(tableName = "income",
foreignKeys = {
#ForeignKey(
entity = CarServiceEntry.class,
parentColumns = "id",
childColumns = "incomeCarServiceEntryId",
onDelete = CASCADE,
onUpdate = CASCADE
)
},
indices = {#Index(
value = {"incomeCarServiceEntryId"}
)}
)
public class Income {
#PrimaryKey
private Long incomeId;
private Long incomeCarServiceEntryId;
private int paymentType;
private float totalAmount;
.... getters and setters
}
note foreign keys = { .... } nor indicies = { .... } are required but suggested that they be used as the help to ensure referential integrity
Note the additional column incomeCarServiceEntryId this is the ID of the related CarServiceEntry.
Next the expense table (pretty similar to income table):-
#Entity(tableName = "expense",
foreignKeys = {
#ForeignKey(
entity = CarServiceEntry.class,
parentColumns = {"id"},
childColumns = {"expenseCarServiceEntryId"},
onDelete = CASCADE,
onUpdate = CASCADE
)
},
indices = {
#Index(
value = {"expenseCarServiceEntryId"}
)}
)
public class Expense {
#PrimaryKey
private Long expenseId;
private long expenseCarServiceEntryId;
private String location;
private float totalCost;
.... getters and setters
}
Now a POJO (not a table) for extracting the related data (i.e CarServiceEntry with all of the related Incomes and all the related Expenses) named CarServiceEntryWithIncomeWithExpense :-
public class CarServiceEntryWithIncomeWithExpense {
#Embedded
CarServiceEntry carServiceEntry;
#Relation(entity = Income.class,parentColumn = "id",entityColumn = "incomeCarServiceEntryId")
List<Income> incomeList;
#Relation(entity = Expense.class,parentColumn = "id",entityColumn = "expenseCarServiceEntryId")
List<Expense> expenseList;
}
YES that's it
Now the Dao's (All in One) AllDao :-
#Dao
interface AllDao {
#Insert
long insert(CarServiceEntry carServiceEntry);
#Insert
long insert(Expense expense);
#Insert
long insert(Income income);
#Query("SELECT * FROM car_service_entry")
List<CarServiceEntryWithIncomeWithExpense> getAllCarServiceEntriesWithIncomesAndWithExpenses();
}
The #Database (includes singleton approach) named Database (probably better to use another name) :-
#Database(entities = {CarServiceEntry.class,Income.class,Expense.class},version = 1)
public abstract class Database extends RoomDatabase {
abstract AllDao getAllDao();
private static volatile Database instance;
public static Database getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(context,Database.class,"carservice.db")
.allowMainThreadQueries()
.build();
}
return instance;
}
}
Note for brevity and convenience of the demo, the main thread is used.
Finally putting it all together and demonstrating MainActivity.
The demo adds 3 carServiceEntries, with incomes and expenses (the 3rd has none of either). It then extracts the 3 carServiceEntries with all the incomes and expenses. Traversing the extracted and outputting what has been extracted to the log.
:-
public class MainActivity extends AppCompatActivity {
Database db;
AllDao dao;
private static final String TAG = "CSEINFO";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
db = Database.getInstance(this);
dao = db.getAllDao();
/* Add a Service Entry noting it's ID */
CarServiceEntry cse1 = new CarServiceEntry();
cse1.setDate("2021-06-01");
cse1.setOdometer(5120.78F);
long cse1Id = dao.insert(cse1);
/* Add another Service Entry noting it's id */
CarServiceEntry cse2 = cse1;
cse2.setDate("2021-06-02");
cse2.setOdometer(7065.83F);
long cse2Id = dao.insert(cse2);
/* Use a single Income to add 3 Income Entries for (linked to) the 1st Service Entry */
Income incomeEntry = new Income();
incomeEntry.setIncomeCarServiceEntryId(cse1Id);
incomeEntry.setPaymentType(10);
incomeEntry.setTotalAmount(120.00F);
dao.insert(incomeEntry);
incomeEntry.setPaymentType(20);
incomeEntry.setTotalAmount(230.00F);
dao.insert(incomeEntry);
incomeEntry.setPaymentType(15);
incomeEntry.setTotalAmount(75.55F);
dao.insert(incomeEntry);
/* Use the same Income Entry to add 1 Entry for the 2nd Service Entry */
incomeEntry.setIncomeCarServiceEntryId(cse2Id);
incomeEntry.setPaymentType(25);
incomeEntry.setTotalAmount(134.56F);
dao.insert(incomeEntry);
/* Add some Expense Entries */
Expense expenseEntry = new Expense();
expenseEntry.setExpenseCarServiceEntryId(cse1Id);
expenseEntry.setLocation("London");
expenseEntry.setTotalCost(500.00F);
dao.insert(expenseEntry);
expenseEntry.setLocation("New York");
expenseEntry.setTotalCost(60.66F);
dao.insert(expenseEntry);
expenseEntry.setExpenseCarServiceEntryId(cse2Id);
expenseEntry.setLocation("Paris");
dao.insert(expenseEntry);
expenseEntry.setLocation("Hamburg");
dao.insert(expenseEntry);
expenseEntry.setLocation("Madrid");
dao.insert(expenseEntry);
dao.insert(new CarServiceEntry("2021-06-03",1765.34F));
for (CarServiceEntryWithIncomeWithExpense cse: dao.getAllCarServiceEntriesWithIncomesAndWithExpenses()) {
Log.d(
TAG,
"CSE ID = " + cse.carServiceEntry.getId() +
" Date = " + cse.carServiceEntry.getDate() +
" ODO = " + cse.carServiceEntry.getOdometer()
);
for (Income i: cse.incomeList) {
Log.d(
TAG,
"\tIncome Payment Type is " + i.getPaymentType() + " Total is " + i.getTotalAmount()
);
}
for(Expense e: cse.expenseList) {
Log.d(
TAG,
"\tExpense Location is " + e.getLocation() + " Total is " + e.getTotalCost()
);
}
}
}
}
Result
The following is output to the Log :-
2021-06-11 13:01:35.116 D/CSEINFO: CSE ID = 1 Date = 2021-06-01 ODO = 5120.78
2021-06-11 13:01:35.116 D/CSEINFO: Income Payment Type is 10 Total is 120.0
2021-06-11 13:01:35.116 D/CSEINFO: Income Payment Type is 20 Total is 230.0
2021-06-11 13:01:35.116 D/CSEINFO: Income Payment Type is 15 Total is 75.55
2021-06-11 13:01:35.116 D/CSEINFO: Expense Location is London Total is 500.0
2021-06-11 13:01:35.116 D/CSEINFO: Expense Location is New York Total is 60.66
2021-06-11 13:01:35.116 D/CSEINFO: CSE ID = 2 Date = 2021-06-02 ODO = 7065.83
2021-06-11 13:01:35.117 D/CSEINFO: Income Payment Type is 25 Total is 134.56
2021-06-11 13:01:35.117 D/CSEINFO: Expense Location is Paris Total is 60.66
2021-06-11 13:01:35.117 D/CSEINFO: Expense Location is Hamburg Total is 60.66
2021-06-11 13:01:35.117 D/CSEINFO: Expense Location is Madrid Total is 60.66
2021-06-11 13:01:35.117 D/CSEINFO: CSE ID = 3 Date = 2021-06-03 ODO = 1765.34
Related
I've got a sample table:
I have a Sample table which has a "creationDate" as an atribute. What I want is a way to increment(update) the "numOfTimesUpdated" attribute each 24h since the creationdate. so lets say "creationdate" is 01.01.2021 12:12 AM => numOfTimesUpdated=0, 02.01.2021 12:12 AM => numOfTimesUpdated=1, 03.01.2021 12:12 AM => numOfTimesUpdated=3.
How can I implement something like this in the best way?
Does SQLite has some kind of background scheduler/trigger where a UPDATE Query gets automatically called? Or Is my only chance the client side(application) using smth. like an ApplicationManager?
How can I implement something like this in the best way?
You don't appear to even need a numberOfTimesUpdated column as the number of days since the creationDate can be calculated when required.
If the date/time were stored in a supported format (e.g. YYYY-MM-DD HH:MM) it is very simple.
For example consider this :-
DROP TABLE IF EXISTS table1;
CREATE TABLE IF NOT EXISTS table1 (id INTEGER PRIMARY KEY, name TEXT, creationdate TEXT);
INSERT INTO table1 VALUES
(null,'myname','2021-01-02'),(null,'anothername','2021-03-03'),(null,'andanother','2021-06-06')
;
SELECT *,strftime('%s','now')/(60 * 60 * 24) - strftime('%s',creationdate)/(60 * 60 * 24) AS numOfTimesUpdated FROM table1;
DROP TABLE IF EXISTS table1;
It :-
Drops the table if it exists
Creates the table
Inserts 3 rows with creation dates (21st Jan 2021, 3rd March 2021 and 6th June 2021)
Extracts all of the rows PLUS a calculated column with the number of days since the creation date.
Cleans up the test environment by deleting the table.
The results as run on 13th June 2021 are :-
Does SQLite has some kind of background scheduler/trigger where a UPDATE Query gets automatically called?
not time based.
Or Is my only chance the client side(application) using smth. like an ApplicationManager?
Yes, but again you don't appear to need this.
Working Room Example
The following is a working room example that implements the SQLite example above: -
The Table1 Entity :-
#Entity(tableName = "table1")
public class Table1 {
#PrimaryKey
Long id;
String name;
String creationDate;
public Table1(){}
#Ignore
public Table1(String name, String creationDate) {
this.name = name;
this.creationDate = creationDate;
}
}
Note as in theory id's can be long long instead of int has been used. As long MUST have a value Long has been used to allow autogenerated id's (without the inefficient AUTOGENERATE).
A POJO Table1WithNumberOfUpdates to get the Table1 with the additional calculated column:-
public class Table1WithNumberOfUpdates {
#Embedded
Table1 table1;
int numOfTimesUpdated;
}
A Dao AllDao to allow inserts and extracting a List of Table1WithNumberOfUpdates objects :-
#Dao
interface AllDao {
#Insert
long insert(Table1 table1);
#Query("SELECT *, strftime('%s','now')/(60 * 60 * 24) - strftime('%s',creationdate)/(60 * 60 * 24) AS numOfTimesUpdated FROM table1")
List<Table1WithNumberOfUpdates> getTable1WithNumberOfUpdatesList();
}
A standard #Database that returns an instance of the Database :-
#Database(entities = {Table1.class},exportSchema = false,version = 1)
abstract class TheDatabase extends RoomDatabase {
abstract AllDao getAllDao();
private static volatile TheDatabase instance;
public static TheDatabase getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(
context,
TheDatabase.class,
"state.db"
)
.allowMainThreadQueries()
.build();
}
return instance;
}
}
And finally some code in an Activity to add the three rows and extract the result outputting it to the log :-
public class MainActivity extends AppCompatActivity {
TheDatabase db;
AllDao dao;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Instantiate Database and get dao
db = TheDatabase.getInstance(this);
dao = db.getAllDao();
dao.insert(new Table1("myname","2021-01-02"));
dao.insert(new Table1("anothername","2021-03-03"));
dao.insert(new Table1("andanothername","2021-06-06"));
for (Table1WithNumberOfUpdates t: dao.getTable1WithNumberOfUpdatesList()) {
Log.d("TABLE1INFO","Name is " + t.table1.name + " Created = " + t.table1.creationDate + " Updates Since Creation = " + t.numOfTimesUpdated);
}
}
}
Result :-
2021-06-14 10:17:44.498 D/TABLE1INFO: Name is myname Created = 2021-01-02 Updates Since Creation = 163
2021-06-14 10:17:44.499 D/TABLE1INFO: Name is anothername Created = 2021-03-03 Updates Since Creation = 103
2021-06-14 10:17:44.499 D/TABLE1INFO: Name is andanothername Created = 2021-06-06 Updates Since Creation = 8
Days are 1 extra due to device using local time zone as opposed to the SQLite Tool used above (Navicat for SQLite).
I have the following ORM model:
Basically I want to track all public facilities of a state. So I have 1-n Relationship with a public facility like e.g. school:
public class StateWithFacilities {
#Embedded
State state;
#Relation(entity = School.class,parentColumn = "schoolId",entityColumn = "id")
ArrayList<School> allSchools;
}
The issue is that, I'm don't quite sure how to get allLocations which belong to a school, as a school has a HashMap of locations where the value represents to cost of the building.
My 1) idea looks like this:
public class StateWithFacilities {
#Embedded
State state;
#Relation(entity = School.class,parentColumn = "schoolId",entityColumn = "id")
ArrayList<SchoolWithLocations> allSchools;
}
public class SchoolWithLocations {
#Embedded
School school;
#Relation(entity = Location.class,parentColumn = "locationId",entityColumn = "id")
HashMap<Location, float> alllocationsWithCost;
}
The proposed idea wouldn't work, since I can't keep track of the cost of the building. What would the best solution be? I want to have the implemented, rather than creating a new table/entity class unless I don't have another option.
I think you might be causing yourself angst because of how you are handling the relationships.
Considering StateWithFacilities
You appear to be saying get the school who's id is equal to the schoolId column in the state.
While the realtionship should be from a column in the School that stores the appropriate StateId.
You appear to be using the reverse.
Example
Perhaps consider this example based upon what you appear to be wanting to do: (States, Schools and Locations have been given a name column to make the output easier to understand)-
State class (Entity and therefore Table) which is top of the hierarchy.
#Entity
public class State {
#PrimaryKey
Long stateId;
String stateName;
// etc //
public State(){}
#Ignore
public State(String stateName){
this.stateName = stateName;
}
.... getters and setters
}
School class (Entity and therefore Table) which will belong to a State.
#Entity(
foreignKeys = {
#ForeignKey(entity = State.class,parentColumns = "stateId",childColumns = "stateIdMap"),
},
indices = {#Index("stateIdMap")}
)
public class School {
#PrimaryKey
Long schoolId;
Long stateIdMap;
String schoolName;
// etc
public School(){}
#Ignore
public School(String schoolName, long stateId) {
this.schoolName = schoolName;
this.stateIdMap = stateId;
}
.... getters and setters
}
ForeignKeys aren't necessary but can assist with maintaining referential integrity.
Likewise the index on the stateIdMap column isn't required but if the ForeignKeys are defined Room issues a warning if the index doesn't exist.
Location class (Entity and therefore Table) which will belong to a School (a Scholl can have many Locations).
#Entity(
foreignKeys = {
#ForeignKey(entity = School.class,parentColumns = "schoolId",childColumns = "schoolIdMap")
},
indices = {#Index("schoolIdMap")}
)
public class Location {
#PrimaryKey
Long locationId;
Long schoolIdMap;
String locationName;
float x1;
float y1;
float x2;
float y2;
// etc
public Location(){}
#Ignore
public Location(String locationName,long schoolId, float x1, float y1, float x2, float y2) {
this.locationName = locationName;
this.schoolIdMap = schoolId;
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
.... getters and setters
}
To make the demo easier to read, Location has been given a name.
To cater for retrieving a parent with it's children the following POJO's are used :-
SchoolWithLocations
public class SchoolWithLocations {
#Embedded
School school;
#Relation(entity = Location.class,parentColumn = "schoolId",entityColumn = "schoolIdMap")
List<Location> locationList;
}
StateWithSchoolsWithLocations
public class StateWithSchoolsWithLocations {
#Embedded
State state;
#Relation(entity = School.class, parentColumn = "stateId",entityColumn = "stateIdMap")
List<SchoolWithLocations> schoolWithLocationsList;
}
A Dao AllDao with some common useful Dao's :-
#Dao
interface AllDao {
#Insert
long insert(State state);
#Insert
long[] insert(State...states);
#Insert
long insert(Location location);
#Insert
long[] insert(Location...locations);
#Insert
long insert(School school);
#Insert
long[] insert(School...schools);
#Query("SELECT * FROM State")
List<State> getAllStates();
#Query("SELECT * FROM State WHERE stateId=:stateId")
State getStateById(long stateId);
#Query("SELECT * FROM Location")
List<Location> getAllLocations();
#Query("SELECT * FROM Location WHERE locationId=:locationId")
Location getLocationById(long locationId);
#Query("SELECT * FROM Location WHERE x1=:x1 AND y1=:y1 AND x2=:x2 AND y2=:y2")
Location getLocationByCoords(float x1,float y1,float x2,float y2);
#Query("SELECT * FROM School")
List<School> getAllSchools();
#Transaction
#Query("SELECT * FROM State")
List<StateWithSchoolsWithLocations> getStateWithSchoolsAndLocations();
#Transaction
#Query("SELECT * FROM State WHERE stateId=:stateId")
List<StateWithSchoolsWithLocations> getStateByIdWithSchoolsAndLocations(long stateId);
}
A Database class TheDatabase
#Database(entities = {State.class,Location.class,School.class},exportSchema = false,version = 1)
abstract class TheDatabase extends RoomDatabase {
abstract AllDao getAllDao();
private static volatile TheDatabase instance;
public static TheDatabase getInstance(Context context) {
if (instance == null) {
instance = Room.databaseBuilder(
context,
TheDatabase.class,
"state.db"
)
.allowMainThreadQueries()
.build();
}
return instance;
}
}
And finally and activity to demonstrate (run on the main thread) :-
public class MainActivity extends AppCompatActivity {
TheDatabase db;
AllDao dao;
static final String TAG = "StateINFO";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//Instantiate Database and get dao
db = TheDatabase.getInstance(this);
dao = db.getAllDao();
// Add 3 states
long s1Id = dao.insert(new State("State1"));
long s2Id = dao.insert(new State("State2"));
// Add 2 Schools (in State1)
long sc1 = dao.insert(new School("School1 in State1",s1Id));
long sc2 = dao.insert(new School("School2 in State1",s1Id));
// Add 4 Locations
long l1Id = dao.insert(new Location("Loc1",sc1,1f,1f,2f,2f));
long l2Id = dao.insert(new Location("Loc2",sc1,2f,2f,3f,3f));
long l3Id = dao.insert(new Location("Loc3",sc1,3f,3f,4f,4f));
long l4Id = dao.insert(new Location("Loc4",sc2,4f,4f,5f,5f));
// Get Everything via State
for (StateWithSchoolsWithLocations swswl: dao.getStateWithSchoolsAndLocations() ) {
Log.d(TAG,"State is " + swswl.state.stateName);
for (SchoolWithLocations s: swswl.schoolWithLocationsList) {
Log.d(TAG,"\tSchool is " + s.school.schoolName);
for (Location l: s.locationList) {
Log.d(TAG,"\t\tLocation is " + l.locationName + " XYvalues are X1=" + l.x1 + " Y1=" + l.y2 + " X2=" + l.x2 + " Y2=" + l.y2);
}
}
}
}
}
Result
As can be seen it's easy to retrieve all the locations and the x1..Y2 values. The log, when the above is run, includes :-
2021-06-13 08:53:40.748 D/StateINFO: State is State1
2021-06-13 08:53:40.748 D/StateINFO: School is School1 in State1
2021-06-13 08:53:40.748 D/StateINFO: Location is Loc1 XYvalues are X1=1.0 Y1=2.0 X2=2.0 Y2=2.0
2021-06-13 08:53:40.748 D/StateINFO: Location is Loc2 XYvalues are X1=2.0 Y1=3.0 X2=3.0 Y2=3.0
2021-06-13 08:53:40.748 D/StateINFO: Location is Loc3 XYvalues are X1=3.0 Y1=4.0 X2=4.0 Y2=4.0
2021-06-13 08:53:40.748 D/StateINFO: School is School2 in State1
2021-06-13 08:53:40.748 D/StateINFO: Location is Loc4 XYvalues are X1=4.0 Y1=5.0 X2=5.0 Y2=5.0
2021-06-13 08:53:40.748 D/StateINFO: State is State2
I want to have the implemented, rather than creating a new table/entity class unless I don't have another option.
Whether or not the above could be used to make appropriate corrections to keep your current tables is something that you would have to determine.
Additional re HashMaps
The method added to SchoolWithLocations POJO :-
public HashMap<String,Float> getLocationsAsHashMap() {
HashMap<String,Float> rv = new HashMap<>();
for (Location l: locationList) {
String basekey = this.getClass().getSimpleName() + (rv.size() + 1);
rv.put(basekey+"x1",l.x1);
rv.put(basekey+ "y1",l.y1);
rv.put(basekey+"x2",l.x2);
rv.put(basekey+"y2",l.y2);
}
return rv;
}
The method added to School
public HashMap<String,Float> getLocationsAsHashMap(AllDao dao) {
HashMap<String,Float> rv = new HashMap<>();
for(Location l: dao.getLocationsBySchool(schoolId)) {
String basekey = this.getClass().getSimpleName() + (rv.size() + 1);
rv.put(basekey+"x1",l.x1);
rv.put(basekey+ "y1",l.y1);
rv.put(basekey+"x2",l.x2);
rv.put(basekey+"y2",l.y2);
}
return rv;
}
Notice the Subtle differences. As the School object does not contain the Locations then these need to be retrieved from the database. Hence, it needs an instance of the AllDao as it uses a dao to get the Locations.
AllDao
The following was added to AllDao to facilitate getting the applicable Locations for the School :-
#Query("SELECT * FROM location WHERE schoolIdMap=:schoolId")
List<Location> getLocationsBySchool(long schoolId);
The Amended loop that traverses the retrieved List of StateWithSchoolsWithLocations
// Get Everything via State
HashMap<String,Float> locations = new HashMap<>(); //<<<<< ADDED
HashMap<String,Float> locationsFromSchool = new HashMap<>(); //<<<<<ADDDED
for (StateWithSchoolsWithLocations swswl: dao.getStateWithSchoolsAndLocations() ) {
Log.d(TAG,"State is " + swswl.state.stateName);
for (SchoolWithLocations s: swswl.schoolWithLocationsList) {
Log.d(TAG,"\tSchool is " + s.school.schoolName);
for (Location l: s.locationList) {
Log.d(TAG,"\t\tLocation is " + l.locationName + " XYvalues are X1=" + l.x1 + " Y1=" + l.y2 + " X2=" + l.x2 + " Y2=" + l.y2);
}
/* ADDED get HashMap of Locations */
locations = s.getLocationsAsHashMap();
/* OR */
locationsFromSchool = s.school.getLocationsAsHashMap(dao);
Float value = 99.99999F; //<<<<< ADDED for setting a breakpoint
}
}
Result of the Amended Code
A breakpoint was added to the Line Float value = 99.99999F and run in debug mode.
When the Breakpoint was first hit (first StateWithSchoolsAndWithLocations) the debug window was :-
The Second Breakpoint :-
There are many questions about seeding many-to-many relationships in Entity Framework. However, most of them are extremely old, and many-to-many behavior has changed significantly in EFCore5. The official docs recommend overriding OnModelCreating to implement ModelBuilder.Entity<>.HasData().
However, with the new many-to-many behavior (without explicit mappings), I can find no clear path to seed the intermediate tables. To use the example of this tutorial, the BookCategories class is now implicit. Therefore, there is no path to explicitly declare the intermediate table values while seeding.
I've also tried simply assigning the arrays, e.g.,:
public class Book
{
public int BookId { get; set; }
public string Title { get; set; }
public ICollection<Category> Categories { get; set; }
}
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public ICollection<Book> Books { get; set; }
}
And then at seed time:
Book book = new Book() { BookId = 1, Title = "Brave New World" }
Category category = new Category() { CategoryId = 1, CategoryName = "Dystopian" }
category.Books = new List<Book>() { book };
book.Categories = new List<Category>() { category };
modelBuilder.Entity<Book>().HasData(book);
modelBuilder.Entity<Category>().HasData(category);
... but there are no entries created for BookCategories in the resulting migration. This was somewhat expected, as this article suggests that one must explicitly seed the intermediate table. What I want is something like this:
modelBuilder.Entity<BookCategory>().HasData(
new BookCategory() { BookId = 1, CategoryId = 1 }
);
However, again, since there is no concrete class to describe BookCategories in EFCore5, the only way I can think of to seed the table is to manually edit the migration with additional MigrationBuilder.InsertData commands, which rather defeats the purpose of seeding data via application code.
However, again, since there is no concrete class to describe BookCategories in EFCore5
Actually, as explained in the What's new link, EF Core 5 allows you to have explicit join entity
public class BookCategory
{
public int BookId { get; set; }
public EBook Book { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
}
and configure the many-to-many relationship to use it
modelBuilder.Entity<Book>()
.HasMany(left => left.Categories)
.WithMany(right => right.Books)
.UsingEntity<BookCategory>(
right => right.HasOne(e => e.Category).WithMany(),
left => left.HasOne(e => e.Book).WithMany().HasForeignKey(e => e.BookId),
join => join.ToTable("BookCategories")
);
This way you can use all normal entity operations (query, change tracking, data model seeding etc.) with it
modelBuilder.Entity<BookCategory>().HasData(
new BookCategory() { BookId = 1, CategoryId = 1 }
);
still having the new many-to-many skip navigations mapping.
This is probably the simplest as well as the type-safe approach.
In case you thing it's too much, using the conventional join entity is also possible, but you need to know the shared dictionary entity type name, as well as the two shadow property names. Which as you will see by convention might not be what you expect.
So, by convention the join entity (and table) name is
{LeftEntityName}{RightEntityName}
and the shadow property (and column) names are
{LeftEntityNavigationPropertyName}{RightEntityKeyName}
{RightEntityNavigationPropertyName}{LeftEntityKeyName}
The first question would be - which is the left/right entity? The answer is (not documented yet) - by convention the left entity is the one which name is less in alphabetical order. So with your example Book is left, Category is right, so the join entity and table name would be BookCategory.
It can be changed adding explicit
modelBuilder.Entity<Category>()
.HasMany(left => left.Books)
.WithMany(right => right.Categories);
and now it would be CategoryBook.
In both cases the shadow property (and column) names would be
CategoriesCategoryId
BooksBookId
So neither the table name, nor the property/column names are what you'd normally do.
And apart from the database table/column names, the entity and property names are important because you'd need them for entity operations, including the data seeding in question.
With that being said, even if you don't create explicit join entity, it's better to configure fluently the one created automatically by EF Core convention:
modelBuilder.Entity<Book>()
.HasMany(left => left.Categories)
.WithMany(right => right.Books)
.UsingEntity("BookCategory", typeof(Dictionary<string, object>),
right => right.HasOne(typeof(Category)).WithMany().HasForeignKey("CategoryId"),
left => left.HasOne(typeof(Book)).WithMany().HasForeignKey("BookId"),
join => join.ToTable("BookCategories")
);
Now you can use the entity name to access the EntityTypeBuilder
modelBuilder.Entity("BookCategories")
and you can seed it similar to normal entities with shadow FK properties with anonymous type
modelBuilder.Entity("BookCategory").HasData(
new { BookId = 1, CategoryId = 1 }
);
or for this specific property bag type entity, also with Dictionary<string, object> instances
modelBuilder.Entity("BookCategory").HasData(
new Dictionary<string, object> { ["BookId"] = 1, ["CategoryId"] = 1 }
);
Update:
People seem to misinterpret the aforementioned "extra" steps and find them redundant and "too much", not needed.
I never said they are mandatory. If you know the conventional join entity and property names, go ahead directly to the last step and use anonymous type or Dictionary<string, object>.
I already explained the drawbacks of taking that route - loosing the C# type safety and using "magic" strings out of your control. You have to be smart enough to know the exact EF Core naming conventions and to realize that if you rename class Book to EBook the new join entity/table name will change from "BookCategory" to "CategoryEBook" as well as the order of the PK properties/columns, associated indexes etc.
Regarding the concrete problem with data seeding. If you really want to generalize it (OP attempt in their own answer), at least make it correctly by using the EF Core metadata system rather than reflection and assumptions. For instance, the following will extract these names from the EF Core metadata:
public static void HasJoinData<TFirst, TSecond>(
this ModelBuilder modelBuilder,
params (TFirst First, TSecond Second)[] data)
where TFirst : class where TSecond : class
=> modelBuilder.HasJoinData(data.AsEnumerable());
public static void HasJoinData<TFirst, TSecond>(
this ModelBuilder modelBuilder,
IEnumerable<(TFirst First, TSecond Second)> data)
where TFirst : class where TSecond : class
{
var firstEntityType = modelBuilder.Model.FindEntityType(typeof(TFirst));
var secondEntityType = modelBuilder.Model.FindEntityType(typeof(TSecond));
var firstToSecond = firstEntityType.GetSkipNavigations()
.Single(n => n.TargetEntityType == secondEntityType);
var joinEntityType = firstToSecond.JoinEntityType;
var firstProperty = firstToSecond.ForeignKey.Properties.Single();
var secondProperty = firstToSecond.Inverse.ForeignKey.Properties.Single();
var firstValueGetter = firstToSecond.ForeignKey.PrincipalKey.Properties.Single().GetGetter();
var secondValueGetter = firstToSecond.Inverse.ForeignKey.PrincipalKey.Properties.Single().GetGetter();
var seedData = data.Select(e => (object)new Dictionary<string, object>
{
[firstProperty.Name] = firstValueGetter.GetClrValue(e.First),
[secondProperty.Name] = secondValueGetter.GetClrValue(e.Second),
});
modelBuilder.Entity(joinEntityType.Name).HasData(seedData);
}
Also here you don't need to know which type is "left" and which is "right", neither requires special base class or interface. Just pass sequence of entity pairs and it will properly seed the conventional join entity, e.g. with OP example, both
modelBuilder.HasJoinData((book, category));
and
modelBuilder.HasJoinData((category, book));
would do.
Update (EF Core 5.0.2)
It's working well using the name of the associative table:
builder.Entity("ContractDeclarationType").HasData(
new { ContractsId = 1L, DeclarationTypesId = 1L },
new { ContractsId = 1L, DeclarationTypesId = 2L },
new { ContractsId = 1L, DeclarationTypesId = 3L });
I ended up whipping up a generic solution to this problem based upon the answer from Ivan (thanks!). I'm now able to seed all my M2M tables with this syntax:
// Add book1 and book2 to category1:
modelBuilder.HasM2MData(new [] { book1, book2 }, new [] { category1 });
This may not be fully robust, but it should work with conventional M2M mappings.
It makes some assumptions:
T1 & T2 Inherit from some ModelBase that provides an Id property.
T1 & T2 Have exactly one ICollection<OtherType> property.
You know the correct order (which model is T1 and which is T2) — this can be discovered by running the migration for the tables first and inspecting the migration.
You're running EFCore5 RC2 or later (see this issue).
public static void HasM2MData<T1, T2>
(this ModelBuilder mb, T1[] t1s, T2[] t2s)
where T1 : ModelBase where T2 : ModelBase
{
string table = $"{typeof(T1).Name}{typeof(T2).Name}";
PropertyInfo t1Prop = GetM2MProperty<T1, T2>();
PropertyInfo t2Prop = GetM2MProperty<T2, T1>();
string t1Key = $"{t1Prop.Name}Id";
string t2Key = $"{t2Prop.Name}Id";
foreach (T1 t1 in t1s) {
foreach (T2 t2 in t2s) {
mb.Entity(table).HasData(new Dictionary<string, object>() { [t2Key] = t1.Id, [t1Key] = t2.Id });
}
}
}
// Get a property on T1 which is assignable to type ICollection<T2>, representing the m2m relationship
private static PropertyInfo GetM2MProperty<T1, T2>() {
Type assignableType = typeof(ICollection<T2>);
List<PropertyInfo> props = typeof(T1).GetProperties()
.Where(pi => pi.PropertyType.IsAssignableTo(assignableType))
.ToList();
if (props.Count() != 1) {
throw new SystemException(
$"Expected {typeof(T1)} to have exactly one column of type {assignableType}; got: {props.Count()}");
}
return props.First();
}
In the migration, we see something like:
migrationBuilder.InsertData(
table: "BookCategory",
columns: new[] { "BooksId", "CategoriesId" },
values: new object[,]
{
{ "book1", "category1" },
{ "book2", "category1" }
});
I am trying to have SQLite create automatic timestamps with CURRENT_TIMESTAMP.
I took the liberty of using Google's code:
// roomVersion = '2.2.2'
#Entity
public class Playlist {
#PrimaryKey(autoGenerate = true)
long playlistId;
String name;
#Nullable
String description;
#ColumnInfo(defaultValue = "normal")
String category;
#ColumnInfo(defaultValue = "CURRENT_TIMESTAMP")
String createdTime;
#ColumnInfo(defaultValue = "CURRENT_TIMESTAMP")
String lastModifiedTime;
}
#Dao
interface PlaylistDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(playlist: Playlist): Long
}
This translates into an SQLite-Statement:
CREATE TABLE `Playlist` (
`playlistId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` TEXT,
`description` TEXT,
`category` TEXT DEFAULT 'normal',
`createdTime` TEXT DEFAULT CURRENT_TIMESTAMP,
`lastModifiedTime` TEXT DEFAULT CURRENT_TIMESTAMP
)
I did make one insert:
mDb.playListDao().insert(Playlist().apply { name = "Test 1" })
But the timestamps are always Null.
With the DB Browser for SQLite I added another entry, here I get timestamps.
How do I insert without a Null-Timestamp in room?
(Info: createdTime is also always the same as lastModifiedTime. I think this has to be done with triggers in SQLite, but that is a different problem not to be discussed here).
You don't need to use another class, you can use #Query as an alternative to the convenience #Insert.
as per :-
There are 4 type of statements supported in Query methods: SELECT, INSERT, UPDATE, and DELETE.
Query
e.g.
#Query("INSERT INTO test_table001 (name) VALUES(:name) ")
void insert(String name);
You are also not limited to CURRENT_TIMESTAMP as the only means of getting the current timestamp you can use embedded datetime functions (as is shown below), which can store the value more efficiently and also be more flexible e.g. you could adjust the current time using modifiers such as '+7 days'.
If you consider the following :-
#Entity(tableName = "test_table001")
public class TestTable001 {
#PrimaryKey
Long id;
#ColumnInfo(defaultValue = "CURRENT_TIMESTAMP")
String dt1;
#ColumnInfo(defaultValue = "(datetime('now'))")
String dt2;
#ColumnInfo(defaultValue = "(strftime('%s','now'))")
String dt3;
String name;
}
Note that the inefficient autogenerate = true has not been used BUT as will be shown you can still have an SQLite assigned id (note that you must use the type Long/Integer as opposed to long or int)
Also note the alternative ways of getting the current date time (the latter being more efficient as the value will ultimately be stored as an Integer (max 8 bytes) rather than a more byte hungry String).
With a Dao as :-
#Dao
public interface TestTable001Dao {
#Insert()
long insert(TestTable001 testTable001);
#Query("INSERT INTO test_table001 (name) VALUES(:name) ")
long insert(String name);
#Query("SELECT * FROM test_table001")
List<TestTable001> getAllTestTable001();
}
And the following to test/demonstrate :-
public class MainActivity extends AppCompatActivity {
AppDatabase mRoomDB;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRoomDB = Room.databaseBuilder(this,AppDatabase.class,"testdb")
.allowMainThreadQueries()
.build();
TestTable001 tt01 = new TestTable001();
tt01.setName("tt01");
mRoomDB.useTestTable001().insert(tt01);
mRoomDB.useTestTable001().insert("tt02");
logAllTestTable001();
}
private void logAllTestTable001() {
for (TestTable001 tt: mRoomDB.useTestTable001().getAllTestTable001()) {
Log.d(
"TTINFO",
"ID = " + tt.getId() +
" Name = " + tt.getName() +
" Date1 = " + tt.getDt1() +
" Date2 = " + tt.getDt2() +
" Date3 = " + tt.getDt3());
}
}
}
The result is :-
2019-12-14 03:18:32.569 D/TTINFO: ID = 1 Name = tt01 Date1 = null Date2 = null Date3 = null
2019-12-14 03:18:32.569 D/TTINFO: ID = 2 Name = tt02 Date1 = 2019-12-13 16:18:32 Date2 = 2019-12-13 16:18:32 Date3 = 1576253912
Found it. Did not read the manual.
You have to create a 2nd class without the auto-set fields to insert.
public class NameAndDescription {
String name;
String description
}
I think, this is not a good idea.
If you have an autoincrement field in the DB it will get an automatically updated value when you pass 0.
Likewise the default value of the timestamp should be used when passing null or "".
I found the best solution was creating an abstract Dao that implemented the insert and update methods. I didn't get the default value to work (perhaps I was doing something wrong). Take a look at my answer here: How to implement created_at and updated_at column using Room Persistence ORM tools in android
there are 3 database tables (movies, reviews, users)
the reviews table include ( MemeberID, MovieID, Review Text, Rate, ReviewDate)
(the MemeberID, and MovieID in the Review are the FK of the members table and the movies table)
The Movie can have many reviews, and i'm trying to add review to a movie
even I have movie class and the member class, I have a problem, in order to insert review, i need to reference it to movie and users , link them, and i don't know how to do it
this code make a error:
" The relationship between the two objects cannot be defined because they are attached to different ObjectContext objects. "
This is my code...
public bool InsertNewReview(Movie _TheMovie, Member _TheMember, string _Text, byte _Rate, DateTime _ReviewDate)
{
Review ReviewToInsert = new Review()
{
MovieID = _TheMovie.MovieID,
MemberID = _TheMember.MemberID,
Movie = _TheMovie,
Member = _TheMember,
Rate = _Rate,
ReviewDate = _ReviewDate,
ReviewText = _Text
};
videoLib.Reviews.AddObject(ReviewToInsert);
videoLib.SaveChanges();
return true;
}
..
there are more data to insert to the Review class
Images: here
..
and the tables: (the "all columns" isn't a field in database tables)
Images: here
could you try like this
Review ReviewToInsert = videoLib.Reviews.CreateObject();
ReviewToInsert.MovieID = _TheMovie.MovieID
...
...
videoLib.Reviews.AddObject(ReviewToInsert);
videoLib.SaveChanges();
I got a solution, I need to define only the MovieID, MemberID, and not using their object
and use try & catch, to detect if thier the same MovieID (fk) and MemberID (fk) in the same row (because the review don't have is own id in the database)
public bool InsertNewReview(string _MovieID, int _MemberID, string _Text, byte _Rate, DateTime _ReviewDate)
{
try
{
Review ReviewToInsert = new Review()
{
Rate = _Rate,
ReviewDate = _ReviewDate,
ReviewText = _Text,
MovieID = _MovieID,
MemberID = _MemberID
};
videoLib.Reviews.AddObject(ReviewToInsert);
videoLib.SaveChanges();
return true;
}
catch
{
return false;
}
}