I have a database with a few dozen tables. My app deals with just one of those tables. I'd like propel to generate the schema just for that table. Is there a way I can specify the table(s) in build.properties?
A simple extension of Propel (code examples based on Propel-1.6.8) to allow ignore a custom set of tables (configured using a property):
Extend build-propel.xml:
<!-- Ignore Tables Feature -->
<taskdef
name="propel-schema-reverse-ignore"
classname="task.PropelSchemaReverseIgnoreTask" classpathRef="propelclasses"/>
Extend build.xml:
<target name="reverse-ignore" depends="configure">
<phing phingfile="build-propel.xml" target="reverse-ignore"/>
</target>
Extend PropelSchemaReverseTask:
class PropelSchemaReverseIgnoreTask extends PropelSchemaReverseTask {
/**
* Builds the model classes from the database schema.
* #return Database The built-out Database (with all tables, etc.)
*/
protected function buildModel()
{
// ...
// Loads Classname reverse.customParserClass if present
$customParserClassname = $config->getBuildProperty("reverseCustomParserClass");
if ($customParserClassname!=null) {
$this->log('Using custom parser class: '.$customParserClassname);
$parser = $config->getConfiguredSchemaParserForClassname($con, $customParserClassname);
} else {
$parser = $config->getConfiguredSchemaParser($con);
}
// ...
}
}
Add function to GeneratorConfig:
class GeneratorConfig implements GeneratorConfigInterface {
// ...
/**
* Ignore Tables Feature:
* Load a specific SchemaParser class
*
* #param PDO $con
* #param string $clazzName SchemaParser class to load
* #throws BuildException
* #return Ambigous <SchemaParser, unknown>
*/
public function getConfiguredSchemaParserForClassname(PDO $con = null, $clazzName)
{
$parser = new $clazzName();
if (!$parser instanceof SchemaParser) {
throw new BuildException("Specified platform class ($clazz) does implement SchemaParser interface.", $this->getLocation());
}
$parser->setConnection($con);
$parser->setMigrationTable($this->getBuildProperty('migrationTable'));
$parser->setGeneratorConfig($this);
return $parser;
}
}
Extend MysqlSchemaParser:
class MysqlSchemaIgnoreParser extends MysqlSchemaParser {
public function parse(Database $database, Task $task = null)
{
$this->addVendorInfo = $this->getGeneratorConfig()->getBuildProperty('addVendorInfo');
$stmt = $this->dbh->query("SHOW FULL TABLES");
// First load the tables (important that this happen before filling out details of tables)
$tables = array();
$configIgnoreTables = $this->getGeneratorConfig()->getBuildProperty("reverseIgnoreTables");
$tables_ignore_list = array();
$tables_ignore_list = explode(",", $configIgnoreTables);
if ($task) {
$task->log("Reverse Engineering Tables", Project::MSG_VERBOSE);
}
while ($row = $stmt->fetch(PDO::FETCH_NUM)) {
$name = $row[0];
$type = $row[1];
if (!in_array($name, $tables_ignore_list)) {
// ...
} else {
$task->log("Ignoring table: " .$name);
}
}
// ...
}
}
Add new (optional) properties to build.properties:
# Propel Reverse Custom Properties
propel.reverse.customParserClass=MysqlSchemaIgnoreParser
propel.reverse.ignoreTables = table1,table2
A solution could be to write your own schema parser that ignores certain tables. These tables to exclude could be read from a config file or so.
Take a look at the MysqlSchemaParser.php to get an idea of how such a parser works. You could extend such an existing parser and just override the parse() method. When tables are added, you would check them for exclusion/inclusion and only add them if they meet your subset criteria.
How to create custom propel tasks is described in the Propel cookbook.
Instead of invoking propel-gen reverse you would then invoke your customized reverse engineering task.
If done neatly, I'd find it worth being added as a contribution to the Propel project and you'd definitely deserve fame for that! :-)
Related
am getting this error when seeding data into my database. apparently the I have 2 models bookings and bookingstatus model.the bookingstatus seeder is working well and is seeding the status very well but when it comes to the bookings the data is unable to seed.this is the error that pops up
ErrorException
Undefined variable:
at F:\Main Server\htdocs\voskillproject\database\seeders\BookingsSeeder.php:73
69▕ 'event_details'=>'we would like to book you for a wedding'
70▕
71▕ ]);
72▕ $Approved->bookingstatus()->attach($Approvedstatus);
➜ 73▕ $Cancelled->bookingstatus()->attach($$Cancelledstatus);
74▕ $DepositPaid->bookingstatus()->attach($DepositPaidstatus);
75▕ $Published->bookingstatus()->attach($Publishedstatus);
76▕ }
77▕ }
1 F:\Main Server\htdocs\voskillproject\database\seeders\BookingsSeeder.php:73
Illuminate\Foundation\Bootstrap\HandleExceptions::handleError("Undefined variable: ", "F:\Main Server\htdocs\voskillproject\database\seeders\BookingsSeeder.php")
2 F:\Main Server\htdocs\voskillproject\vendor\laravel\framework\src\Illuminate\Container\BoundMethod.php:36
Database\Seeders\BookingsSeeder::run()
this is my BookingsFactory
<?php
namespace Database\Factories;
use App\Models\Bookings;
use Illuminate\Database\Eloquent\Factories\Factory;
class BookingsFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* #var string
*/
protected $model = Bookings::class;
/**
* Define the model's default state.
*
* #return array
*/
public function definition()
{
return [
'is_booking'=>0,
'full_name'=>$this->faker->unique()->name,
'location'=>$this->faker->location,
'phone'=>$this->faker->number,
'is_booking'=>3,
'email'=>$this->faker->safeEmail,
'date'=>$this->faker->date,
'event_id'=>$this->faker->event_id,
'event_details'=>$this->faker->paragraph,
];
}
}
this is my database seeder
<?php
namespace Database\Seeders;
use App\Models\Role;
use App\Models\Bookingstatus;
use Illuminate\Database\Seeder;
use Database\Seeders\RoleSeeder;
use Database\Seeders\Userseeder;
use Database\Seeders\BookingsSeeder;
use Database\Seeders\BookingstatusSeeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* #return void
*/
public function run()
{
$this->call([
BookingsSeeder::class,
BookingstatusSeeder::class,
]);
\App\Models\Bookingstatus::factory()->hasbookings(10)->create();
}
}
i have not understood where ie gone wrong in the code
i figure out where the issue was
1.first on the bookings factory i have to change the faker details to this
return [
'is_booking'=>0,
'full_name'=>$this->faker->unique()->name,
'location'=>$this->faker->address,
'phone'=>$this->faker->numerify('###-###-####'),
'is_booking'=>3,
'email'=>$this->faker->safeEmail,
'date'=>$this->faker->unique()->dateTime()->format('Y/m/d'),
'event_id'=>$this->faker->randomDigit(),
'event_details'=>$this->faker->paragraphs(2, true),
];
also i should have imported this code at the top of the bookingstatus factory
use Illuminate\Support\Str;
The concept:
I have a task which imports some data on the database. The database schema can be changed at any time since it is exported to a folder. So i have to read the database file and create the structure. I would like to do it with TYPO3 API. Before TYPO3 9 i would do something like that:
$sqlQueries = explode(';', file_get_contents(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'myFile.sql'));
foreach ($sqlQueries as $sqlQuery) {
$sqlQuery = trim($sqlQuery);
if (!empty($sqlQuery) && $this->db instanceof DatabaseConnection && method_exists($this->db, 'sql_query')) {
$this->db->sql_query($sqlQuery);
}
}
How do i do that with TYPO3 10?
Best regards
TYPO3 10 has already such functionality and you can find it in the maintenance module unter the
Analyze Database Structure. Thanks to Mathias Brodala i found my solution. (It can always be improved).
In your task you call the SqlReader class in order to extract the statements from the SQL file. The use namespace is the following (As of TYPO3 10)
use TYPO3\CMS\Core\Database\Schema\SqlReader;
Now we have two problems with that
The SQL evaluated and executed by TYPO3 is limited to schemas. For content changes you need a 3rd party library/tool.
It only creates tables and doesn't drop them by default.
The first "problem" it says that you can only perform database structure changes but not the content in it. For that, one could use the QueryBuilder.
The second problem is that the getCreateTableStatementArray function in the SqlReader class does the following:
return $this->getStatementArray($dumpContent, '^CREATE TABLE');
This calls the getStatementArray function and it only returns the CREATE TABLE commands. So if you have a DROP TABLE command in the SQL file, it won't be taken into consideration. For that, we need to create our own function for the getCreateTableStatementArray. So in your Task, create this function:
/**
* #param string $dumpContent
* #return array
*/
public function getCreateTableStatementArray(string $dumpContent): array
{
$sqlReader = GeneralUtility::makeInstance(SqlReader::class);
return $sqlReader->getStatementArray($dumpContent);
}
With this, TYPO3 will not evaluate the statements by command name and it will get all the commands available in the sql file.
Now that we have done that, we need to pass the statements into our custom function to be parsed. So somewhere in your code you get file's content. I have it like this:
/**
* #return bool
* #throws \Exception
*/
public function execute()
{
...
$this->createTableStructure();
...
}
protected function createTableStructure()
{
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$sqlStatements = $this->getCreateTableStatementArray(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'myFile.sql'));
foreach ($sqlStatements as $sqlStatement) {
if (strpos($sqlStatement, ' CHARSET=utf8') !== false) {
$sqlStatement = str_replace(" DEFAULT CHARSET=utf8", "", $sqlStatement);
}
$connection = $connectionPool->getConnectionByName('Default');
try {
$connection->executeUpdate($sqlStatement);
} catch (DBALException $e) {
//Your log here
}
}
}
For this part i got an error that TYPO3 could not read the DEFAULT attribute so i had to remove it.
if (strpos($sqlStatement, ' CHARSET=utf8') !== false) {
$sqlStatement = str_replace(" DEFAULT CHARSET=utf8", "", $sqlStatement);
}
This looks like this in the SQL file:
DROP TABLE IF EXISTS `myTable`;
CREATE TABLE IF NOT EXISTS `myTable` (
`myColumn1` int(10) NOT NULL DEFAULT '0',
`myColumn2` varchar(255) DEFAULT NULL,
`myColumn3` varchar(255) DEFAULT NULL,
`myColumn4` date DEFAULT NULL,
`myColumn5` varchar(255) DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Hope it was helpful!
Best regards
I have to log the changes of each entity. I've Listener which listens for doctrine's events on preRemove, postUpdate and postDelete.
My entity AccessModule has relations:
App\Entity\AccessModule.php
/**
* #ORM\OneToMany(targetEntity="App\Entity\AccessModule", mappedBy="parent")
* #ORM\OrderBy({"id" = "ASC"})
*/
private $children;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\AccessModule", inversedBy="children")
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id", nullable=true)
*/
private $parent;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\AccessModuleRoute", inversedBy="access_modules")
* #ORM\JoinTable(name="access_routes",
* joinColumns={#ORM\JoinColumn(name="access_module_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="route_id", referencedColumnName="id")})
*
*/
private $routes;
in listener:
App\EventListener\EntityListener.php
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
$encoders = [new XmlEncoder(), new JsonEncoder()];
$normalizer = new ObjectNormalizer();
$normalizer->setCircularReferenceHandler(function ($object) {
return $object->getId();
});
$this->serializer = new Serializer([$normalizer], $encoders);
public function createLog(LifecycleEventArgs $args, $action){
$em = $args->getEntityManager();
$entity = $args->getEntity();
if ($this->tokenStorage->getToken()->getUser()) {
$username = $this->tokenStorage->getToken()->getUser()->getUsername();
} else {
$username = 'anon'; // TODO Remove anon. set null value
}
$log = new Log();
// $log->setData('dddd')
$log->setData($this->serializer->serialize($entity, ''json)
->setAction($action)
->setActionTime(new \DateTime())
->setUser($username)
->setEntityClass(get_class($entity));
$em->persist($log);
$em->flush();
}
I've problem with serialization
When I use $log->setData($entity) I get problem with Circular.
Whan I do serialization $log->setData($this->serializer->serialize($entity, ''json) I get full of relations, with parent's children, with children children. In a result I get full tree :/
I'd like to get
Expect
[
'id' => ID,
'name' => NAME,
'parent' => parent_id // ManyToOne, I'd like get its id
'children' => [$child_id, $child_id, $child_id] // array of $id of children array collection
]
(ofcourse this is draft before encode it to json)
How can I get expected data without full relations?
Thing you are looking for is called serialization groups: here and here.
Now let me explain how it works. It's quite simple. Say you have Post Entity:
class Post
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"default"})
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User\User")
* #Groups({"default"})
*/
private $author;
}
And you have also User Entity:
class User
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"default"})
*/
private $id;
/**
* #ORM\Column(type="string", length=40)
* #Groups({"default"})
*/
private $firstName;
/**
* #ORM\Column(type="string", length=40)
*/
private $lastName;
}
Post can have author(User) but I don't want to return all User data every single time. I'm interested only in id and first name.
Take a closer look at #Groups annotation. You can specify so called serialization groups. It's nothing more than convinient way of telling Symfony which data you would like to have in your result set.
You have to tell Symfony serializer which relationships you would like to keep by adding relevant groups in form of annotation above property/getter. You also have to specify which properties or getters of your relationships you would like to keep.
Now how to let Symfony know about that stuff?
When you prepare/configure your serializaition service you just simply have to provide defined groups like that:
return $this->serializer->serialize($data, 'json', ['groups' => ['default']]);
It's good to build some kind of wrapper service around native symfony serializer so you can simplify the whole process and make it more reusable.
Also make sure that serializer is correctly configured - otherwise it will not take these group into account.
That is also one way(among other ways) of "handling" circular references.
Now you just need to work on how you will format your result set.
ignored_attributes provides a quick, and easy way to accomplish what you're looking for.
$serializer->serialize($object, 'json', ['ignored_attributes' => ['ignored_property']]);
Heres how its used with the serializer:
use Acme\Person;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
$person = new Person();
$person->setName('foo');
$person->setAge(99);
$normalizer = new ObjectNormalizer();
$encoder = new JsonEncoder();
$serializer = new Serializer([$normalizer], [$encoder]);
$serializer->serialize($person, 'json', ['ignored_attributes' => ['age']]);
Documentation: https://symfony.com/doc/current/components/serializer.html#ignoring-attributes
Tested in Symfony 4.1, here is the documentation that actually works https://symfony.com/blog/new-in-symfony-2-7-serialization-groups
Robert's explanation https://stackoverflow.com/a/48756847/579646 is missing the $classMetadataFactory in order to work. Here is my code:
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$encoders = [new JsonEncoder()];
$normalizer = new ObjectNormalizer($classMetadataFactory);
$normalizer->setCircularReferenceLimit(2);
// Add Circular reference handler
$normalizer->setCircularReferenceHandler(function ($object) {
return $object->getId();
});
$normalizers = [$normalizer];
$serializer = new Serializer($normalizers, $encoders);
$jsonContent = $serializer->serialize($jobs, 'json', array('groups' => ['default']));
return JsonResponse::fromJsonString($jsonContent);
We’re trying to decide between providing generic vs specific record formats for consumption by our clients
with an eye to providing an online schema registry clients can access when the schemas are updated.
We expect to send out serialized blobs prefixed with a few bytes denoting the version number so schema
retrieval from our registry can be automated.
Now, we’ve come across code examples illustrating the relative adaptability of the generic format for
schema changes but we’re reluctant to give up the type safety and ease-of-use provided by the specific
format.
Is there a way to obtain the best of both worlds? I.e. could we work with and manipulate the specific generated
classes internally and then have them converted them to generic records automatically just before serialization?
Clients would then deserialize the generic records (after looking up the schema).
Also, could clients convert these generic records they received to specific ones at a later time? Some small code examples would be helpful!
Or are we looking at this all the wrong way?
What you are looking for is Confluent Schema registry service and libs which helps to integrate with this.
Providing a sample to write Serialize De-serialize avro data with a evolving schema. Please note providing sample from Kafka.
import io.confluent.kafka.serializers.KafkaAvroDeserializer;
import io.confluent.kafka.serializers.KafkaAvroSerializer;
import org.apache.avro.generic.GenericRecord;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import java.util.HashMap; import java.util.Map;
public class ConfluentSchemaService {
public static final String TOPIC = "DUMMYTOPIC";
private KafkaAvroSerializer avroSerializer;
private KafkaAvroDeserializer avroDeserializer;
public ConfluentSchemaService(String conFluentSchemaRigistryURL) {
//PropertiesMap
Map<String, String> propMap = new HashMap<>();
propMap.put("schema.registry.url", conFluentSchemaRigistryURL);
// Output afterDeserialize should be a specific Record and not Generic Record
propMap.put("specific.avro.reader", "true");
avroSerializer = new KafkaAvroSerializer();
avroSerializer.configure(propMap, true);
avroDeserializer = new KafkaAvroDeserializer();
avroDeserializer.configure(propMap, true);
}
public String hexBytesToString(byte[] inputBytes) {
return Hex.encodeHexString(inputBytes);
}
public byte[] hexStringToBytes(String hexEncodedString) throws DecoderException {
return Hex.decodeHex(hexEncodedString.toCharArray());
}
public byte[] serializeAvroPOJOToBytes(GenericRecord avroRecord) {
return avroSerializer.serialize(TOPIC, avroRecord);
}
public Object deserializeBytesToAvroPOJO(byte[] avroBytearray) {
return avroDeserializer.deserialize(TOPIC, avroBytearray);
} }
Following classes have all the code you are looking for.
io.confluent.kafka.serializers.KafkaAvroDeserializer;
io.confluent.kafka.serializers.KafkaAvroSerializer;
Please follow the link for more details :
http://bytepadding.com/big-data/spark/avro/avro-serialization-de-serialization-using-confluent-schema-registry/
Can I convert between them?
I wrote the following kotlin code to convert from a SpecificRecord to GenericRecord and back - via JSON.
PositionReport is an object generated off of avro with the avro plugin for gradle - it is:
#org.apache.avro.specific.AvroGenerated
public class PositionReport extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord {
...
The functions used are below
/**
* Encodes a record in AVRO Compatible JSON, meaning union types
* are wrapped. For prettier JSON just use the Object Mapper
* #param pos PositionReport
* #return String
*/
private fun PositionReport.toAvroJson() : String {
val writer = SpecificDatumWriter(PositionReport::class.java)
val baos = ByteArrayOutputStream()
val jsonEncoder = EncoderFactory.get().jsonEncoder(this.schema, baos)
writer.write(this, jsonEncoder)
jsonEncoder.flush()
return baos.toString("UTF-8")
}
/**
* Converts from Genreic Record into JSON - Seems smarter, however,
* to unify this function and the one above but whatevs
* #param record GenericRecord
* #param schema Schema
*/
private fun GenericRecord.toAvroJson(): String {
val writer = GenericDatumWriter<Any>(this.schema)
val baos = ByteArrayOutputStream()
val jsonEncoder = EncoderFactory.get().jsonEncoder(this.schema, baos)
writer.write(this, jsonEncoder)
jsonEncoder.flush()
return baos.toString("UTF-8")
}
/**
* Takes a Generic Record of a position report and hopefully turns
* it into a position report... maybe it will work
* #param gen GenericRecord
* #return PositionReport
*/
private fun toPosition(gen: GenericRecord) : PositionReport {
if (gen.schema != PositionReport.getClassSchema()) {
throw Exception("Cannot convert GenericRecord to PositionReport as the Schemas do not match")
}
// We will convert into JSON - and use that to then convert back to the SpecificRecord
// Probalby there is a better way
val json = gen.toAvroJson()
val reader: DatumReader<PositionReport> = SpecificDatumReader(PositionReport::class.java)
val decoder: Decoder = DecoderFactory.get().jsonDecoder(PositionReport.getClassSchema(), json)
val pos = reader.read(null, decoder)
return pos
}
/**
* Converts a Specific Record to a Generic Record (I think)
* #param pos PositionReport
* #return GenericData.Record
*/
private fun toGenericRecord(pos: PositionReport): GenericData.Record {
val json = pos.toAvroJson()
val reader : DatumReader<GenericData.Record> = GenericDatumReader(pos.schema)
val decoder: Decoder = DecoderFactory.get().jsonDecoder(pos.schema, json)
val datum = reader.read(null, decoder)
return datum
}
There are a couple difference however between the two:
Fields in the SpecificRecord that are of Instant type will be encoded in the GenericRecord as long and Enums are slightly different
So for example in my unit test of this function time fields are tested like this:
val gen = toGenericRecord(basePosition)
assertEquals(basePosition.getIgtd().toEpochMilli(), gen.get("igtd"))
And enums are validated by string
val gen = toGenericRecord(basePosition)
assertEquals(basePosition.getSource().toString(), gen.get("source").toString())
So to convert between you can do:
val gen = toGenericRecord(basePosition)
val newPos = toPosition(gen)
assertEquals(newPos, basePosition)
I am working on a backend edit page for my custom entity. I have almost everything working, including saving a bunch of different text fields. I have a problem, though, when trying to set the value of a boolean field.
I have tried:
$landingPage->setEnabled(1);
$landingPage->setEnabled(TRUE);
$landingPage->setEnabled(0);
$landingPage->setEnabled(FALSE);
None seem to persist a change to my database.
How are you supposed to set a boolean field using magento ORM?
edit
Looking at my database, mysql is storing the field as a tinyint(1), so magento may be seeing this as an int not a bool. Still can't get it to set though.
This topic has bring curiosity to me. Although it has been answered, I'd like to share what I've found though I didn't do intense tracing.
It doesn't matter whether the cache is enabled / disabled, the table schema will be cached.
It will be cached during save process.
Mage_Core_Model_Abstract -> save()
Mage_Core_Model_Resource_Db_Abstract -> save(Mage_Core_Model_Abstract $object)
Mage_Core_Model_Resource_Db_Abstract
public function save(Mage_Core_Model_Abstract $object)
{
...
//any conditional will eventually call for:
$this->_prepareDataForSave($object);
...
}
protected function _prepareDataForSave(Mage_Core_Model_Abstract $object)
{
return $this->_prepareDataForTable($object, $this->getMainTable());
}
Mage_Core_Model_Resource_Abstract
protected function _prepareDataForTable(Varien_Object $object, $table)
{
$data = array();
$fields = $this->_getWriteAdapter()->describeTable($table);
foreach (array_keys($fields) as $field) {
if ($object->hasData($field)) {
$fieldValue = $object->getData($field);
if ($fieldValue instanceof Zend_Db_Expr) {
$data[$field] = $fieldValue;
} else {
if (null !== $fieldValue) {
$fieldValue = $this->_prepareTableValueForSave($fieldValue, $fields[$field]['DATA_TYPE']);
$data[$field] = $this->_getWriteAdapter()->prepareColumnValue($fields[$field], $fieldValue);
} else if (!empty($fields[$field]['NULLABLE'])) {
$data[$field] = null;
}
}
}
}
return $data;
}
See the line: $fields = $this->_getWriteAdapter()->describeTable($table);
Varien_Db_Adapter_Pdo_Mysql
public function describeTable($tableName, $schemaName = null)
{
$cacheKey = $this->_getTableName($tableName, $schemaName);
$ddl = $this->loadDdlCache($cacheKey, self::DDL_DESCRIBE);
if ($ddl === false) {
$ddl = parent::describeTable($tableName, $schemaName);
/**
* Remove bug in some MySQL versions, when int-column without default value is described as:
* having default empty string value
*/
$affected = array('tinyint', 'smallint', 'mediumint', 'int', 'bigint');
foreach ($ddl as $key => $columnData) {
if (($columnData['DEFAULT'] === '') && (array_search($columnData['DATA_TYPE'], $affected) !== FALSE)) {
$ddl[$key]['DEFAULT'] = null;
}
}
$this->saveDdlCache($cacheKey, self::DDL_DESCRIBE, $ddl);
}
return $ddl;
}
As we can see:
$ddl = $this->loadDdlCache($cacheKey, self::DDL_DESCRIBE);
will try to load the schema from cache.
If the value is not exists: if ($ddl === false)
it will create one: $this->saveDdlCache($cacheKey, self::DDL_DESCRIBE, $ddl);
So the problem that occurred in this question will be happened if we ever save the model that is going to be altered (add column, etc).
Because it has ever been $model->save(), the schema will be cached.
Later after he add new column and "do saving", it will load the schema from cache (which is not containing the new column) and resulting as: the data for new column is failed to be saved in database
Delete var/cache/* - your DB schema is cached by Magento even though the new column is already added to the MySQL table.