Основні дані щодо контексту основних даних


81

У мене є велике завдання імпорту, яке мені потрібно зробити з основними даними.
Скажімо, моя основна модель даних виглядає так:

Car
----
identifier 
type

Я отримую список інформації про автомобіль JSON зі свого сервера, а потім хочу синхронізувати його зі своїм основним Carоб’єктом даних , тобто:
Якщо це новий автомобіль -> створити новий Carоб’єкт Основних даних із нової інформації.
Якщо машина вже існує -> оновіть Carоб’єкт Core Data .

Тому я хочу зробити цей імпорт у фоновому режимі, не блокуючи інтерфейс, і поки використання прокручує подання таблиці автомобілів, що представляє всі машини.

На даний момент я роблю щось подібне:

// create background context
NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[bgContext setParentContext:self.mainContext];

[bgContext performBlock:^{
    NSArray *newCarsInfo = [self fetchNewCarInfoFromServer]; 

    // import the new data to Core Data...
    // I'm trying to do an efficient import here,
    // with few fetches as I can, and in batches
    for (... num of batches ...) {

        // do batch import...

        // save bg context in the end of each batch
        [bgContext save:&error];
    }

    // when all import batches are over I call save on the main context

    // save
    NSError *error = nil;
    [self.mainContext save:&error];
}];

Але я не зовсім впевнений, що роблю тут правильно, наприклад:

Це нормально, що я використовую setParentContext?
Я бачив кілька прикладів, які використовують це так, але я бачив інші приклади, які не викликають setParentContext, натомість вони роблять щось подібне:

NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
bgContext.persistentStoreCoordinator = self.mainContext.persistentStoreCoordinator;  
bgContext.undoManager = nil;

Ще одна річ, у якій я не впевнений, коли викликати save в основному контексті. У моєму прикладі я просто викликаю save наприкінці імпорту, але я бачив приклади, в яких використовується:

[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {
    NSManagedObjectContext *moc = self.managedObjectContext;
    if (note.object != moc) {
        [moc performBlock:^(){
            [moc mergeChangesFromContextDidSaveNotification:note];
        }];
    }
}];  

Як я вже згадував раніше, я хочу, щоб користувач міг взаємодіяти з даними під час оновлення, то що, якщо я користувач змінив тип автомобіля, а імпорт змінив ту саму машину, чи безпечно те, як я написав це?

ОНОВЛЕННЯ:

Завдяки чудовому поясненню @TheBasicMind я намагаюся реалізувати варіант А, тому мій код виглядає приблизно так:

Це конфігурація основних даних в AppDelegate:

AppDelegate.m  

#pragma mark - Core Data stack

