I'm trying to use sambas database as I need it for the web version of my app but I'm struggling with it.
I have an OpeningTimes form in my app that I save to db as a Map.
The logic I use in the saving method is: Load the form from db using the userName parameter of the OpeningTimes object I pass in , if none is found then save it, if instead a record is found then update it with the passed in object. I'm using flutter_bloc to manage it all so in OpeningTimesBloc when the form is saved to db, it loads it and yield it so to update UI to last saved sate. The problem is that when loading from db after updating, the loaded value is not the one passed in update method( eg monMorAct: to be updated from true to false):
flutter: updateOpeningTimes() : update opening time received {userName: zazza zenigata, monMorOp: 10:00, monMorCl: 19:00, monMorAct: false, monAftOp: , monAftCl: , monAftAct: false, tueMorOp: , tueMorCl: , tueMorAct: false, tueAftOp: , tueAftCl: , tueAftAct: false, wedMorOp: , wedMorCl: , wedMorAct: false, wedAftOp: , wedAftCl: , wedAftAct: false, thuMorOp: , thuMorCl: , thuMorAct: false, thuAftOp: , thuAftCl: , thuAftAct: false, friMorOp: , friMorCl: , friMorAct: false, friAftOp: , friAftCl: , friAftAct: false, satMorOp: , satMorCl: , satMorAct: false, satAftOp: , satAftCl: , satAftAct: false, sunMorOp: , sunMorCl: , sunMorAct: false, sunAftOp: , sunAftCl: , sunAftAct: false}
but when loading it:
flutter: loadOpeningTimes() snapshot is: Record(openingTimeStorage, 1) {userName: zazza zenigata, monMorOp: 10:00, monMorCl: 19:00, monMorAct: true, monAftOp: , monAftCl: , monAftAct: false, tueMorOp: , tueMorCl: , tueMorAct: false, tueAftOp: , tueAftCl: , tueAftAct: false, wedMorOp: , wedMorCl: , wedMorAct: false, wedAftOp: , wedAftCl: , wedAftAct: false, thuMorOp: , thuMorCl: , thuMorAct: false, thuAftOp: , thuAftCl: , thuAftAct: false, friMorOp: , friMorCl: , friMorAct: false, friAftOp: , friAftCl: , friAftAct: false, satMorOp: , satMorCl: , satMorAct: false, satAftOp: , satAftCl: , satAftAct: false, sunMorOp: , sunMorCl: , sunMorAct: false, sunAftOp: , sunAftCl: , sunAftAct: false}
Can you spot what I am doing wrong?
As always thank you very much for your time and help.
This is the singleton:
import 'dart:async';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_io.dart';
class AppDatabase {
// Singleton instance
static final AppDatabase _singleton = AppDatabase._();
// Singleton accessor
static AppDatabase get instance => _singleton;
// Completer is used for transforming synchronous code into asynchronous code.
Completer<Database> _dbOpenCompleter;
// A private constructor. Allows us to create instances of AppDatabase
// only from within the AppDatabase class itself.
AppDatabase._();
// Sembast database object
Database _database;
// Database object accessor
Future<Database> get database async {
// If completer is null, AppDatabaseClass is newly instantiated, so database is not yet opened
if (_dbOpenCompleter == null) {
_dbOpenCompleter = Completer();
// Calling _openDatabase will also complete the completer with database instance
_openDatabase();
}
// If the database is already opened, awaiting the future will happen instantly.
// Otherwise, awaiting the returned future will take some time - until complete() is called
// on the Completer in _openDatabase() below.
return _dbOpenCompleter.future;
}
Future _openDatabase() async {
// Get a platform-specific directory where persistent app data can be stored
final appDocumentDir = await getApplicationDocumentsDirectory();
// Path with the form: /platform-specific-directory/demo.db
final dbPath = join(appDocumentDir.path, 'demo.db');
final database = await databaseFactoryIo.openDatabase(dbPath);
// Any code awaiting the Completer's future will now start executing
_dbOpenCompleter.complete(database);
}
}
This is the bloc:
class OpeningTimesBloc extends Bloc<OpeningTimesEvent, OpeningTimesState> {
OpeningTimesRepository _openingTimesRepository = OpeningTimesRepository();
#override
OpeningTimesState get initialState => InitialState();
#override
Stream<OpeningTimesState> mapEventToState(OpeningTimesEvent event) async* {
if (event is LoadOpeningTimes) {
print('GetOpeningTimes event received');
yield* _mapLoadOpeningTimesToState(event);
}
if (event is SaveOpeningTimes) {
yield* _mapSaveOpeningTimesToState(event);
}
}
Stream<OpeningTimesState> _mapLoadOpeningTimesToState(
LoadOpeningTimes event) async* {
OpeningTimes openingTimes = await _openingTimesRepository.loadOpeningTimes(
userName: event.user.name);
print(
'_mapLoadOpeningTimesToState() loaded openingTimes is: ${openingTimes.toMap().toString()}');
yield ShopOpeningTimes(openingTimes);
}
Stream<OpeningTimesState> _mapSaveOpeningTimesToState(
SaveOpeningTimes event) async* {
await _openingTimesRepository.saveOpeningTimes(event.openingTimes);
add(LoadOpeningTimes(event.user));
}
}
this is the repository:
import 'package:sembast/sembast.dart';
class OpeningTimesRepository {
// name for the storage
static const String openingTimeStorage = 'openingTimeStorage';
// storage reference
final _openingTimesFolder = intMapStoreFactory.store(openingTimeStorage);
// database instance
Future<Database> get _db async => await AppDatabase.instance.database;
Future saveOpeningTimes(OpeningTimes openingTimes) async {
print(
'saveOpeningTimes(): save opening times received ${openingTimes.toMap().toString()}'); // prints correct
final snapshot = await loadOpeningTimes(userName: openingTimes.userName);
print('saveOpeningTimes() snapshot is $snapshot');
if (snapshot == null) {
print('opening times are to save');
await _openingTimesFolder.add(await _db, openingTimes.toMap());
} else {
print('opening times are to update');
await updateOpeningTimes(openingTimes);
}
}
Future updateOpeningTimes(OpeningTimes openingTimes) async {
print(
'updateOpeningTimes() : update opening time received ${openingTimes.toMap().toString()}'); // correct
final Finder finder = Finder(filter: Filter.byKey(openingTimes.userName));
await _openingTimesFolder.update(await _db, openingTimes.toMap(),
finder: finder);
}
Future<OpeningTimes> loadOpeningTimes({String userName}) async {
print(
'loadOpeningTimes() called userName is $userName'); // prints correct userName
final finder = Finder(sortOrders: [SortOrder('userName')]);
final snapshot =
await _openingTimesFolder.findFirst(await _db, finder: finder);
print('loadOpeningTimes() snapshot is: ${snapshot}'); // correct
return OpeningTimes.fromMap(snapshot.value);
}
}
and this is the model:
import 'package:flutter/material.dart';
class OpeningTimes {
int id;
final String userName;
final String monMorOp;
final String monMorCl;
final bool monMorAct;
final String monAftOp;
final String monAftCl;
final bool monAftAct;
final String tueMorOp;
final String tueMorCl;
final bool tueMorAct;
final String tueAftOp;
final String tueAftCl;
final bool tueAftAct;
final String wedMorOp;
final String wedMorCl;
final bool wedMorAct;
final String wedAftOp;
final String wedAftCl;
final bool wedAftAct;
final String thuMorOp;
final String thuMorCl;
final bool thuMorAct;
final String thuAftOp;
final String thuAftCl;
final bool thuAftAct;
final String friMorOp;
final String friMorCl;
final bool friMorAct;
final String friAftOp;
final String friAftCl;
final bool friAftAct;
final String satMorOp;
final String satMorCl;
final bool satMorAct;
final String satAftOp;
final String satAftCl;
final bool satAftAct;
final String sunMorOp;
final String sunMorCl;
final bool sunMorAct;
final String sunAftOp;
final String sunAftCl;
final bool sunAftAct;
OpeningTimes(
{#required this.userName,
#required this.monMorOp,
#required this.monMorCl,
#required this.monMorAct,
#required this.monAftOp,
#required this.monAftCl,
#required this.monAftAct,
#required this.tueMorOp,
#required this.tueMorCl,
#required this.tueMorAct,
#required this.tueAftOp,
#required this.tueAftCl,
#required this.tueAftAct,
#required this.wedMorOp,
#required this.wedMorCl,
#required this.wedMorAct,
#required this.wedAftOp,
#required this.wedAftCl,
#required this.wedAftAct,
#required this.thuMorOp,
#required this.thuMorCl,
#required this.thuMorAct,
#required this.thuAftOp,
#required this.thuAftCl,
#required this.thuAftAct,
#required this.friMorOp,
#required this.friMorCl,
#required this.friMorAct,
#required this.friAftOp,
#required this.friAftCl,
#required this.friAftAct,
#required this.satMorOp,
#required this.satMorCl,
#required this.satMorAct,
#required this.satAftOp,
#required this.satAftCl,
#required this.satAftAct,
#required this.sunMorOp,
#required this.sunMorCl,
#required this.sunMorAct,
#required this.sunAftOp,
#required this.sunAftCl,
#required this.sunAftAct});
#override
List<Object> get props => [
userName,
monMorOp,
monMorCl,
monMorAct,
monAftOp,
monAftCl,
monAftAct,
tueMorOp,
tueMorCl,
tueMorAct,
tueAftOp,
tueAftCl,
tueAftAct,
wedMorOp,
wedMorCl,
wedMorAct,
wedAftOp,
wedAftCl,
wedAftAct,
thuMorOp,
thuMorCl,
thuMorAct,
thuAftOp,
thuAftCl,
thuAftAct,
friMorOp,
friMorCl,
friMorAct,
friAftOp,
friAftCl,
friAftAct,
satMorOp,
satMorCl,
satMorAct,
satAftOp,
satAftCl,
satAftAct,
sunMorOp,
sunMorCl,
sunMorAct,
sunAftOp,
sunAftCl,
sunAftAct
];
factory OpeningTimes.fromMap(Map<String, dynamic> map) => new OpeningTimes(
userName: map['userName'],
monMorOp: map['monMorOp'],
monMorCl: map['monMorCl'],
monMorAct: map['monMorAct'],
monAftOp: map['monAftOp'],
monAftCl: map['monAftCl'],
monAftAct: map['monAftAct'],
tueMorOp: map['tueMorOp'],
tueMorCl: map['tueMorCl'],
tueMorAct: map['tueMorAct'],
tueAftOp: map['tueAftOp'],
tueAftCl: map['tueAftCl'],
tueAftAct: map['tueAftAct'],
wedMorOp: map['wedMorOp'],
wedMorCl: map['wedMorCl'],
wedMorAct: map['wedMorAct'],
wedAftOp: map['wedAftOp'],
wedAftCl: map['wedAftCl'],
wedAftAct: map['wedAftAct'],
thuMorOp: map['thuMorOp'],
thuMorCl: map['thuMorCl'],
thuMorAct: map['thuMorAct'],
thuAftOp: map['thuAftOp'],
thuAftCl: map['thuAftCl'],
thuAftAct: map['thuAftAct'],
friMorOp: map['friMorOp'],
friMorCl: map['friMorCl'],
friMorAct: map['friMorAct'],
friAftOp: map['friAftOp'],
friAftCl: map['friAftCl'],
friAftAct: map['friAftAct'],
satMorOp: map['satMorOp'],
satMorCl: map['satMorCl'],
satMorAct: map['satMorAct'],
satAftOp: map['satAftOp'],
satAftCl: map['satAftCl'],
satAftAct: map['satAftAct'],
sunMorOp: map['sunMorOp'],
sunMorCl: map['sunMorCl'],
sunMorAct: map['sunMorAct'],
sunAftOp: map['sunAftOp'],
sunAftCl: map['sunAftCl'],
sunAftAct: map['sunAftAct'],
);
Map<String, dynamic> toMap() => {
'userName': userName,
'monMorOp': monMorOp,
'monMorCl': monMorCl,
'monMorAct': monMorAct,
'monAftOp': monAftOp,
'monAftCl': monAftCl,
'monAftAct': monAftAct,
'tueMorOp': tueMorOp,
'tueMorCl': tueMorCl,
'tueMorAct': tueMorAct,
'tueAftOp': tueAftOp,
'tueAftCl': tueAftCl,
'tueAftAct': tueAftAct,
'wedMorOp': wedMorOp,
'wedMorCl': wedMorCl,
'wedMorAct': tueMorAct,
'wedAftOp': wedAftOp,
'wedAftCl': wedAftCl,
'wedAftAct': wedAftAct,
'thuMorOp': thuMorOp,
'thuMorCl': thuMorCl,
'thuMorAct': thuMorAct,
'thuAftOp': thuAftOp,
'thuAftCl': thuAftCl,
'thuAftAct': thuAftAct,
'friMorOp': friMorOp,
'friMorCl': friMorCl,
'friMorAct': friMorAct,
'friAftOp': friAftOp,
'friAftCl': friAftCl,
'friAftAct': friAftAct,
'satMorOp': satMorOp,
'satMorCl': satMorCl,
'satMorAct': satMorAct,
'satAftOp': satAftOp,
'satAftCl': satAftCl,
'satAftAct': satAftAct,
'sunMorOp': sunMorOp,
'sunMorCl': sunMorCl,
'sunMorAct': sunMorAct,
'sunAftOp': sunAftOp,
'sunAftCl': sunAftCl,
'sunAftAct': sunAftAct
};
}
Updated repository methods to use autogenerated key:
static const String openingTimeStorage = 'openingTimeStorage';
// storage reference
final _openingTimesFolder = intMapStoreFactory.store(openingTimeStorage);
// database instance
Future<Database> get _db async => await AppDatabase.instance.database;
Future saveOpeningTimes(OpeningTimes openingTimes) async {
print(
'saveOpeningTimes(): save opening times received ${openingTimes.toMap().toString()}'); // prints correct
final snapshot = await loadOpeningTimes();
print('saveOpeningTimes() snapshot is $snapshot');
if (snapshot == null) {
print('opening times are to save');
await _openingTimesFolder.add(await _db, openingTimes.toMap());
return;
} else {
print('opening times are to update');
await updateOpeningTimes(openingTimes);
}
}
Future updateOpeningTimes(OpeningTimes openingTimes) async {
print(
'updateOpeningTimes() : update opening time received ${openingTimes.toMap().toString()}'); // correct
final Finder finder = Finder(
filter: Filter.byKey(
_openingTimesFolder.record(openingTimes.id).get(await _db)));
await _openingTimesFolder.update(await _db, openingTimes.toMap(),
finder: finder);
}
Future<OpeningTimes> loadOpeningTimes() async {
final finder = Finder(sortOrders: [SortOrder('userName')]);
final snapshot =
await _openingTimesFolder.findFirst(await _db, finder: finder);
if (snapshot != null) {
print('loadOpeningTimes() snapshot is: $snapshot'); // correct
OpeningTimes openingTimes = OpeningTimes.fromMap(snapshot.value);
openingTimes.id = snapshot.key;
return openingTimes;
} else {
return null;
}
}
on updating I get flutter: Erros is Invalid argument(s): Record key cannot be null.
In updateOpeningTimes your filter looks incorrect.
final Finder finder = Finder(filter: Filter.byKey(openingTimes.userName));
Here you are looking for objects with the userName as the key but the key is an int (autogenerated) here so it cannot match. You should have something like:
final Finder finder = Finder(filter: Filter.equals('userName', userName));
In loadOpeningTimes, you just take the first record sorted by userName. If you look for an explicit userName, you should use the same finder than for update.
As a side note, it is a lot faster to load records by key: store.record(key).get(db)
I am having issues with a Laravel application using an existing database where MS SQL UUIDs are used. My application has a customer:
class Customer extends Model
{
protected $table = 'ERP.Customer';
public $timestamps = false;
protected $primaryKey = 'CustID';
protected $keyType = 'string';
protected $fillable = [
'CustID',
'SysRowID',
'CustNum',
'LegalName',
'ValidPayer',
'TerritoryID',
'Address1',
'Address2',
'Address3',
'City',
'State',
'Zip',
'Country',
'SalesRepCode',
'CurrencyCode',
'TermsCode',
'CreditHold',
'FaxNum',
'PhoneNum',
'CustomerType'
];
public function SalesTer()
{
return $this->belongsTo(SalesTer::class,'TerritoryID', 'TerritoryID');
}
public function Shipments()
{
return $this->hasMany(Shipment::class, 'CustNum', 'CustNum');
}
public function Equipments()
{
return $this->hasMany(Equipment::class,'CustNum', 'CustNum');
}
public function Customer_UD()
{
return $this->hasOne(Customer_UD::class,'ForeignSysRowID', 'SysRowID');
}
}
Which (in the native ERP application) has a UD table which end users can used to customise the Customer entity:
class Customer_UD extends Model
{
protected $table = 'ERP.Customer_UD';
protected $primaryKey = 'ForeignSysRowID';
public $timestamps = false;
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = [
'ForeignSysRowID',
'MakesCans_c',
'MakesEnds_c',
'Industry_c'
];
public function Customer()
{
return $this->hasOne(Customer::class,'SysRowID', 'ForeignSysRowID');
}
}
CustomerController:
public function show($CustID)
{
if(Customer::find($CustID))
{
$Customer = Customer::find($CustID);
$Customer_UD = $Customer->Customer_UD()
->get();
$Shipments = $Customer->Shipments()
->where('Voided', '0')
->get();
$Equipments = $Customer->Equipments()
->with('Part') // load the Part too in a single query
->where('SNStatus', 'SHIPPED')
->get();
return view('Customer.show', ['NoCust' => '0'],
compact('Equipments', 'Customer','Shipments', 'Parts', 'Customer_UD'));
}
else
{
return view('Customer.show', ['NoCust' => '1']);
}
}
The Customer has (for whatever reason) a CustID (which people use to refer to the customer) a CustNum (which is not used outside of the database and a SysRowID. The SysRowID is used to link the Customer table with the Customer_UD table.
An example row from Customer_UD is:
My issue is that when trying to return the UD fields along with the Customer fields I get an error:
SQLSTATE[HY000]: General error: 20018 Incorrect syntax near ''.
[20018] (severity 15) [select * from [ERP].[Customer_UD] where [ERP].
[Customer_UD].[ForeignSysRowID] = '���_�X�O�Q3�^w' and [ERP].
[Customer_UD].[ForeignSysRowID] is not null]
I thought it was odd, so I commended out the Customer_UD lines in the CustomerController and simply tried to display the Customer UUID field in the show blade:
SysRowID: {{$Customer->SysRowID}}
I get nothing, no errors but no data. I created a controller and index blade for the Customer_UD model and can display all of the Customer_UD database fields apart from the UUID field.
I don't actually want to display the UUID fields - but do need to use them to build the relationships. Can anyone help point me in the right direction?
I found that adding:
'options' => [
PDO::DBLIB_ATTR_STRINGIFY_UNIQUEIDENTIFIER => true,
],
To the database configuration in config\database.php resolved the issue.