- (void)saveContext {
    NSError *error = nil;
    NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
    if (managedObjectContext != nil) {
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
            DDLogError(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}  

// main
- (NSManagedObjectContext *)managedObjectContext {
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    _managedObjectContext.parentContext = [self saveManagedObjectContext];

    return _managedObjectContext;
}

// save context, parent of main context
- (NSManagedObjectContext *)saveManagedObjectContext {
    if (_writerManagedObjectContext != nil) {
        return _writerManagedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        _writerManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [_writerManagedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return _writerManagedObjectContext;
}  

І ось як зараз виглядає мій метод імпорту:

- (void)import {
    NSManagedObjectContext *saveObjectContext = [AppDelegate saveManagedObjectContext];

    // create background context
    NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    bgContext.parentContext = saveObjectContext;

    [bgContext performBlock:^{
        NSArray *newCarsInfo = [self fetchNewCarInfoFromServer];

        // import the new data to Core Data...
        // I'm trying to do an efficient import here,
        // with few fetches as I can, and in batches
        for (... num of batches ...) {

            // do batch import...

            // save bg context in the end of each batch
            [bgContext save:&error];
        }

        // no call here for main save...
        // instead use NSManagedObjectContextDidSaveNotification to merge changes
    }];
}  

І у мене також є такий спостерігач:

[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {

    NSManagedObjectContext *mainContext = self.managedObjectContext;
    NSManagedObjectContext *otherMoc = note.object;

    if (otherMoc.persistentStoreCoordinator == mainContext.persistentStoreCoordinator) {
        if (otherMoc != mainContext) {
            [mainContext performBlock:^(){
                [mainContext mergeChangesFromContextDidSaveNotification:note];
            }];
        }
    }
}];

Якщо ви використовуєте модель батьків-дітей, вам потрібно зберегти дитину, а потім батьків, щоб забезпечити коректне оновлення бази даних.
carloabelli

це частина мого запитання, чи слід використовувати модель батьків-дітей для такого роду завдань? і які у мене інші варіанти?
Eyal

1
ви повинні відвідати цей пост, вам стане в нагоді raywenderlich.com/15916/…
Хуліо Монтойя

1
@ cabellicar123 чому? З Посібника з програмування основних даних: Паралельність: Once all of the data has been consumed and turned into NSManagedObject instances, you call save on the private context, which moves all of the changes into the main queue context without blocking the main queue. developer.apple.com/library/ios/documentation/Cocoa/Conceptual/…
Діма Деплов

@Eyal "Я намагаюся зробити тут ефективний імпорт" Що це таке?
Сказано

Відповіді:


187

Це надзвичайно заплутана тема для людей, які вперше звертаються до Основних даних. Я не кажу це легковажно, але маючи досвід, я впевнений, що документація Apple дещо вводить в оману щодо цього питання (насправді це послідовно, якщо ви читаєте її дуже уважно, але вони недостатньо ілюструють, чому злиття даних залишається у багатьох випадках краще рішення, ніж покладатися на батьківський / дочірній контекст і просто економити від дитини до батьків).

Документація справляє сильне враження, що батьківський / дочірній контекст є новим кращим способом фонової обробки. Однак Apple нехтує виділити деякі сильні застереження. По-перше, майте на увазі, що все, що ви потрапляєте в контекст вашої дитини, спочатку витягується через батьківський. Тому найкраще обмежити будь-яку дочірню частину основного контексту, що працює в основному потоці, обробкою (редагуванням) даних, які вже були представлені в інтерфейсі користувача в основному потоці. Якщо ви використовуєте його для загальних завдань синхронізації, швидше за все, вам захочеться обробити дані, які виходять далеко за межі того, що ви зараз відображаєте в інтерфейсі користувача. Навіть якщо ви використовуєте NSPrivateQueueConcurrencyType, для дочірнього контексту редагування ви потенційно перетягуєте велику кількість даних через основний контекст, що може призвести до поганої продуктивності та блокування. Зараз найкраще не робити основний контекст дочірнім для контексту, який ви використовуєте для синхронізації, оскільки він не отримуватиме сповіщення про оновлення синхронізації, якщо ви не збираєтеся робити це вручну, плюс ви будете виконувати потенційно тривалі завдання на контексті, можливо, вам доведеться реагувати на збереження, ініційовані каскадом із контексту редагування, який є дочірнім елементом вашого основного контексту, через основний контакт і аж до сховища даних. Вам доведеться або об’єднати дані вручну, а також, можливо, відстежити, що потрібно зробити недійсним у основному контексті та повторно синхронізувати. Не найпростіший зразок. крім того, ви будете виконувати потенційно давно запущені завдання в контексті, який, можливо, вам доведеться реагувати на збереження, ініційовані як каскад, із контексту редагування, який є дочірнім елементом вашого основного контексту, через основний контакт і аж до сховища даних. Вам доведеться або об’єднати дані вручну, а також, можливо, відстежити, що потрібно зробити недійсним у основному контексті та повторно синхронізувати. Не найпростіший зразок. крім того, ви будете виконувати потенційно давно запущені завдання в контексті, який, можливо, вам доведеться реагувати на збереження, ініційовані як каскад, із контексту редагування, який є дочірнім елементом вашого основного контексту, через основний контакт і аж до сховища даних. Вам доведеться або об’єднати дані вручну, а також, можливо, відстежити, що потрібно зробити недійсним у основному контексті та повторно синхронізувати. Не найпростіший зразок.

У документації Apple не ясно, що вам, швидше за все, знадобиться гібрид методів, описаних на сторінках, що описують "старий" спосіб обмеження потоків, і новий спосіб здійснення дій у контексті батьків та дітей.

Найкраще, мабуть (і я даю загальне рішення тут, найкраще рішення може залежати від ваших детальних вимог) - мати контекст збереження NSPrivateQueueConcurrencyType як найвищий батько, який зберігає безпосередньо в сховищі даних. [Редагувати: ви не будете робити дуже багато безпосередньо в цьому контексті], тоді надайте цьому контексту збереження принаймні двох прямих дочірніх елементів. Один ваш основний контекст NSMainQueueConcurrencyType, який ви використовуєте для інтерфейсу користувача [Редагувати: найкраще бути дисциплінованим і уникати будь-якого редагування даних у цьому контексті], інший - NSPrivateQueueConcurrencyType, який ви використовуєте для редагування даних користувача, а також (у варіант А на доданій схемі) ваші завдання синхронізації.

Потім ви робите основний контекст цільовим повідомленням NSManagedObjectContextDidSave, генерованим контекстом синхронізації, і надсилаєте словник сповіщень .userInfo до mergeChangesFromContextDidSaveNotification :.

Наступним питанням, яке слід розглянути, є те, куди ви поміщаєте контекст редагування користувача (контекст, де внесені користувачем зміни відображаються назад в інтерфейсі). Якщо дії користувача завжди обмежуються редагуванням невеликих обсягів представлених даних, то зробити це дочірнім елементом основного контексту за допомогою NSPrivateQueueConcurrencyType - найкращий вибір і найпростіший в управлінні (збереження дозволить зберегти зміни безпосередньо в основному контексті і якщо у вас є NSFetchedResultsController, відповідний метод делегування буде викликаний автоматично, щоб ваш інтерфейс міг обробляти контролер оновлень: didChangeObject: atIndexPath: forChangeType: newIndexPath :) (знову це опція A).

Якщо, з іншого боку, дії користувачів можуть призвести до обробки великих обсягів даних, можливо, ви захочете зробити його іншим рівнем основного контексту та контексту синхронізації, таким чином, щоб контекст збереження мав трьох прямих дочірніх елементів. main , синхронізувати (тип приватної черги) та редагувати (тип приватної черги). Я показав це розташування як варіант В на схемі.

Подібно до контексту синхронізації, вам потрібно буде [Редагувати: налаштувати основний контекст для отримання сповіщень], коли дані зберігаються (або якщо вам потрібна більша деталізація, коли дані оновлюються) і вжити заходів для об’єднання даних (як правило, за допомогою mergeChangesFromContextDidSaveNotification: ). Зверніть увагу, що за такої домовленості немає необхідності, щоб основний контекст коли-небудь викликав метод save:. введіть тут опис зображення

Щоб зрозуміти стосунки батьків / дітей, візьміть варіант А: Підхід батьків-дочір просто означає, що якщо контекст редагування вибирає NSManagedObjects, вони будуть "скопійовані" (зареєстровані) спочатку контекстом збереження, потім основним контекстом, а потім нарешті редагувати контекст. Ви зможете внести до них зміни, тоді коли ви зателефонуєте зберегти: у контексті редагування зміни будуть збережені лише в основному контексті . Вам доведеться викликати save: в основному контексті, а потім викликати save: у контексті збереження, перш ніж вони будуть записані на диск.

Коли ви зберігаєте від дитини, до батьків, різні NSManagedObject сповіщення про зміну та збереження запускаються. Так, наприклад, якщо ви використовуєте контролер результатів отримання, щоб керувати вашими даними для вашого інтерфейсу, то будуть викликані його методи делегування, щоб ви могли оновити інтерфейс відповідно.

Деякі наслідки: Якщо ви отримуєте об'єкт та NSManagedObject A у контексті редагування, змініть його та збережіть, щоб модифікації повернулися до основного контексту. Тепер ви маєте зареєстрований модифікований об’єкт як для основного, так і для контексту редагування. Це було б поганим стилем, але тепер ви можете змінити об'єкт знову в основному контексті, і він тепер буде відрізнятися від об'єкта, оскільки він зберігається в контексті редагування. Якщо після цього ви спробуєте внести подальші зміни в об’єкт, що зберігається в контексті редагування, ваші зміни не будуть синхронізовані з об’єктом в основному контексті, і будь-яка спроба зберегти контекст редагування призведе до помилки.

З цієї причини, з таким розташуванням, як варіант А, добре спробувати отримати об’єкти, змінити їх, зберегти та скинути контекст редагування (наприклад, [editContext reset] з будь-якою окремою ітерацією циклу циклу (або в межах будь-який заданий блок, переданий в [editContext performBlock:]). Також найкраще бути дисциплінованим та уникати будь- яких змін у основному контексті. Крім того, повторити ітерацію, оскільки вся обробка на main є основним потоком, якщо ви отримуєте багато об'єктів в контексті редагування, основний контекст буде робити його обробку вибірки в основному потоціоскільки ці об’єкти ітеративно копіюються з батьківського в дочірній контекст. Якщо обробляється багато даних, це може спричинити невідповідність інтерфейсу користувача. Отже, якщо, наприклад, у вас є великий магазин керованих об’єктів, і у вас є опція інтерфейсу, що призведе до того, що всі вони будуть відредаговані. В цьому випадку буде поганою ідеєю налаштувати додаток, як варіант А. У такому випадку варіант Б є кращим варіантом.

Якщо ви не обробляєте тисячі об’єктів, то варіанту A може бути цілком достатньо.

До речі, не дуже турбуйтеся, який варіант ви вибрали. Можливо, було б непогано почати з А, і якщо вам потрібно змінити на В. Це зробити простіше, ніж ви думаєте, і, як правило, має менше наслідків, ніж можна було очікувати.


дякую (та +1) за вашу детальну відповідь, на цьому етапі моєї розробки це буде великою зміною, додаючи ці додаткові контексти. Я дійсно хочу використовувати лише один додатковий контекст, щоб виконати це фонове завдання та зробити його максимально простим. Я все ще не розумію різниці між підходом батьків та дітей та іншими альтернативами. Я вдячний, якщо ви можете взяти мій приклад коду та виправити будь-які неправильні кроки, які я там зробив, що буде простіше для мене зрозуміти та зробити.
Eyal

Візьміть варіант A. Підхід батьківського дочірника просто означає, що якщо контекст редагування вибирає NSManagedObjects, вони будуть "скопійовані" (зареєстровані) спочатку контекстом збереження, потім основним контекстом, а потім нарешті редагувати контекст. Ви зможете внести до них зміни, тоді коли ви зателефонуєте зберегти: у контексті редагування зміни будуть збережені лише в основному контексті . Вам доведеться викликати save: у основному контексті, а потім викликати save: on the save, перш ніж вони будуть записані на диск. Я зміню свою відповідь.
TheBasicMind

3
Я дуже рада, що ви вирішили відповісти тут, ваша інформація безцінна. Я думаю, що зараз я отримав більше впевненості, щоб спробувати це, мені просто потрібно зробити якесь розпорядження до всієї цієї чудової інформації в моїй голові. Контекст редагування мене трохи бентежить, яка різниця між ним та будь-яким іншим фоновим контекстом? чому він дитина, а контекст синхронізації - ні? У будь-якому разі, я думаю, що я не буду використовувати окремий контекст для редагування користувачів, оскільки користувач може видаляти лише один елемент (автомобіль) за раз або змінювати властивість по одному, тому я роблю кожне редагування з основним контекстом і безпосередньо викликаю економити: на ньому.
Eyal

Я також оновив своє запитання яким-небудь новим кодом, який я написав, намагаючись реалізувати варіант A (за винятком дочірнього редагування контексту). чи можете ви, будь ласка, перевірити, чи добре, я не впевнений, чи потрібно мені дзвонити save: у контексті Зберегти де-небудь? чи досить об’єднати зміни в повідомленні?
Eyal,

@TheBasicMind, коли час зберігати контекст "збереження"? Ви робите це незалежно від взаємодії з користувачем?
ctietze

15

По-перше, батьківський / дочірній контекст не призначений для фонової обробки. Вони призначені для атомного оновлення пов’язаних даних, які можуть бути створені в декількох контролерах перегляду. Отже, якщо останній контролер подання скасовано, дочірній контекст може бути викинутий без негативних наслідків для батьків. Це повністю пояснює Apple внизу цієї відповіді на [^ 1]. Тепер це не заважає, і ви не впали на типову помилку, ви можете зосередитися на тому, як правильно робити фонові основні дані.

Створіть новий постійний координатор магазину (більше не потрібен в iOS 10, див. Оновлення нижче) та контекст приватної черги. Прослухайте сповіщення про збереження та об’єднайте зміни в основний контекст (на iOS 10 контекст має властивість робити це автоматично)

Зразок від Apple див. У розділі "Землетруси: заповнення основного сховища даних за допомогою фонової черги" https://developer.apple.com/library/mac/samplecode/Earthquakes/Introduction/Intro.html Як видно з історії редагувань 19.08.2014 р. вони додали "Новий зразок коду, який показує, як використовувати другий стек основних даних для отримання даних у фоновій черзі".

Ось цей біт від AAPLCoreDataStackManager.m:

// Creates a new Core Data stack and returns a managed object context associated with a private queue.
- (NSManagedObjectContext *)createPrivateQueueContext:(NSError * __autoreleasing *)error {

    // It uses the same store and model, but a new persistent store coordinator and context.
    NSPersistentStoreCoordinator *localCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[AAPLCoreDataStackManager sharedManager].managedObjectModel];

    if (![localCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil
                                                  URL:[AAPLCoreDataStackManager sharedManager].storeURL
                                              options:nil
                                                error:error]) {
        return nil;
    }

    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [context performBlockAndWait:^{
        [context setPersistentStoreCoordinator:localCoordinator];

        // Avoid using default merge policy in multi-threading environment:
        // when we delete (and save) a record in one context,
        // and try to save edits on the same record in the other context before merging the changes,
        // an exception will be thrown because Core Data by default uses NSErrorMergePolicy.
        // Setting a reasonable mergePolicy is a good practice to avoid that kind of exception.
        context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;

        // In OS X, a context provides an undo manager by default
        // Disable it for performance benefit
        context.undoManager = nil;
    }];
    return context;
}

І в AAPLQuakesViewController.m

- (void)contextDidSaveNotificationHandler:(NSNotification *)notification {

    if (notification.object != self.managedObjectContext) {

        [self.managedObjectContext performBlock:^{
            [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
        }];
    }
}

Ось повний опис конструкції зразка:

Землетруси: використання "приватного" постійного координатора сховища для отримання даних у фоновому режимі

У більшості програм, що використовують Основні дані, використовується єдиний постійний координатор сховища для посередницького доступу до певного постійного сховища. Землетруси показують, як використовувати додатковий "приватний" постійний координатор магазину при створенні керованих об'єктів за допомогою даних, отриманих з віддаленого сервера.

Архітектура додатків

Додаток використовує два "стеки" основних даних (що визначається існуванням постійного координатора сховища). Перший - це типовий стек "загального призначення"; другий створюється контролером перегляду спеціально для отримання даних з віддаленого сервера (станом на iOS 10 другий координатор більше не потрібен, див. оновлення внизу відповіді).

Основний постійний координатор сховища продається єдиним об’єктом «контролера стеку» (екземпляр CoreDataStackManager). Клієнти несуть відповідальність за створення контексту керованого об'єкта для роботи з координатором [^ 1]. Контролер стека також надає властивості для керованої об'єктної моделі, що використовується додатком, і розташування постійного сховища. Клієнти можуть використовувати ці останні властивості для створення додаткових постійних координаторів магазинів, які працюватимуть паралельно з головним координатором.

Основний контролер подання, екземпляр QuakesViewController, використовує постійний координатор сховища контролера стеку, щоб отримати землетруси з постійного сховища для відображення у поданні таблиці. Отримання даних із сервера може бути довготривалою операцією, яка вимагає значної взаємодії з постійним сховищем, щоб визначити, чи є записи, отримані з сервера, новими землетрусами чи потенційними оновленнями існуючих землетрусів. Щоб програма могла залишатися чутливою під час цієї операції, контролер подання використовує другого координатора для управління взаємодією з постійним сховищем. Він налаштовує координатора на використання тієї самої керованої об'єктної моделі та постійного сховища, що і основний координатор, проданий контролером стека.

[^ 1]: Це підтримує підхід "передавати естафету", завдяки якому - особливо в додатках iOS - контекст передається від одного контролера перегляду до іншого. Кореневий контролер подання відповідає за створення початкового контексту та передачу його контролерам дочірнього перегляду за необхідності.

Причиною цього шаблону є забезпечення належного обмеження змін у графіку керованого об’єкта. Основні дані підтримують "вкладені" контексти керованих об'єктів, які забезпечують гнучку архітектуру, яка полегшує підтримку незалежних, скасовуваних, змінних наборів. За допомогою дочірнього контексту ви можете дозволити користувачеві вносити набір змін до керованих об’єктів, які потім можуть або бути передані оптом для батьків (і в кінцевому рахунку збережені в сховищі) як одна транзакція, або відкинуті. Якщо всі частини програми просто отримують той самий контекст, скажімо, від делегата програми, це ускладнює або неможливо підтримати цю поведінку.

Оновлення: У iOS 10 Apple перемістила синхронізацію з рівня файлу sqlite на постійний координатор. Це означає, що тепер ви можете створити приватний контекст черги та повторно використати існуючий координатор, що використовується основним контекстом, без тих самих проблем із продуктивністю, які ви мали б робити таким чином раніше, класно!


7

До речі це документ Apple дуже чітко пояснює цю проблему. Швидка версія вище для всіх, хто цікавиться

let jsonArray = … //JSON data to be imported into Core Data
let moc = … //Our primary context on the main queue

let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.parentContext = moc

privateMOC.performBlock {
    for jsonObject in jsonArray {
        let mo = … //Managed object that matches the incoming JSON structure
        //update MO with data from the dictionary
    }
    do {
        try privateMOC.save()
        moc.performBlockAndWait {
            do {
                try moc.save()
            } catch {
                fatalError("Failure to save context: \(error)")
            }
        }
    } catch {
        fatalError("Failure to save context: \(error)")
    }
}

І ще простіше, якщо ви використовуєте NSPersistentContainer для iOS 10 і новіших версій

let jsonArray = …
let container = self.persistentContainer
container.performBackgroundTask() { (context) in
    for jsonObject in jsonArray {
        let mo = CarMO(context: context)
        mo.populateFromJSON(jsonObject)
    }
    do {
        try context.save()
    } catch {
        fatalError("Failure to save context: \(error)")
    }
}

як отримати дані, ви можете навести приклад, тому що я отримую випадкові збої під час отримання та іноді працюю належним чином. @hariszaman
Parth

Можливо, ви намагаєтесь одночасно змінити та зберегти контекст
hariszaman
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.